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    fn sysconf(name: i32) -> i64;
36}
37
38/// `_SC_PAGESIZE`. Linux says 30, macOS says 29 — the one constant in
39/// this file that is not shared, which is itself why it is worth asking
40/// the system rather than assuming.
41#[cfg(target_os = "linux")]
42const SC_PAGESIZE: i32 = 30;
43#[cfg(target_os = "macos")]
44const SC_PAGESIZE: i32 = 29;
45
46#[cfg(any(target_os = "linux", target_os = "macos"))]
47const PROT_READ: i32 = 0x1;
48#[cfg(any(target_os = "linux", target_os = "macos"))]
49const PROT_WRITE: i32 = 0x2;
50#[cfg(any(target_os = "linux", target_os = "macos"))]
51const MAP_PRIVATE: i32 = 0x2;
52
53#[cfg(target_os = "linux")]
54const MAP_ANONYMOUS: i32 = 0x20;
55#[cfg(target_os = "macos")]
56const MAP_ANONYMOUS: i32 = 0x1000;
57
58/// Discard the contents of a resident range and return the physical
59/// pages to the OS, keeping the mapping addressable.
60///
61/// Linux `MADV_DONTNEED` (4) drops the pages outright: RSS falls and a
62/// later touch faults in a zero page. macOS has no equivalent that
63/// *guarantees* the drop — `MADV_FREE` (5) marks pages reclaimable and
64/// the kernel takes them under pressure, so RSS may not move promptly.
65/// The difference is why M4 (reclaim proven directly) is asserted on
66/// Linux and reported as informational on macOS rather than being
67/// quietly assumed to hold on both.
68#[cfg(target_os = "linux")]
69const MADV_DISCARD: i32 = 4;
70#[cfg(target_os = "macos")]
71const MADV_DISCARD: i32 = 5;
72
73/// The system page size this module assumes for rounding.
74///
75/// It is a constant because the geometry is: `PAGES_PER_SPAN` is
76/// `SPAN_BYTES / PAGE`, and `SpanMeta::discarded` is exactly a `u16` for
77/// the sixteen pages that gives. What is NOT safe is assuming the
78/// system agrees — see [`page_size_matches`].
79pub const PAGE: usize = 4096;
80
81/// Whether the running system's page size is the one the geometry above
82/// was built for.
83///
84/// This is not pedantry. The development machine for this crate reports
85/// 16384 and the bench box reports 4096, and the reclaim path issues
86/// `madvise` on ranges computed at 4096 granularity. On a 16 KiB-page
87/// system those ranges are not page-aligned; macOS answers 0 anyway and
88/// reclaims nothing, and the three callers in `reclaim.rs` discard the
89/// return value — so the allocator marks the pages discarded, counts
90/// them in `returned`, lowers `predicted_resident()`, and the kernel
91/// hands back nothing at all.
92///
93/// A measuring device that fails in the shape of data. The README's
94/// headline "plus returning the pages = 29.3 ns/op" was taken on the
95/// 16384 machine, which means it timed a run of `madvise` calls that
96/// could not do what the line says they did.
97///
98/// Answered once and cached: `sysconf` is a call, and this sits under
99/// the reclaim tick.
100///
101/// ```
102/// // Stable across calls — it is answered once and cached, and an
103/// // answer that flapped would be worse than either value.
104/// let a = kevy_alloc::os::page_size_matches();
105/// assert_eq!(a, kevy_alloc::os::page_size_matches());
106/// ```
107#[cfg(any(target_os = "linux", target_os = "macos"))]
108pub fn page_size_matches() -> bool {
109    use core::sync::atomic::{AtomicU8, Ordering};
110    static ANSWER: AtomicU8 = AtomicU8::new(0); // 0 unknown, 1 yes, 2 no
111    if let Some(known) = decode_memo(ANSWER.load(Ordering::Relaxed)) {
112        return known;
113    }
114    // SAFETY: `sysconf` reads no Rust memory and takes an int.
115    let got = unsafe { sysconf(SC_PAGESIZE) };
116    let ok = usable_page_size(got);
117    ANSWER.store(encode_memo(ok), Ordering::Relaxed);
118    ok
119}
120
121/// The memo's three states, as a function of the stored byte.
122///
123/// Every machine takes exactly one of `1` and `2` forever, so whichever
124/// it is not is unreachable code there — a permanently dead region on
125/// any single platform's coverage run. Splitting the decode out makes
126/// all three answerable from a test anywhere.
127#[cfg(any(target_os = "linux", target_os = "macos"))]
128const fn decode_memo(stored: u8) -> Option<bool> {
129    match stored {
130        1 => Some(true),
131        2 => Some(false),
132        _ => None,
133    }
134}
135
136/// The inverse of [`decode_memo`], kept beside it so the two cannot
137/// drift into disagreeing about which byte means what.
138#[cfg(any(target_os = "linux", target_os = "macos"))]
139const fn encode_memo(answer: bool) -> u8 {
140    if answer { 1 } else { 2 }
141}
142
143/// The decision, separated from the syscall that supplies it.
144///
145/// Every machine gives one answer, so the other branch cannot be
146/// executed where it runs — which is how a coverage ratchet ends up
147/// holding a permanently dead region, and how the interesting half (a
148/// mismatch: the one that makes reclaim inert) stays untested on
149/// exactly the machines where it is false. Taking the measurement as an
150/// argument makes both answers reachable from a test anywhere.
151#[must_use]
152pub(crate) fn usable_page_size(measured: i64) -> bool {
153    measured > 0 && measured as u64 == PAGE as u64
154}
155
156/// Non-Unix has no reclaim path at all, so nothing can be misreported.
157#[cfg(not(any(target_os = "linux", target_os = "macos")))]
158pub fn page_size_matches() -> bool {
159    false
160}
161
162/// Round `n` up to a multiple of `align`, which must be a power of two.
163#[must_use]
164pub const fn round_up(n: usize, align: usize) -> usize {
165    (n + align - 1) & !(align - 1)
166}
167
168/// Map `len` bytes anonymously with the returned address aligned to
169/// `align` bytes.
170///
171/// `align` must be a power of two and a multiple of [`PAGE`]; `len` must
172/// be a non-zero multiple of `align`. Over-allocates by one alignment
173/// unit and trims both sides, because `mmap` only promises page
174/// alignment. Returns `None` on failure — never panics, because an
175/// allocator that panics on OOM is worse than one that reports it.
176pub fn map_aligned(len: usize, align: usize) -> Option<NonNull<u8>> {
177    if len == 0 || !align.is_power_of_two() || !len.is_multiple_of(align) {
178        return None;
179    }
180    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
181    {
182        None
183    }
184    #[cfg(any(target_os = "linux", target_os = "macos"))]
185    {
186        if cfg!(miri) {
187            return None;
188        }
189        let total = len.checked_add(align)?;
190        // SAFETY: the canonical anonymous mapping call. No Rust memory is
191        // read or written; a null hint lets the kernel choose the address.
192        let raw = unsafe {
193            mmap(
194                core::ptr::null_mut(),
195                total,
196                PROT_READ | PROT_WRITE,
197                MAP_PRIVATE | MAP_ANONYMOUS,
198                -1,
199                0,
200            )
201        };
202        if raw as isize == -1 {
203            return None;
204        }
205        NonNull::new(trim(raw as usize, total, len, align) as *mut u8)
206    }
207}
208
209/// Trim an over-allocated mapping down to `len` bytes starting at the
210/// first `align`-aligned address inside it, unmapping both offcuts.
211#[cfg(any(target_os = "linux", target_os = "macos"))]
212fn trim(raw: usize, total: usize, len: usize, align: usize) -> usize {
213    let start = (raw + align - 1) & !(align - 1);
214    let prefix = start - raw;
215    let suffix = total - prefix - len;
216    if prefix > 0 {
217        // SAFETY: `prefix` bytes at `raw` are part of the mapping we
218        // just made and are not otherwise referenced.
219        unsafe { munmap(raw as *mut c_void, prefix) };
220    }
221    if suffix > 0 {
222        // SAFETY: same mapping, the tail past the aligned region.
223        unsafe { munmap((start + len) as *mut c_void, suffix) };
224    }
225    start
226}
227
228/// Unmap `len` bytes at `ptr`.
229///
230/// # Safety
231/// `ptr`/`len` must describe a live mapping produced by [`map_aligned`]
232/// (or a whole sub-range of one that is no longer referenced).
233pub unsafe fn unmap(ptr: NonNull<u8>, len: usize) {
234    #[cfg(any(target_os = "linux", target_os = "macos"))]
235    {
236        if cfg!(miri) {
237            return;
238        }
239        // SAFETY: delegated to the caller's contract.
240        unsafe { munmap(ptr.as_ptr().cast(), len) };
241    }
242    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
243    {
244        let _ = (ptr, len);
245    }
246}
247
248/// Return the physical pages backing `len` bytes at `ptr` to the OS
249/// while keeping the range mapped and addressable.
250///
251/// The range must be page-aligned and a whole number of pages. Contents
252/// are discarded: a later read sees zeroes, which is why only spans with
253/// no live slots are ever passed here.
254///
255/// # Safety
256/// `ptr`/`len` must lie inside a live mapping from [`map_aligned`], and
257/// no live data may remain in the range.
258pub unsafe fn discard(ptr: NonNull<u8>, len: usize) -> bool {
259    #[cfg(any(target_os = "linux", target_os = "macos"))]
260    {
261        if cfg!(miri) {
262            return false;
263        }
264        // SAFETY: delegated to the caller's contract; madvise reads no
265        // Rust memory.
266        unsafe { madvise(ptr.as_ptr().cast(), len, MADV_DISCARD) == 0 }
267    }
268    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
269    {
270        let _ = (ptr, len);
271        false
272    }
273}
274
275/// Whether this target can map memory at all. Used by tests and by the
276/// heap's construction path to fail fast rather than mysteriously.
277#[must_use]
278pub const fn available() -> bool {
279    cfg!(any(target_os = "linux", target_os = "macos")) && !cfg!(miri)
280}
281
282#[cfg(test)]
283mod page_size_tests {
284    use super::{PAGE, usable_page_size};
285
286    /// The memo round-trips, and its unknown state is distinct from
287    /// both answers. Whichever of the two a machine stores, the other
288    /// arm is unreachable there — which is why this is tested through a
289    /// pure function rather than left to a coverage run that can only
290    /// ever see one of them.
291    #[test]
292    fn the_memo_round_trips_and_unknown_is_neither_answer() {
293        use super::{decode_memo, encode_memo};
294        assert_eq!(decode_memo(encode_memo(true)), Some(true));
295        assert_eq!(decode_memo(encode_memo(false)), Some(false));
296        assert_eq!(decode_memo(0), None, "0 is unasked, not an answer");
297        assert_ne!(encode_memo(true), 0, "an answer must not read as unasked");
298        assert_ne!(encode_memo(false), 0);
299    }
300
301    /// Both answers, including the one this machine cannot give. The
302    /// 16384 case is not hypothetical — it is every Apple Silicon Mac,
303    /// and it is the case in which page-granular reclaim does nothing.
304    #[test]
305    fn only_an_exact_match_is_usable() {
306        assert!(usable_page_size(PAGE as i64));
307        assert!(!usable_page_size(16384), "a 16 KiB page is not our 4 KiB arithmetic");
308        assert!(!usable_page_size(1024), "a smaller page misaligns the same way");
309        // `sysconf` reports failure as -1, and a negative cast to
310        // unsigned is how a refusal becomes an enormous page size.
311        assert!(!usable_page_size(-1), "a failed sysconf must not read as a match");
312        assert!(!usable_page_size(0));
313    }
314}