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}