vasi_sync/lazy_lock.rs
1use core::marker::PhantomData;
2use core::mem::MaybeUninit;
3
4use num_enum::{IntoPrimitive, TryFromPrimitive};
5
6use crate::sync::atomic;
7use crate::sync::{self, ConstPtr, UnsafeCell};
8
9/// For use with `LazyLock`.
10///
11/// This trait is implemented for `FnOnce` closures, so most users can just use
12/// that directly. However in case they need to name a type in the `LazyLock`,
13/// they can create a type that implements this trait instead.
14pub trait Producer<T> {
15 fn initialize(self) -> T;
16}
17
18impl<F, T> Producer<T> for F
19where
20 F: FnOnce() -> T,
21{
22 fn initialize(self) -> T {
23 (self)()
24 }
25}
26
27#[derive(TryFromPrimitive, IntoPrimitive, Eq, PartialEq, Debug)]
28#[repr(u32)]
29enum InitState {
30 Uninitd,
31 Initd,
32 // A thread is currently initializing, and there are no threads
33 // asleep on the futex.
34 Initializing,
35 // A thread is currently initializing, and there *are* threads asleep on the
36 // futex. We use this extra state to avoid doing a `futex_wake` when there
37 // are no threads asleep on the futex.
38 InitializingWithSleepers,
39}
40
41/// Analogous to `std::sync::LazyLock`, but works on stable Rust, is no_std,
42/// only makes direct syscalls, etc.
43//
44// TODO: implement or derive `VirtualAddressSpaceIndependent`. The Derive
45// macro currently doesn't work, because the default for `Init` is *not*
46// `VirtualAddressSpaceIndependent`. We could implement the trait manually,
47// but this is error-prone, and we don't currently need it.
48// // #[cfg_attr(not(loom), derive(VirtualAddressSpaceIndependent))]
49//
50// If we *do* make this `VirtualAddressSpaceIndependent`, consider also making
51// it `repr(C)`. Left as `repr(rust)` for now to let the compiler choose the
52// most efficient field ordering based on the alignments of `T` and `Init`.
53pub struct LazyLock<T, Init = fn() -> T> {
54 value: UnsafeCell<MaybeUninit<T>>,
55 initializer: UnsafeCell<Option<Init>>,
56 init_state: atomic::AtomicU32,
57}
58// `T` must be `Sync`, since it will be shared across threads once initialized.
59// `Init` only needs to be `Send` since it'll only ever be accessed by the
60// single thread that performs the initialization.
61unsafe impl<T, Init> Sync for LazyLock<T, Init>
62where
63 T: Sync,
64 Init: Send,
65{
66}
67unsafe impl<T, Init> Send for LazyLock<T, Init>
68where
69 T: Send,
70 Init: Send,
71{
72}
73
74impl<T, Init> LazyLock<T, Init>
75where
76 Init: Producer<T>,
77{
78 // TODO: merge with `new` when loom's `UnsafeCell` supports const init.
79 #[cfg(not(loom))]
80 pub const fn const_new(initializer: Init) -> Self {
81 Self {
82 value: UnsafeCell::new(MaybeUninit::uninit()),
83 init_state: atomic::AtomicU32::new(InitState::Uninitd as u32),
84 initializer: UnsafeCell::new(Some(initializer)),
85 }
86 }
87
88 pub fn new(initializer: Init) -> Self {
89 Self {
90 value: UnsafeCell::new(MaybeUninit::uninit()),
91 init_state: atomic::AtomicU32::new(InitState::Uninitd.into()),
92 initializer: UnsafeCell::new(Some(initializer)),
93 }
94 }
95
96 fn init(&self, mut state: InitState) {
97 loop {
98 match state {
99 InitState::Uninitd => {
100 match self.init_state.compare_exchange(
101 InitState::Uninitd.into(),
102 InitState::Initializing.into(),
103 atomic::Ordering::Relaxed,
104 // (Potentially) pairs with `Release` in `Initializing` case.
105 atomic::Ordering::Acquire,
106 ) {
107 Ok(_) => {
108 // We have the initialization lock.
109 self.value.get_mut().with(|p: *mut MaybeUninit<T>| unsafe {
110 let initializer =
111 self.initializer.get_mut().deref().take().unwrap();
112 (*p).write(initializer.initialize());
113 });
114 // `Release` pairs with `Acquire` in `Self::force`
115 // and/or this method's `Initializing` case.
116 let prev = InitState::try_from(
117 self.init_state
118 .swap(InitState::Initd.into(), atomic::Ordering::Release),
119 )
120 .unwrap();
121 match prev {
122 InitState::Initializing => {
123 // No sleepers; no need to do a futex wake
124 }
125 InitState::InitializingWithSleepers => {
126 // There are blocked threads; wake them.
127 sync::futex_wake_all(&self.init_state).unwrap();
128 }
129 other => panic!("Unexpected state {other:?}"),
130 };
131 return;
132 }
133 Err(v) => {
134 // It changed out from under us; update and try again.
135 state = v.try_into().unwrap();
136 }
137 }
138 }
139 InitState::Initializing => {
140 state = match self.init_state.compare_exchange(
141 InitState::Initializing.into(),
142 InitState::InitializingWithSleepers.into(),
143 atomic::Ordering::Relaxed,
144 // Potentially pair with initialization `Release`.
145 atomic::Ordering::Acquire,
146 ) {
147 Ok(_) => InitState::InitializingWithSleepers,
148 Err(v) => v.try_into().unwrap(),
149 }
150 }
151 InitState::InitializingWithSleepers => {
152 match sync::futex_wait(
153 &self.init_state,
154 InitState::InitializingWithSleepers.into(),
155 ) {
156 Ok(_) | Err(rustix::io::Errno::INTR) | Err(rustix::io::Errno::AGAIN) => (),
157 Err(e) => panic!("Unexpected error: {e:?}"),
158 }
159 // `Acquire` pairs with `Release` in `Uninitd` case.
160 state = InitState::try_from(self.init_state.load(atomic::Ordering::Acquire))
161 .unwrap();
162 }
163 InitState::Initd => {
164 break;
165 }
166 }
167 }
168 }
169
170 /// Force initialization and return a reference object.
171 ///
172 /// std's LazyLock returns an actual reference here and implements Deref.
173 /// We can't because of our usage of loom's UnsafeCell, which doesn't support
174 /// getting a raw reference that's not borrowed from an intermediate temp object.
175 #[inline]
176 pub fn force(&self) -> Ref<T> {
177 // `Acquire` pairs with `Release` in `Uninitd` case of `init`.
178 let state = InitState::try_from(self.init_state.load(atomic::Ordering::Acquire)).unwrap();
179
180 // Having this check in this small `inline` function that avoids calling
181 // the larger initialization function in the common case substantially
182 // improves performance in the microbenchmark.
183 if state != InitState::Initd {
184 // Call the non-inlined slow path
185 self.init(state);
186 }
187
188 Ref {
189 val: self.value.get(),
190 _phantom: PhantomData,
191 }
192 }
193
194 /// Whether `self` is initialized yet.
195 #[inline]
196 pub fn initd(&self) -> bool {
197 InitState::try_from(self.init_state.load(atomic::Ordering::Relaxed)).unwrap()
198 == InitState::Initd
199 }
200}
201
202/// We only implement Deref outside of Loom, since Loom requires an intermediate
203/// object to catch invalid accesses to our internal `UnsafeCell`s.
204#[cfg(not(loom))]
205impl<T, Init> core::ops::Deref for LazyLock<T, Init>
206where
207 Init: Producer<T>,
208{
209 type Target = T;
210
211 fn deref(&self) -> &Self::Target {
212 self.force();
213 let ptr: *mut MaybeUninit<T> = self.value.untracked_get();
214 // SAFETY: Pointer is valid and no mutable refs exist after
215 // initialization.
216 let ptr = unsafe { ptr.as_ref().unwrap() };
217 // SAFETY: `force` ensured initialization.
218 unsafe { ptr.assume_init_ref() }
219 }
220}
221
222impl<T, Init> Drop for LazyLock<T, Init> {
223 fn drop(&mut self) {
224 // `Acquire` pairs with `Release` in `init`.
225 if InitState::try_from(self.init_state.load(atomic::Ordering::Acquire)).unwrap()
226 == InitState::Initd
227 {
228 unsafe { self.value.get_mut().deref().assume_init_drop() }
229 }
230 }
231}
232
233pub struct Ref<'a, T> {
234 val: ConstPtr<MaybeUninit<T>>,
235 _phantom: PhantomData<&'a T>,
236}
237
238impl<T> core::ops::Deref for Ref<'_, T> {
239 type Target = T;
240
241 fn deref(&self) -> &Self::Target {
242 // SAFETY: LazyLock enforces no more mutable references to this data
243 // before any instances of this type are constructed, and that val is
244 // initd.
245 unsafe { self.val.deref().assume_init_ref() }
246 }
247}