Skip to main content

once_ptr_cell/
lib.rs

1//! `once-ptr-cell` — a lazy, CAS-published pointer cell with fallible init,
2//! OOM rollback, and loser re-race.
3//!
4//! [`OncePtrCell<T>`] is a three-state machine over a **single** `AtomicPtr<T>`:
5//!
6//! ```text
7//! UNINIT(null) --CAS--> INITIALIZING(sentinel=1) --Release store--> READY(real *mut T)
8//!                              |
9//!                              +-- init returns None (OOM) --> UNINIT(null)  [rollback]
10//! ```
11//!
12//! - The thread that CASes `null -> sentinel` becomes the **winner** and runs
13//!   the caller's init closure exactly once.
14//! - On success the winner publishes the real pointer with **`Release`** and
15//!   leaks it for the process lifetime (the cell never drops or frees `T`).
16//! - **Losers spin-`Acquire` only while the state is `INITIALIZING`** — NOT
17//!   `while != READY`. Spinning on `!= READY` deadlocks against the OOM-rollback
18//!   path: if the winner hits OOM and rolls the sentinel back to `null` without
19//!   ever publishing `READY`, a `!= READY` spinner waits forever for a `READY`
20//!   that will never come. Spinning on `== INITIALIZING` instead means a loser
21//!   that observes the rollback (`null`) falls out of the spin and **re-races
22//!   the CAS itself**.
23//! - On winner **OOM** the sentinel is rolled back to `null` and losers
24//!   re-race the CAS themselves, rather than being blocked and woken.
25//!
26//! ## Why not `OnceLock`?
27//!
28//! This cell fills the niche `OnceLock` cannot: it is
29//!
30//! - **`no_std` and allocation-free** — the cell itself is one `AtomicPtr`; it
31//!   never touches the heap.
32//! - **usable inside a `#[global_allocator]`** — the cell's own non-panicking
33//!   operations use NO `std` sync primitive (no `Mutex`, no parking, no
34//!   `OnceLock`) and allocate nothing, so the cell itself cannot re-enter the
35//!   allocator being bootstrapped. That is a property of the CELL, not of a
36//!   whole `get_or_try_init` call: the caller's `init` closure runs inside
37//!   that call and carries hard obligations of its own — see
38//!   ["Using this inside a `#[global_allocator]`"](#using-this-inside-a-global_allocator)
39//!   below. Used by hand-rolled allocators, runtimes, and bare-metal
40//!   bootstraps that must publish a process-`'static` pointer before any heap
41//!   exists.
42//! - **fallible without blocking** — `OnceLock::get_or_init` cannot fail at
43//!   all, and its `get_or_try_init` is still unstable (`once_cell_try`). Both
44//!   may also BLOCK the losing threads for the duration of the winner's init
45//!   (the documented contract is that losers block; the mechanism `std` uses
46//!   to do so is an implementation detail, not a stable promise). This
47//!   cell reports failure as a plain `None` per caller and lets losers re-race
48//!   the CAS with no OS involvement, so a later attempt (after the OS frees
49//!   memory, say) can succeed without a blocking primitive anywhere.
50//!
51//!   To be precise about what `OnceLock` does and does not do, since this
52//!   crate's earlier docs got it wrong: `OnceLock::get_or_try_init` does NOT
53//!   poison the cell on `Err`, and a failed or panicking initialiser leaves it
54//!   uninitialised and retryable (`std` drives it through
55//!   `Once::call_once_force`, which deliberately ignores poisoning). The real
56//!   distinctions are the ones above — `no_std`, no parking, no internal
57//!   allocation, and a raw-pointer/lifetime posture — not recoverability.
58//!
59//! ## The spin-wait (no parking, no `std`)
60//!
61//! Losers busy-spin with [`core::hint::spin_loop`] — there is no OS park/unpark
62//! (that would need `std` sync and could re-enter the allocator). **There is
63//! no bounded-latency guarantee**: a loser waits for exactly as long as the
64//! winner's `init` closure takes — **provided a winner is currently running
65//! at all**. `init` is arbitrary caller code — a closure that blocks on a
66//! syscall, gets preempted, or simply runs long makes every loser wait that
67//! long too. The intended usage (typically one OS reservation + one publish
68//! store) keeps the spin short in practice, but that is a caller obligation,
69//! not something this cell enforces: **`init` must be fast and
70//! non-blocking**, on top of the re-entry restriction below. A cell whose
71//! `INITIALIZING` owner has stopped running (see "Fork and signal safety"
72//! below) is waited on forever, not merely for a long time. This is a
73//! deliberate design constraint of the "usable inside the global allocator"
74//! niche, not an oversight — see the module docs above.
75//!
76//! ## Using this inside a `#[global_allocator]`
77//!
78//! The cell is built for this niche, but the niche has hard rules that are the
79//! CALLER's to keep — the cell can enforce none of them:
80//!
81//! - **`init` must not allocate**, directly or transitively, and must not
82//!   otherwise re-enter the allocator being bootstrapped. `init` runs while
83//!   this thread holds the `INITIALIZING` sentinel; an allocation from inside
84//!   it re-enters an allocator whose own bootstrap is mid-flight.
85//! - **`init` must not block** — every loser thread spins for exactly as long
86//!   as `init` runs (see "The spin-wait" above).
87//! - **`init` must not wait on another cell that can wait back** — the
88//!   re-entry restriction is transitive, and several cells form a lock-order
89//!   graph. An allocator bootstrap is exactly the shape that produces this
90//!   (many per-chunk cells plus a sidecar path); see
91//!   [`OncePtrCell::get_or_try_init`]'s own docs for the two-cell deadlock.
92//! - **`init` must not panic, and no panic may unwind through a `GlobalAlloc`
93//!   method** — [unwinding out of a global allocator is undefined
94//!   behaviour][ga]. This crate's rollback guard keeps the CELL consistent
95//!   across an unwinding `init` (the sentinel is rolled back, not left wedged),
96//!   but it cannot make the unwind itself sound once the frame below is
97//!   `GlobalAlloc::alloc`.
98//! - **An `init` that returns the sentinel address is a caller bug, not a
99//!   recoverable error.** The release-active `assert!` documented under
100//!   [`OncePtrCell::get_or_try_init`]'s `# Panics` exists to make that bug
101//!   loud — it is a violated precondition, not a condition an allocator is
102//!   expected to survive.
103//!
104//! ### Panic sites and the two link environments
105//!
106//! The panic sites, independently:
107//!
108//! | # | Panic site | Whose code | Reaches the panic runtime? | Message shape | Allocations before a non-allocating hook (measured — see below) |
109//! |---|---|---|---|---|---|
110//! | 1 | sentinel-collision `assert!` in `get_or_try_init` | this crate | yes | bare `&'static str` | 0 |
111//! | 2 | an unwinding `init` closure | **yours** | yes | whatever you wrote | 0 if a bare literal, ≥ 2 if formatted |
112//! | 3 | `align_of::<T>() >= 2` in `new`, `static` form | this crate | **no** — const-eval failure, compile time | n/a | n/a |
113//! | 3 | `align_of::<T>() >= 2` in `new`/`default`, non-const form | this crate | yes | bare `&'static str` | 0 |
114//!
115//! **Normative contract, separate from the measurement below: `init` must
116//! not panic, full stop.** The `std` panic path *may* allocate before any
117//! hook runs — especially for a formatted message — so the absence of an
118//! allocation is never something to rely on. The numbers in this table and
119//! the paragraph below are a measurement (rustc 1.97,
120//! `x86_64-pc-windows-msvc`, `--release`, `RUST_BACKTRACE=0`, one specific
121//! non-allocating hook), not an API guarantee about the panic runtime, this
122//! crate's MSRV, other `std` implementations/targets, or future toolchains.
123//!
124//! The two link environments need genuinely different mitigations, **not a
125//! shared recipe**:
126//!
127//! - A `no_std` binary supplies its own `#[panic_handler]`. Written not to
128//!   allocate, it closes the hazard completely: the whole panic path is
129//!   yours, so nothing on it can re-enter the allocator.
130//! - A `std` binary's `panic = "abort"` profile setting removes the
131//!   **unwind** (the UB when the frame below is `GlobalAlloc::alloc`), but it
132//!   does **not** stop the panic runtime from allocating: with the DEFAULT
133//!   hook, every panic sampled here allocated before it could print anything
134//!   (measured: 2 allocations under `panic = "abort"`, `RUST_BACKTRACE=0`,
135//!   `--release`, rustc 1.97, x86_64-pc-windows-msvc). Inside a
136//!   `#[global_allocator]` that allocation re-enters the very cell that is
137//!   mid-`init`, and the thread deadlocks on its own sentinel instead of
138//!   aborting — the diagnostic the release-active `assert!` above exists to
139//!   print never reaches stderr, because the allocation that would have
140//!   printed it is the one that deadlocked. A `std` consumer therefore needs
141//!   `panic = "abort"` **and** a `std::panic::set_hook` that goes straight to
142//!   `std::process::abort` without formatting — or, better, an `init` that
143//!   cannot panic at all. **Residual limit: a hook cannot help if the panic
144//!   message is formatted.** `std` materialises the message (`payload.get()`)
145//!   as an *argument* to the hook call, so `unwrap`/`expect`/`assert_eq!`/
146//!   `panic!("{}", …)` allocate before any hook runs, whether or not the hook
147//!   itself allocates — measured: 2 allocations for `Result::unwrap`, 4 for
148//!   `assert_eq!`, with the same non-allocating hook that reaches 0 for a
149//!   bare-`&'static str` panic. Only a panic whose message is a bare
150//!   `&'static str` was measured allocation-free under that hook. The
151//!   crate's own two `assert!`s (the sentinel-collision check and the
152//!   `align_of::<T>() >= 2` check) are of that shape and measure 0
153//!   allocations before the hook; **an unwinding `init` is your code, and
154//!   its message is whatever you wrote**, so it is covered only if you
155//!   keep it a bare literal — and even then only as a measured observation
156//!   on one toolchain, not a promise. **The only mitigation the contract
157//!   actually rests on is an `init` that cannot panic at all.** Note also
158//!   that `panic = "abort"` compiles the crate's internal rollback guard out
159//!   entirely (it is unwind-only) — under this profile the cell-consistency
160//!   guarantee above comes from the process dying, not from the guard.
161//!
162//! ### Fork and signal safety
163//!
164//! Everything above is about `init` and the panic path it can reach; two
165//! further hazards break the cell from **outside** any of that, with no
166//! misbehaving closure and no panic anywhere. **The cell is neither
167//! fork-safe nor async-signal-safe.**
168//! `INITIALIZING` is owned by a specific thread:
169//!
170//! - **`fork()` in a multithreaded process.** If one thread holds the
171//!   sentinel (running `init`) when another thread calls `fork()`, the child
172//!   process inherits a cell that reads `INITIALIZING` but has no thread that
173//!   can ever publish or roll it back — every subsequent caller in the child
174//!   spins forever. There is no reset API: `dbg_rollback_reenterable`'s entry
175//!   CAS requires the cell to already be `null` and is a no-op on a
176//!   sentinel-holding cell, by design.
177//! - **An allocating signal handler.** If a signal is delivered to the thread
178//!   that holds the sentinel and the handler allocates (directly, or
179//!   transitively — a `format!`, a `Vec`, a panic-hook path), the allocator
180//!   reaches the same cell from inside the handler, the claim CAS fails, and
181//!   the handler spins on a sentinel owned by the very thread it interrupted:
182//!   an unrecoverable single-thread self-deadlock.
183//!
184//! **The rule for a multithreaded POSIX process is the POSIX rule, and this
185//! crate adds nothing to it: after `fork()`, the child may call only
186//! async-signal-safe functions until a successful `exec()`; if `exec()`
187//! fails, terminate through an async-signal-safe path such as `_exit`.**
188//! That means no Rust allocator, no `get_or_try_init`, no `init` closure, no
189//! panic path, and no other ordinary Rust code in the child before `exec()`
190//! — the child inherits the whole address space, including every lock and
191//! resource state left behind by threads that do not exist in it, and POSIX
192//! specifies that a function is not async-signal-safe unless it is
193//! explicitly documented to be ([POSIX `fork()`], [async-signal-safety]).
194//!
195//! There is a narrower, cell-local invariant worth stating separately,
196//! because it is the part this crate can speak to at all: **`fork()` must
197//! not race any thread's `init`, anywhere in the process** — not just once,
198//! before some notional "first" fork; every subsequent `fork()`, and every
199//! cell created or reset afterward, is bound by it. A process-wide barrier
200//! establishes it: every initializer holds the barrier's shared side for the
201//! whole duration of its `init`, and the forking thread takes it
202//! exclusively — which by construction both waits for quiescence and blocks
203//! new inits — calls `fork()` **while still holding it**, and releases it
204//! only after `fork()` returns. (Acquiring, observing quiescence, releasing,
205//! and only then forking leaves a window in which a fresh `init` starts
206//! before the fork; holding across the call is the load-bearing part.)
207//!
208//! **That barrier prevents exactly one thing: a child snapshotting a cell
209//! wedged at `INITIALIZING` with no thread alive to finish it. It does NOT
210//! make the allocator, this cell, or Rust runtime code callable in the child
211//! before `exec()`** — inherited allocator and runtime locks are untouched
212//! by it, and a `get_or_try_init` call in the child is a non-async-signal-safe
213//! call regardless of what any cell's state word says. Anything broader than
214//! the POSIX rule above is an environment-specific contract you own, and owes
215//! a fully proven `atfork` protocol covering every affected resource, not
216//! just these cells.
217//!
218//! Do not allocate in a signal handler.
219//!
220//! [ga]: https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html#safety
221//! [POSIX `fork()`]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/fork.html
222//! [async-signal-safety]: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html
223//!
224//! ## Sentinel encoding
225//!
226//! The `INITIALIZING` state is the address `1` (`SENTINEL_INITIALIZING`), a bare
227//! marker that is **never dereferenced, only compared** for pointer equality.
228//! Constructed via [`core::ptr::without_provenance_mut`] so it carries no
229//! provenance — strict-provenance-clean, since it is never turned back into a
230//! dereferenceable pointer. An *aligned* pointer to `T` can never have address
231//! `1` (`align_of::<T>() >= 2` is asserted at construction) — but a
232//! *misaligned or synthesised* pointer at address `1` IS reachable from safe
233//! code (an `init` closure can construct and return one). That case is
234//! rejected by a release-active `assert!` in
235//! [`OncePtrCell::get_or_try_init`] — see its `# Panics`.
236//!
237//! ## What the caller owns
238//!
239//! The cell stores and hands back a `*mut T` / `NonNull<T>`; it does **not**
240//! own the pointee. The init closure is responsible for producing a pointer
241//! valid for the lifetime the caller treats the cell's output as living (for the
242//! bootstrap use case: a leaked, process-`'static` allocation). Reading the
243//! payload behind the pointer is `unsafe` and left to the caller, who knows the
244//! pointee's real lifetime — see [`OncePtrCell::get`] and
245//! [`OncePtrCell::get_or_try_init`].
246//!
247//! ## Portability limit — requires pointer-width atomic CAS
248//!
249//! The whole cell is one `AtomicPtr<T>` driven by `compare_exchange`; that is
250//! not an incidental implementation choice, it is the entire mechanism. This
251//! crate therefore needs `target_has_atomic = "ptr"` and will **not compile**
252//! on a target without it. `thumbv6m-none-eabi` (Cortex-M0/M0+) and
253//! `riscv32imc-unknown-none-elf` (no `A` extension) have load/store atomics
254//! but no CAS; `msp430-none-elf` has no atomics at all. This crate is
255//! `no_std` and allocation-free, but neither property implies pointer-width
256//! CAS. A build on an unsupported target fails with an explicit
257//! [`compile_error!`] naming the requirement, and with **nothing else**:
258//! the implementation carries the positive `#[cfg(target_has_atomic =
259//! "ptr")]`, so its body is not compiled there at all. That replaces the
260//! "no method named `compare_exchange`" cascade an unguarded build would
261//! produce on `thumbv6m-none-eabi`/`riscv32imc-unknown-none-elf`, and the
262//! unresolved `AtomicPtr` import on `msp430-none-elf` (which has no atomics
263//! for `core` to define it from), with one sentence naming the real
264//! requirement.
265//!
266//! ## Layout — `#[repr(transparent)]`
267//!
268//! `OncePtrCell<T>` carries `#[repr(transparent)]`: its layout is guaranteed
269//! identical to `AtomicPtr<T>` — same size, same alignment. This is a real
270//! contract, not merely an observation about the current compiler: the
271//! "one `AtomicPtr`"/"one word" language throughout this crate's docs would
272//! otherwise describe an unstated detail of plain `repr(Rust)` layout
273//! (field order, padding, and single-field size equivalence are not
274//! guaranteed there), which is not something to leave implicit for a type
275//! meant to sit in allocator metadata or an array of cells.
276
277// This crate is a two-file seam crate: `lib.rs` is a documentation +
278// portability-guard facade with no code of its own, and ALL `unsafe` is
279// confined to `imp.rs`, lifted by the crate-level `#![allow(unsafe_code)]`
280// below. ONE
281// documented reason holds `unsafe` here — handing a raw `*mut T` /
282// `NonNull<T>` back to the caller — and it materialises at exactly two
283// audited kinds of site:
284//
285//   1. `unsafe impl Send/Sync` for the `AtomicPtr`-backed cell (justified
286//      below at the impls themselves);
287//   2. `unsafe { NonNull::new_unchecked(p) }` at the accessor sites where
288//      `p` was already proven non-null by an `is_ready`/`!= 0` check.
289//
290// Note what is NOT on that list: constructing the never-dereferenced
291// `INITIALIZING` sentinel via `core::ptr::without_provenance_mut` needs no
292// `unsafe` at all (it is a safe `const fn` on modern toolchains) — the
293// sentinel-comparison discipline is a correctness invariant, not an `unsafe`
294// one. All raw-pointer *dereferencing* is the CALLER's responsibility; this
295// crate never reads through `T`. The `#![allow(unsafe_code)]` is retained
296// (rather than `#![forbid]`) so the crate can expose the raw seam types and
297// those two site kinds. Every `unsafe fn` / `unsafe impl` carries a
298// `# Safety` / `// SAFETY:` justification.
299#![allow(unsafe_code)]
300#![deny(missing_docs)]
301#![no_std]
302
303// The whole cell is one AtomicPtr driven by compare_exchange (see the
304// crate-doc "Portability limit" section above) — that requires pointer-width
305// atomic CAS from the target. The implementation module below carries the
306// POSITIVE form of this same cfg, so on an unsupported target the body is
307// not compiled at all and this named diagnostic is the ONLY error the user
308// sees — no follow-on E0599 (thumbv6m-none-eabi/riscv32imc) or E0432
309// (msp430) cascade from code that could never have compiled there.
310#[cfg(not(target_has_atomic = "ptr"))]
311compile_error!(
312    "once-ptr-cell requires a target with pointer-width atomic \
313     compare-and-swap (target_has_atomic = \"ptr\"): the whole cell is one \
314     AtomicPtr driven by compare_exchange. thumbv6m-none-eabi \
315     (Cortex-M0/M0+) and riscv32imc-unknown-none-elf (no `A` extension) have \
316     load/store atomics but no CAS; msp430-none-elf has no atomics at all. \
317     None of these are supported, despite this crate being no_std and \
318     allocation-free."
319);
320
321/// The implementation. Split out of `lib.rs` so the whole body can carry the
322/// POSITIVE `target_has_atomic = "ptr"` cfg in one place — on an unsupported
323/// target this module is not compiled, leaving the `compile_error!` above as
324/// the single diagnostic. Not a public module: its two items are re-exported
325/// below and are the crate's entire public API.
326#[cfg(target_has_atomic = "ptr")]
327mod imp;
328
329#[cfg(target_has_atomic = "ptr")]
330pub use imp::{OncePtrCell, RollbackProbe};