Skip to main content

kevy_alloc/
os.rs

1//! The OS boundary: anonymous mapping, unmapping, and returning pages.
2//!
3//! Three hand-declared `extern "C"` symbols, no `libc` crate — the house
4//! rule for OS boundaries. Linux and macOS only; elsewhere every entry
5//! point reports failure and the allocator is simply unavailable.
6//!
7//! # Why not `kevy-madvise`
8//!
9//! That crate already binds `mmap`/`munmap`/`madvise`, so reusing it was
10//! the first choice. It does not fit: it is Linux-only by construction
11//! and its contract *is* huge-page advice — every mapping it hands out
12//! has `MADV_HUGEPAGE` applied. An allocator needs mappings on macOS too
13//! (that is where this is developed), and it must be able to *return*
14//! pages, which is the property the whole experiment rests on. Widening
15//! a crate whose name is its contract costs more than three extern
16//! declarations, so the boundary lives here — which is also why
17//! `kevy-alloc` is in the recorded unsafe set (allocgate M8).
18
19#[cfg(any(target_os = "linux", target_os = "macos"))]
20use core::ffi::c_void;
21use core::ptr::NonNull;
22
23#[cfg(any(target_os = "linux", target_os = "macos"))]
24unsafe extern "C" {
25    fn mmap(
26        addr: *mut c_void,
27        length: usize,
28        prot: i32,
29        flags: i32,
30        fd: i32,
31        offset: i64,
32    ) -> *mut c_void;
33    fn munmap(addr: *mut c_void, length: usize) -> i32;
34    fn madvise(addr: *mut c_void, length: usize, advice: i32) -> i32;
35}
36
37#[cfg(any(target_os = "linux", target_os = "macos"))]
38const PROT_READ: i32 = 0x1;
39#[cfg(any(target_os = "linux", target_os = "macos"))]
40const PROT_WRITE: i32 = 0x2;
41#[cfg(any(target_os = "linux", target_os = "macos"))]
42const MAP_PRIVATE: i32 = 0x2;
43
44#[cfg(target_os = "linux")]
45const MAP_ANONYMOUS: i32 = 0x20;
46#[cfg(target_os = "macos")]
47const MAP_ANONYMOUS: i32 = 0x1000;
48
49/// Discard the contents of a resident range and return the physical
50/// pages to the OS, keeping the mapping addressable.
51///
52/// Linux `MADV_DONTNEED` (4) drops the pages outright: RSS falls and a
53/// later touch faults in a zero page. macOS has no equivalent that
54/// *guarantees* the drop — `MADV_FREE` (5) marks pages reclaimable and
55/// the kernel takes them under pressure, so RSS may not move promptly.
56/// The difference is why M4 (reclaim proven directly) is asserted on
57/// Linux and reported as informational on macOS rather than being
58/// quietly assumed to hold on both.
59#[cfg(target_os = "linux")]
60const MADV_DISCARD: i32 = 4;
61#[cfg(target_os = "macos")]
62const MADV_DISCARD: i32 = 5;
63
64/// The system page size this module assumes for rounding.
65pub const PAGE: usize = 4096;
66
67/// Round `n` up to a multiple of `align`, which must be a power of two.
68#[must_use]
69pub const fn round_up(n: usize, align: usize) -> usize {
70    (n + align - 1) & !(align - 1)
71}
72
73/// Map `len` bytes anonymously with the returned address aligned to
74/// `align` bytes.
75///
76/// `align` must be a power of two and a multiple of [`PAGE`]; `len` must
77/// be a non-zero multiple of `align`. Over-allocates by one alignment
78/// unit and trims both sides, because `mmap` only promises page
79/// alignment. Returns `None` on failure — never panics, because an
80/// allocator that panics on OOM is worse than one that reports it.
81pub fn map_aligned(len: usize, align: usize) -> Option<NonNull<u8>> {
82    if len == 0 || !align.is_power_of_two() || !len.is_multiple_of(align) {
83        return None;
84    }
85    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
86    {
87        None
88    }
89    #[cfg(any(target_os = "linux", target_os = "macos"))]
90    {
91        if cfg!(miri) {
92            return None;
93        }
94        let total = len.checked_add(align)?;
95        // SAFETY: the canonical anonymous mapping call. No Rust memory is
96        // read or written; a null hint lets the kernel choose the address.
97        let raw = unsafe {
98            mmap(
99                core::ptr::null_mut(),
100                total,
101                PROT_READ | PROT_WRITE,
102                MAP_PRIVATE | MAP_ANONYMOUS,
103                -1,
104                0,
105            )
106        };
107        if raw as isize == -1 {
108            return None;
109        }
110        NonNull::new(trim(raw as usize, total, len, align) as *mut u8)
111    }
112}
113
114/// Trim an over-allocated mapping down to `len` bytes starting at the
115/// first `align`-aligned address inside it, unmapping both offcuts.
116#[cfg(any(target_os = "linux", target_os = "macos"))]
117fn trim(raw: usize, total: usize, len: usize, align: usize) -> usize {
118    let start = (raw + align - 1) & !(align - 1);
119    let prefix = start - raw;
120    let suffix = total - prefix - len;
121    if prefix > 0 {
122        // SAFETY: `prefix` bytes at `raw` are part of the mapping we
123        // just made and are not otherwise referenced.
124        unsafe { munmap(raw as *mut c_void, prefix) };
125    }
126    if suffix > 0 {
127        // SAFETY: same mapping, the tail past the aligned region.
128        unsafe { munmap((start + len) as *mut c_void, suffix) };
129    }
130    start
131}
132
133/// Unmap `len` bytes at `ptr`.
134///
135/// # Safety
136/// `ptr`/`len` must describe a live mapping produced by [`map_aligned`]
137/// (or a whole sub-range of one that is no longer referenced).
138pub unsafe fn unmap(ptr: NonNull<u8>, len: usize) {
139    #[cfg(any(target_os = "linux", target_os = "macos"))]
140    {
141        if cfg!(miri) {
142            return;
143        }
144        // SAFETY: delegated to the caller's contract.
145        unsafe { munmap(ptr.as_ptr().cast(), len) };
146    }
147    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
148    {
149        let _ = (ptr, len);
150    }
151}
152
153/// Return the physical pages backing `len` bytes at `ptr` to the OS
154/// while keeping the range mapped and addressable.
155///
156/// The range must be page-aligned and a whole number of pages. Contents
157/// are discarded: a later read sees zeroes, which is why only spans with
158/// no live slots are ever passed here.
159///
160/// # Safety
161/// `ptr`/`len` must lie inside a live mapping from [`map_aligned`], and
162/// no live data may remain in the range.
163pub unsafe fn discard(ptr: NonNull<u8>, len: usize) -> bool {
164    #[cfg(any(target_os = "linux", target_os = "macos"))]
165    {
166        if cfg!(miri) {
167            return false;
168        }
169        // SAFETY: delegated to the caller's contract; madvise reads no
170        // Rust memory.
171        unsafe { madvise(ptr.as_ptr().cast(), len, MADV_DISCARD) == 0 }
172    }
173    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
174    {
175        let _ = (ptr, len);
176        false
177    }
178}
179
180/// Whether this target can map memory at all. Used by tests and by the
181/// heap's construction path to fail fast rather than mysteriously.
182#[must_use]
183pub const fn available() -> bool {
184    cfg!(any(target_os = "linux", target_os = "macos")) && !cfg!(miri)
185}