Skip to main content

OncePtrCell

Struct OncePtrCell 

Source
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>

Source

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.

Source

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.

Source

pub fn get_or_try_init<F>(&self, init: F) -> Option<NonNull<T>>
where F: FnOnce() -> 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 one Acquire load; init is not called.
  • Winner: the thread that CASes null -> sentinel calls init exactly once. init returns Some(ptr) on success (the cell publishes it with Release and returns it — ptr is leaked for the process lifetime, the cell never frees it), or None on OOM (the cell rolls the sentinel back to null and returns None; a later call may retry).
  • Loser: a thread that loses the CAS spins with Acquire loads only while the state is INITIALIZING. When the winner publishes, the loser returns the same pointer. When the winner rolls back after OOM (state returns to null), the loser falls out of the spin and re-races the CAS itself — it does not wait for a READY that 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.

Source

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.

Source

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>

Source§

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.

Source§

impl<T> Default for OncePtrCell<T>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<T> Send for OncePtrCell<T>

Source§

impl<T> Sync for OncePtrCell<T>

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.