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}