pub struct OncePtrCell<T> { /* private fields */ }Expand description
A lazy, CAS-published pointer cell: UNINIT -> INITIALIZING -> READY over a
single AtomicPtr<T>, with fallible init (OOM rolls back and losers
re-race). See the crate-level docs for the full state machine, the
anti-livelock loser-spin rule, and the “usable inside a
#[global_allocator]” niche.
The cell never drops, frees, or reads through the pointee — it only
publishes and hands back the *mut T the init closure produced.
#[repr(transparent)]: the “one AtomicPtr”/“one word” claims made
throughout this crate’s docs are a LAYOUT GUARANTEE, not an
implementation detail that happens to be true on the current compiler.
PhantomData<*mut T> is the only other field; it is always zero-sized
with alignment 1, which is exactly what repr(transparent) requires of
every field beyond the one real one.
Implementations§
Source§impl<T> OncePtrCell<T>
impl<T> OncePtrCell<T>
Sourcepub const fn new() -> Self
pub const fn new() -> Self
Construct a fresh UNINIT cell (null pointer).
Not const under --cfg loom (loom’s atomics have no const
constructor); on normal builds it is const so the cell can live in a
static. Because --cfg loom is a global RUSTFLAGS cfg, this
applies to every crate in a build that sets it, not only crates that
mean to run loom against OncePtrCell itself — a
static CELL: OncePtrCell<T> = OncePtrCell::new(); anywhere in such a
build fails to compile. Scope the flag to this crate
(cargo test -p once-ptr-cell ...), or supply your own
#[cfg(loom)] const-capable stand-in if you need the flag
workspace-wide.
§Panics
Panics if align_of::<T>() == 1. The INITIALIZING sentinel is encoded
as the address 1 (see the crate-level “Sentinel encoding” docs); that
encoding needs a spare low bit, which requires every valid aligned
address of T to be even — i.e. align_of::<T>() >= 2. In the
documented static CELL: OncePtrCell<T> = OncePtrCell::new(); usage
this assert! is evaluated at compile time (a const-eval failure, not
a runtime panic); called from a non-const context (e.g. inside a
function, or via OncePtrCell::<T>::default()) with a T whose
alignment is 1, it panics at runtime instead.
Sourcepub fn get(&self) -> Option<NonNull<T>>
pub fn get(&self) -> Option<NonNull<T>>
Return the published pointer if the cell is READY, else None.
A pure Acquire load: no CAS, no init, no spin. None means the cell is
UNINIT or INITIALIZING right now (neither the sentinel nor null is
ever returned as Some).
The returned pointer is the exact value the init closure produced; the
Acquire load pairs with the winner’s Release publish, so any read the
caller performs through the pointer sees the fully initialised pointee.
Sourcepub fn get_or_try_init<F>(&self, init: F) -> Option<NonNull<T>>
pub fn get_or_try_init<F>(&self, init: F) -> Option<NonNull<T>>
Get the published pointer, or run init to produce it — with the full
UNINIT -> INITIALIZING -> READY protocol, OOM rollback, and loser
re-race.
Contract:
- Fast path: if the cell is already
READY, returns the published pointer with oneAcquireload;initis not called. - Winner: the thread that CASes
null -> sentinelcallsinitexactly once.initreturnsSome(ptr)on success (the cell publishes it withReleaseand returns it —ptris leaked for the process lifetime, the cell never frees it), orNoneon OOM (the cell rolls the sentinel back tonulland returnsNone; a later call may retry). - Loser: a thread that loses the CAS spins with
Acquireloads only while the state isINITIALIZING. When the winner publishes, the loser returns the same pointer. When the winner rolls back after OOM (state returns tonull), the loser falls out of the spin and re-races the CAS itself — it does not wait for aREADYthat will never come.
Returns Some(published pointer) (same value for all threads across a
successful lifetime) or None if init reported OOM on this thread’s
winning attempt. The returned pointer is never null and never the
sentinel.
init is FnOnce, not FnMut, because ONE call to this method
invokes it at most once: whichever way the winner arm exits
(publish, OOM rollback, or unwind) it leaves the method, and the
loser arm never calls init at all — a loser that falls out of the
spin on a rollback re-races the CAS and, if it wins, is making its
own first and only call. FnOnce is therefore the accurate bound,
and it lets you pass a closure that consumes what it captures.
init must be reentrancy-safe with respect to whatever the cell guards:
it runs while this thread holds the INITIALIZING sentinel, so it must
not itself call back into get_or_try_init on the SAME cell (that would
spin forever — the current thread is the only one able to publish).
The restriction is transitive, and multiple cells form a lock-order
graph: init must not wait, through any chain of calls, on a cell
whose own initialiser can wait on this one. Two cells are enough for a
deadlock with no direct self-recursion anywhere — thread 1 wins A and
its init initialises B, while thread 2 wins B and its init
initialises A; both spin forever at 100% CPU. Acquire multiple cells
in a fixed global order, exactly as you would locks.
init must also be fast and non-blocking: every loser thread spins for
exactly as long as the winner’s init call takes (see the module docs’
“spin-wait” section) — there is no bounded-latency guarantee from the
cell itself, only from the caller keeping init short.
Calling this from inside a #[global_allocator] adds further hard
obligations on init (no allocation, no unwind) — see the crate docs’
“Using this inside a #[global_allocator]”
section.
§Panics
Panics if the winning init call returns Some(ptr) where ptr’s
address is the reserved INITIALIZING sentinel (1) — a safe init
closure can construct and return this exact address, and publishing it
unguarded would make every reader (this thread’s own fast path
included) misclassify the cell as still-initializing forever. This
check is release-active, not debug_assert!-gated.
If init itself panics (unwinds) instead of returning, the panic
propagates out of get_or_try_init and the cell is left in UNINIT
(not wedged in INITIALIZING) — a later call, on any thread, may
retry init. This mirrors the OOM/None rollback above; the only
difference is how the winner exits. Note what this does and does not
buy: it keeps the CELL consistent, but it does not make the unwind
itself sound when the frame below is a GlobalAlloc method, where
unwinding is undefined behaviour regardless of this cell’s state.
Sourcepub fn dbg_is_ready(&self) -> bool
pub fn dbg_is_ready(&self) -> bool
Test-probe introspection: true iff the cell is currently READY
(holds a real, non-null, non-sentinel pointer). Says nothing about
the published value itself (that is OncePtrCell::get’s
contract).
This is functionally identical to get().is_some() — same single
Acquire load, same predicate, no capability get lacks — it does
not avoid racing a concurrent init any differently than get
does (an earlier version of this doc claimed
otherwise). It exists as a named, self-documenting boolean
introspection primitive: a caller writing assert!(cell.dbg_is_ready())
reads as “assert the cell materialised” without an
.is_some()/.is_none() match at the call site. The sefer-alloc
allocator this crate was extracted from relies on exactly that: its
own Registry::dbg_chunk_is_materialised forwards here to assert
chunk-materialisation state in its regression tests.
§Stability
This is a deliberate, STABLE part of the public API — a
dbg_-prefixed test-probe surface, not a hidden implementation
detail. It carries the crate’s normal semver guarantee like any
other public item; a #[doc(hidden)] posture was rejected precisely
because it would advertise this function to downstream consumers’
tests (see OncePtrCell::dbg_rollback_reenterable’s own doc) while
hiding it from the rustdoc those consumers would need to discover it
— see the crate README’s “Test-probe API stability” section for the
full rationale.
Sourcepub fn dbg_rollback_reenterable(&self) -> RollbackProbe
pub fn dbg_rollback_reenterable(&self) -> RollbackProbe
Test-only anti-livelock rollback probe. Drives THIS cell through the
exact null -> sentinel -> rollback -> re-CAS sequence the internal
OOM-bailout runs, and proves the postcondition the whole design rests on:
after a rollback, a fresh CAS(null -> sentinel) MUST succeed (the
sentinel was genuinely cleared, so no future winner or spinning loser is
wedged).
Returns RollbackProbe::Proven if the rollback provably cleared the
sentinel (the postcondition CAS re-won the cell; it is restored to
UNINIT before returning). RollbackProbe::NotApplicable covers
TWO distinct “could not test” cases, deliberately conflated because
neither is evidence rollback is broken: (a) the cell was not observed
UNINIT on the entry CAS (already READY, or another thread owned it
at that instant), or (b) the postcondition CAS in step 3 failed
because a real get_or_try_init caller raced in and re-won the cell
during the probe’s own rollback-then-reCAS window — in that case the
probe leaves the cell alone (does NOT touch the new owner’s state).
There is deliberately no “rollback is broken” variant. This probe cannot distinguish that from “someone else legitimately owns the cell now” by construction — both look identical from here, the postcondition CAS simply fails either way — so the return type encodes exactly the two answers it can actually give, and no third one it could never produce.
Exists so a consumer’s test can drive the rollback on a REAL, LIVE cell
(e.g. a process-global registry chunk) — proving the shipped code path,
not a copy — without a process-terminating OOM. The whole probe is a
bounded, single-threaded sequence of atomic ops; callers MUST pick a
cell no other thread is concurrently initialising. The entry CAS is
only a POINT-IN-TIME check, not mutual exclusion across the whole
probe: if the cell is not observed UNINIT at that instant, the probe
returns RollbackProbe::NotApplicable and touches nothing, but a
concurrent
OncePtrCell::get_or_try_init racing in AFTER the entry CAS (during
the probe’s own rollback-then-reCAS window) is not excluded by it — the
probe’s final restore step accounts for that by only touching the cell
when its own postcondition CAS actually re-won ownership (see the
step-by-step comments in the body).
§Stability
This is a deliberate, STABLE part of the public API, not
#[doc(hidden)]. This function is explicitly written to be called
FROM a downstream consumer’s own test suite (“a consumer’s test can
drive the rollback on a REAL, LIVE cell” above) — a #[doc(hidden)]
posture would have advertised it to those consumers while hiding it
from the rustdoc they would need to find it in the first place, an
unresolvable contradiction the crate’s rust-intel audit caught. See
the crate README’s “Test-probe API stability” section for the full
rationale and the rejected feature-flag alternative.
Trait Implementations§
Source§impl<T> Debug for OncePtrCell<T>
impl<T> Debug for OncePtrCell<T>
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
Diagnostic-only classification of the cell’s current state — never
dereferences the pointee, so no T: Debug bound is needed (T never
appears in the output). Relaxed is enough here: unlike get, this
never hands the pointer back to the caller to dereference, so there is
no happens-before edge to establish. Like any concurrent type’s
Debug impl (OnceLock’s included), the state printed can be stale
the instant after this call returns.