rusty_alloc/lib.rs
1//! rusty_alloc core — a pure-Rust remake of mimalloc v2.4.5.
2//!
3//! Plan of record: `docs/plans/rusty_alloc_v1.md`. Module map mirrors upstream C
4//! files 1:1 (plan §6) so every diff-vs-oracle conversation has a shared map.
5//!
6//! Milestone status: **M4** — per-thread heaps, lock-free cross-thread frees
7//! (the loom-modeled xthread/delayed protocol), thread-exit abandonment and
8//! segment reclaim. No global lock anywhere on the alloc/free paths.
9//!
10//! std note: M4's TLS fast path uses `thread_local!` (const-init, !Drop — the
11//! R1 spike measured it at atomic-load parity). A no_std profile returns
12//! post-v1 with the nightly `#[thread_local]` or a platform TLS shim.
13
14#![cfg_attr(not(feature = "std"), no_std)]
15#![deny(missing_docs)]
16
17// ---------------------------------------------------------------------------
18// P3 of `docs/plans/small-metal.md`: the three things the crate used `std` FOR.
19//
20// These live here, above the `mod` lines, because `macro_rules!` is TEXTUALLY
21// scoped — a macro defined after a module is invisible inside it.
22// ---------------------------------------------------------------------------
23
24/// End the process immediately, without unwinding.
25///
26/// A double free, a corrupted free list and a failed TLS slot all reach this:
27/// the allocator's contract is that it aborts rather than continues, and
28/// unwinding out of `free` into a C caller is not an option (which is why the
29/// release profile is `panic = "abort"`).
30///
31/// Without `std` there is no `process::abort`, so this panics and relies on the
32/// deliverable's panic strategy. **A `no_std` consumer MUST build with
33/// `panic = "abort"`** — every Janus firmware profile already does — or an
34/// abort becomes an unwind and the guarantee is gone.
35#[cold]
36#[inline(never)]
37pub(crate) fn abort() -> ! {
38 #[cfg(feature = "std")]
39 {
40 std::process::abort()
41 }
42 #[cfg(not(feature = "std"))]
43 {
44 panic!("rusty_alloc: abort (build a no_std consumer with panic = \"abort\")")
45 }
46}
47
48/// A `thread_local!` that survives `no_std` — the single-heap profile.
49///
50/// With `std` this expands to `std::thread_local!` unchanged, so the shipped
51/// build keeps the const-init, `!Drop`, initial-exec fast path M10c measured.
52///
53/// Without it there is no thread-local storage and, on the targets this crate
54/// serves without `std`, no second thread either: `prim::fixed::thread_id`
55/// returns a constant and its TLS is a fixed table whose destructors never run,
56/// because there is no thread exit. So a "thread-local" becomes a plain
57/// `static` — which is not a compromise but the point of the profile: one heap,
58/// no TLS lookup at all, a SHORTER fast path than the threaded one.
59/// **`wasm32-unknown-unknown` takes the single-`static` arm too, not just
60/// `no_std`.** That target has exactly one thread unless the atomics+threads
61/// proposal is on, which `prim/wasm.rs` has assumed since it was written. A
62/// `std::thread_local!` there still links lazy initialisation, destructor
63/// registration and the "accessed during or after destruction" panic — none of
64/// which can ever run — and the strings for it ship in every module.
65///
66/// `target_feature = "atomics"` is the precise switch: it is what
67/// `-C target-feature=+atomics` sets to build wasm WITH threads, and such a
68/// build keeps real TLS.
69macro_rules! ra_thread_local {
70 ($($(#[$m:meta])* static $N:ident: $T:ty = const $init:block;)*) => {
71 #[cfg(all(feature = "std", not(all(target_arch = "wasm32", target_os = "unknown", not(target_feature = "atomics")))))]
72 std::thread_local! {
73 $($(#[$m])* static $N: $T = const $init;)*
74 }
75 $(
76 #[cfg(any(not(feature = "std"), all(target_arch = "wasm32", target_os = "unknown", not(target_feature = "atomics"))))]
77 $(#[$m])*
78 static $N: $crate::SingleThreadCell<$T> =
79 $crate::SingleThreadCell::new($init);
80 )*
81 };
82}
83
84// The `no_std` build asserts single-threadedness, so it must be OPTED INTO.
85//
86// Three things in a `no_std` build are sound only because there is exactly one
87// thread: [`SingleThreadCell`]'s `unsafe impl Sync`, `prim::fixed`'s constant
88// thread id and never-contended spin lock, and `options`' 64-bit atomics split
89// into `AtomicU32` halves. None of them is checkable at compile time, and none
90// of them fails loudly if the assumption breaks — they corrupt quietly.
91//
92// A doc comment is not a guard. `no_std` here therefore requires
93// `--cfg ra_single_threaded`, so that using this allocator on a bare-metal
94// target is a decision somebody wrote down rather than a default they
95// inherited. There is no cost to it and no way around it:
96//
97// ```text
98// RUSTFLAGS="--cfg ra_single_threaded" cargo build --no-default-features
99// ```
100//
101// If your target has more than one thread touching the allocator, do not set
102// it — enable the `std` feature instead, or the port is not done.
103#[cfg(all(not(feature = "std"), not(ra_single_threaded), not(doc)))]
104compile_error!(
105 "rusty_alloc's no_std build assumes a SINGLE THREAD (SingleThreadCell's \
106 `unsafe impl Sync`, prim::fixed's constant thread id and spin lock, and \
107 options' split 64-bit atomics all depend on it). Confirm that is true of \
108 your target and opt in with `--cfg ra_single_threaded`, or enable the \
109 `std` feature. See the crate docs on SingleThreadCell."
110);
111
112/// The single-thread half of [`ra_thread_local!`]: a `static` with a `.with()`.
113#[cfg(any(
114 not(feature = "std"),
115 all(
116 target_arch = "wasm32",
117 target_os = "unknown",
118 not(target_feature = "atomics")
119 )
120))]
121pub(crate) struct SingleThreadCell<T>(T);
122
123#[cfg(any(
124 not(feature = "std"),
125 all(
126 target_arch = "wasm32",
127 target_os = "unknown",
128 not(target_feature = "atomics")
129 )
130))]
131// SAFETY: only ever constructed by `ra_thread_local!`, and only on a target
132// this crate serves single-threaded: a `no_std` build (which must opt in with
133// `--cfg ra_single_threaded`), or `wasm32-unknown-unknown` without the atomics
134// proposal, where `prim/wasm.rs` has assumed one thread since it was written.
135// The same standing assumption as `prim::fixed` (constant thread id, TLS
136// destructors that never fire, a spin lock that never contends). With one
137// thread there is no other referent, so shared access cannot race. A build on a
138// target that grows threads must revisit this type FIRST — which is what the
139// `target_feature = "atomics"` half of the condition above is there to catch.
140unsafe impl<T> Sync for SingleThreadCell<T> {}
141
142#[cfg(any(
143 not(feature = "std"),
144 all(
145 target_arch = "wasm32",
146 target_os = "unknown",
147 not(target_feature = "atomics")
148 )
149))]
150impl<T> SingleThreadCell<T> {
151 pub(crate) const fn new(v: T) -> Self {
152 Self(v)
153 }
154 /// Mirrors `LocalKey::with`, which is the only accessor the crate uses.
155 pub(crate) fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
156 f(&self.0)
157 }
158}
159
160/// `true` exactly when this build has ONE thread for the life of the program,
161/// so that `prim::thread_id()` is a compile-time constant.
162///
163/// That is two targets: the bare-metal `prim::fixed` backend, which the crate
164/// refuses to build without `--cfg ra_single_threaded`, and
165/// `wasm32-unknown-unknown` without the atomics proposal, where
166/// `prim/wasm.rs` has returned one id since it was written. It is **not**
167/// `ra_single_threaded` alone: on a hosted target that cfg only unlocks the
168/// fixed backend's unit tests, the OS prim still hands out real thread ids,
169/// and the suite spawns threads.
170///
171/// What it buys: the cross-thread machinery — abandoning a segment when a
172/// thread ends, adopting one back, the delayed-free list a remote free lands
173/// on — is code that CANNOT execute here, and `ra_single_threaded` used to
174/// prune none of it. The linker kept `adopt_segment` (1,154 B) and
175/// `drain_delayed` (995 B) in an ESP32-S3 firmware that had asserted a single
176/// context. Each consumer of this constant folds one branch so that code
177/// becomes provably unreachable and the linker drops it; where a hosted build
178/// would have compared thread ids, it still does
179/// (`docs/plans/finished/firmware-code-size.md`, lever 1).
180///
181/// A `const`, not a `cfg`, so every site reads as `if ONE_THREAD` and a host
182/// build compiles both arms — the pruned code is type-checked and unit-tested
183/// everywhere, and only linked where it can run.
184pub(crate) const ONE_THREAD: bool = cfg!(any(
185 all(
186 ra_single_threaded,
187 not(miri),
188 not(windows),
189 not(unix),
190 not(target_arch = "wasm32")
191 ),
192 all(
193 target_arch = "wasm32",
194 target_os = "unknown",
195 not(target_feature = "atomics")
196 )
197));
198
199/// `true` where `prim::protect` can actually protect a page: the OS backends
200/// and the miri mock.
201///
202/// Guarded objects are a huge segment whose trailing page is `PROT_NONE`, so
203/// an overflow faults on the first byte past the object. `prim::fixed` and
204/// `prim::wasm` have no MMU and return `Err` from `protect`; there the
205/// sampler used to run anyway and hand out a dedicated segment with an
206/// UNPROTECTED trailing page — the whole cost of a guarded object and none of
207/// the protection — while `try_guarded` (1,641 B) and the ChaCha block it
208/// samples with (725 B) stayed in an ESP32-S3 image with `secure` off. The
209/// runtime gate (`guarded_rate`) could not remove them: it is a field, and a
210/// linker cannot prove a field is zero. Every consumer folds on this constant
211/// instead (`docs/plans/finished/firmware-code-size.md`, lever 3).
212pub(crate) const GUARD_PAGES: bool = cfg!(any(unix, windows, miri));
213
214/// Whether anything draws from a heap's CSPRNG: `secure` free-list keys, or
215/// guarded sampling. A build with neither never seeds it.
216pub(crate) const RNG_USED: bool = GUARD_PAGES || cfg!(feature = "secure");
217
218pub mod alloc;
219pub mod arena;
220pub mod bins;
221pub mod heap;
222pub mod init;
223pub mod options;
224pub mod os;
225pub mod page;
226pub mod prim;
227/// Kani proof harnesses (H-30). `cfg(kani)`-only: absent from every shipped
228/// build, so it costs the crate nothing.
229#[cfg(kani)]
230mod proofs;
231pub mod random;
232pub mod segment;
233pub mod segment_map;
234// Wired into the segment paths only on wasm (F2, docs/plans/segment-tax.md);
235// native builds compile it for its unit tests, so its items are "unused"
236// there by design.
237#[cfg_attr(not(all(target_arch = "wasm32", not(miri))), allow(dead_code))]
238pub(crate) mod slice_pool;
239pub mod stats;
240pub mod types;
241
242pub use bins::good_size;
243
244/// Rebuild a pointer at `addr` keeping `p`'s provenance. Used wherever an
245/// address round-trips through an integer (atomic words, encoded links) — the
246/// thrice-learned law: provenance and reachability follow POINTERS.
247#[inline]
248pub fn ptr_with_addr<T>(p: *mut T, addr: usize) -> *mut T {
249 p.with_addr(addr)
250}
251
252/// Our own semantic version, from the crate manifest.
253pub const VERSION: &str = env!("CARGO_PKG_VERSION");
254
255/// The mimalloc version we are API- and ABI-compatible with, in mimalloc's
256/// encoding (major·10⁴ + minor·10² + patch): v2.4.5. `mi_version()` reports this.
257pub const MI_COMPAT_VERSION: i32 = 20405;
258
259/// mimalloc-encoded compat version, as reported by the C ABI `mi_version()`.
260#[inline]
261pub const fn version() -> i32 {
262 MI_COMPAT_VERSION
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn version_is_v2_4_5_compat() {
271 assert_eq!(version(), 20405);
272 }
273}