Skip to main content

static_generics/
once.rs

1//! Lazy, one-time initialization on top of
2//! [`generic_static`](crate::namespace::Namespace::generic_static).
3//!
4//! [`generic_static`](crate::namespace::Namespace::generic_static). only supports
5//! [`bytemuck::Zeroable`]
6//! types, so the storage is always zeroed. That allows
7//! `Atomic*`, [`UnsafeCell`],
8//! [`Cell`](core::cell::Cell), and other similar types to be stored — but not types that need a
9//! non-zero initial value.
10//!
11//! [`OnceSlot`] allows to store non Zeroable types as a generic static:
12//! It is itself zeroable, but it can be lazily initialized provided by initializer closure
13//! into [`OnceSlot::get_or_init`](OnceSlot::get)
14//!
15//! ```rust
16//! use static_generics::{define_namespace, namespace::NamespaceExt};
17//!
18//! define_namespace!(MyNs);
19//!
20//! struct Config {
21//!     retries: u32,
22//! }
23//!
24//! let cfg = MyNs::once::<Config>(|| Config { retries: 3 });
25//! assert_eq!(cfg.retries, 3);
26//! // Second call ignores the closure and returns the same address.
27//! let again = MyNs::once::<Config>(|| Config { retries: 99 });
28//! assert!(core::ptr::eq(cfg, again));
29//! ```
30
31use core::cell::UnsafeCell;
32use core::mem::MaybeUninit;
33use core::sync::atomic::{AtomicU8, Ordering};
34
35#[cfg(feature = "std")]
36use parking_lot::{Condvar, Mutex};
37
38const UNINIT: u8 = 0;
39const BUSY: u8 = 1;
40const INIT: u8 = 2;
41const POISONED: u8 = 3;
42
43struct PoisonGuard<'a, T> {
44    slot: &'a OnceSlot<T>,
45    done: bool,
46}
47
48impl<T> Drop for PoisonGuard<'_, T> {
49    fn drop(&mut self) {
50        if !self.done {
51            self.slot.finish(POISONED);
52        }
53    }
54}
55
56/// Zero-initialized slot for one-time initialization of a `T`.
57///
58/// The slot itself starts zeroed (state `UNINIT`), which is what makes it
59/// usable with `generic_static`.
60/// The `T` value is written exactly once by the thread that wins the
61/// claim race; other threads block until `INIT` (or `POISONED`) becomes
62/// visible.
63///
64/// With the `std` feature, waiters block on a [`Mutex`]/[`Condvar`] pair
65/// (no spinning); without it they spin with [`core::hint::spin_loop`].
66///
67/// # Panics
68///
69/// [`get_or_init`](OnceSlot::get_or_init) must not be called reentrantly
70/// with the same slot (the initializer calling back into the same slot).
71/// That deadlocks: the thread already holds the `BUSY` claim, so the inner
72/// call blocks forever waiting for itself.
73///
74/// If the initializer panics, the slot is poisoned.
75pub struct OnceSlot<T> {
76    state: AtomicU8,
77    #[cfg(feature = "std")]
78    lock: Mutex<()>,
79    #[cfg(feature = "std")]
80    cvar: Condvar,
81    value: UnsafeCell<MaybeUninit<T>>,
82}
83// SAFETY: `state` is 0 for UNINIT, value is zeroed via `MaybeUninit`, and lock/cvar in parking_lot are zeroable.
84unsafe impl<T> bytemuck::Zeroable for OnceSlot<T> {}
85
86unsafe impl<T: Send + Sync> Sync for OnceSlot<T> {}
87unsafe impl<T: Send> Send for OnceSlot<T> {}
88
89impl<T> OnceSlot<T> {
90    /// Create a new uninitialized slot.
91    #[must_use]
92    pub const fn new() -> Self {
93        Self {
94            state: AtomicU8::new(UNINIT),
95            #[cfg(feature = "std")]
96            lock: Mutex::new(()),
97            #[cfg(feature = "std")]
98            cvar: Condvar::new(),
99            value: UnsafeCell::new(MaybeUninit::uninit()),
100        }
101    }
102
103    fn finish(&self, next: u8) {
104        debug_assert!(next == INIT || next == POISONED);
105        #[cfg(feature = "std")]
106        {
107            let _guard = self.lock.lock();
108            self.state.store(next, Ordering::Release);
109            self.cvar.notify_all();
110        }
111        #[cfg(not(feature = "std"))]
112        {
113            self.state.store(next, Ordering::Release);
114        }
115    }
116
117    /// Returns the value if already initialized, otherwise `None`.
118    #[must_use]
119    pub fn get(&'static self) -> Option<&'static T> {
120        if self.state.load(Ordering::Acquire) == INIT {
121            // SAFETY: `INIT` is only stored after the payload was fully
122            // written, with `Release` ordering. `Acquire` here synchronizes
123            // with that store, so the payload is initialized.
124            Some(unsafe { (*self.value.get()).assume_init_ref() })
125        } else {
126            None
127        }
128    }
129
130    /// Returns `true` if initialization completed successfully.
131    #[must_use]
132    pub fn is_completed(&'static self) -> bool {
133        self.state.load(Ordering::Acquire) == INIT
134    }
135
136    /// Returns `true` if the initializer panicked and poisoned the slot.
137    #[must_use]
138    pub fn is_poisoned(&'static self) -> bool {
139        self.state.load(Ordering::Acquire) == POISONED
140    }
141
142    /// Block until the winner finishes, then return or panic on poison.
143    fn wait(&'static self) -> &'static T {
144        #[cfg(feature = "std")]
145        {
146            let mut guard = self.lock.lock();
147            loop {
148                match self.state.load(Ordering::Acquire) {
149                    INIT => {
150                        // SAFETY: same publication argument as in `get`.
151                        return unsafe { (*self.value.get()).assume_init_ref() };
152                    }
153                    POISONED => panic!("OnceSlot has previously been poisoned"),
154                    _ => self.cvar.wait(&mut guard),
155                }
156            }
157        }
158        #[cfg(not(feature = "std"))]
159        {
160            loop {
161                match self.state.load(Ordering::Acquire) {
162                    INIT => {
163                        // SAFETY: same publication argument as in `get`.
164                        return unsafe { (*self.value.get()).assume_init_ref() };
165                    }
166                    POISONED => panic!("OnceSlot has previously been poisoned"),
167                    _ => core::hint::spin_loop(),
168                }
169            }
170        }
171    }
172
173    /// Returns the value, running `init` exactly once to produce it.
174    ///
175    /// Concurrent callers block (on a condvar with `std`, by spinning
176    /// without it) until the winner finishes. If `init` panics, the slot
177    /// is poisoned and every caller — past, present, and future — panics.
178    ///
179    /// # Panics
180    ///
181    /// Panics if the slot is poisoned (including when a concurrent
182    /// initializer panics while this call is blocked). Must not be called
183    /// reentrantly with the same slot.
184    pub fn get_or_init(&'static self, init: impl FnOnce() -> T) -> &'static T {
185        if let Some(value) = self.get() {
186            return value;
187        }
188
189        self.initialize(init)
190    }
191
192    #[cold]
193    #[inline(never)]
194    fn initialize(&'static self, init: impl FnOnce() -> T) -> &'static T {
195        match self
196            .state
197            .compare_exchange(UNINIT, BUSY, Ordering::AcqRel, Ordering::Acquire)
198        {
199            Ok(_) => {
200                let mut guard = PoisonGuard {
201                    slot: self,
202                    done: false,
203                };
204                let value = init();
205                // SAFETY: we hold the unique `BUSY` claim, so no other
206                // thread reads or writes the payload concurrently.
207                unsafe {
208                    (*self.value.get()).write(value);
209                }
210                self.finish(INIT);
211                guard.done = true;
212                // SAFETY: just stored `INIT` after writing the payload.
213                unsafe { (*self.value.get()).assume_init_ref() }
214            }
215            Err(INIT) => {
216                // Winner finished between our fast-path `get()` and the
217                // `CAS`; the payload is initialized.
218                // SAFETY: `INIT` observed, same as in `get`.
219                unsafe { (*self.value.get()).assume_init_ref() }
220            }
221            Err(POISONED) => panic!("OnceSlot has previously been poisoned"),
222            Err(_) => self.wait(),
223        }
224    }
225}
226
227impl<T> Default for OnceSlot<T> {
228    fn default() -> Self {
229        Self::new()
230    }
231}