godot_ffi/global.rs
1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8use std::cell::OnceCell;
9use std::sync::{Mutex, MutexGuard, PoisonError, TryLockError};
10
11/// Ergonomic global variables.
12///
13/// No more `Mutex<Option<...>>` shenanigans with lazy initialization on each use site, or `OnceLock` which limits to immutable access.
14///
15/// This type is very similar to [`once_cell::Lazy`](https://docs.rs/once_cell/latest/once_cell/sync/struct.Lazy.html) in its nature,
16/// with a minimalistic implementation. Unlike `Lazy`, it is only designed for global variables, not for local lazy initialization
17/// (following "do one thing and do it well").
18///
19/// `Global<T>` features:
20/// - `const` constructors, allowing to be used in `static` variables without `Option`.
21/// - Initialization function provided in constructor, not in each use site separately.
22/// - Ergonomic access through guards to both `&T` and `&mut T`.
23/// - Completely safe usage. Little use of `unsafe` in the implementation (for performance reasons).
24///
25/// There are two `const` methods for construction: [`new()`](Self::new) and [`default()`](Self::default).
26/// For access, you should primarily use [`lock()`](Self::lock). There is also [`try_lock()`](Self::try_lock) for special cases.
27pub struct Global<T> {
28 // When needed, this could be changed to use RwLock and separate read/write guards.
29 value: Mutex<OnceCell<T>>,
30 init_fn: fn() -> T,
31}
32
33impl<T> Global<T> {
34 /// Create `Global<T>`, providing a lazy initialization function.
35 ///
36 /// The initialization function is only called once, when the global is first accessed through [`lock()`](Self::lock).
37 pub const fn new(init_fn: fn() -> T) -> Self {
38 // Note: could be generalized to F: FnOnce() -> T + Send. See also once_cell::Lazy<T, F>.
39 Self {
40 value: Mutex::new(OnceCell::new()),
41 init_fn,
42 }
43 }
44
45 /// Create `Global<T>` with `T::default()` as initialization function.
46 ///
47 /// This is inherent rather than implementing the `Default` trait, because the latter is not `const` and thus useless in static contexts.
48 pub const fn default() -> Self
49 where
50 T: Default,
51 {
52 Self::new(T::default)
53 }
54
55 /// Returns a guard that gives shared or mutable access to the value.
56 ///
57 /// Blocks until the internal mutex is available.
58 ///
59 /// # Panics
60 /// If the initialization function panics. Once that happens, the global is considered poisoned and all future calls to `lock()` will panic.
61 /// This can currently not be recovered from.
62 pub fn lock(&self) -> GlobalGuard<'_, T> {
63 let guard = self.value.lock().unwrap_or_else(PoisonError::into_inner);
64 guard.get_or_init(self.init_fn);
65
66 // SAFETY: `get_or_init()` has already panicked if it wants to, propagating the panic to its caller,
67 // so the object is guaranteed to be initialized.
68 unsafe { GlobalGuard::new_unchecked(guard) }
69 }
70
71 /// Non-blocking access with error introspection.
72 pub fn try_lock(&self) -> Result<GlobalGuard<'_, T>, GlobalLockError<'_, T>> {
73 /// Initializes the cell and returns a guard.
74 fn init<'mutex: 'cell, 'cell, T>(
75 g: MutexGuard<'mutex, OnceCell<T>>,
76 init_fn: fn() -> T,
77 ) -> Result<GlobalGuard<'cell, T>, GlobalLockError<'cell, T>> {
78 // Initialize the cell.
79 std::panic::catch_unwind(|| g.get_or_init(init_fn))
80 .map_err(|_| GlobalLockError::InitFailed)?;
81
82 // SAFETY: `get_or_init()` has already panicked if it wants to, which has been successfully unwound,
83 // therefore the object is guaranteed to be initialized.
84 Ok(unsafe { GlobalGuard::new_unchecked(g) })
85 }
86
87 match self.value.try_lock() {
88 Ok(guard) => init(guard, self.init_fn),
89 Err(TryLockError::WouldBlock) => Err(GlobalLockError::WouldBlock),
90
91 // This is a cold branch, where the initialization function panicked.
92 Err(TryLockError::Poisoned(x)) => {
93 // We do the same things as in the hot branch.
94 let circumvent = init(x.into_inner(), self.init_fn)?;
95 Err(GlobalLockError::Poisoned { circumvent })
96 }
97 }
98 }
99}
100
101// ----------------------------------------------------------------------------------------------------------------------------------------------
102// Guards
103
104// Encapsulate private fields.
105mod global_guard {
106 use std::ops::{Deref, DerefMut};
107
108 use super::*;
109
110 /// Guard that temporarily gives access to a `Global<T>`'s inner value.
111 pub struct GlobalGuard<'a, T> {
112 // Safety invariant: `OnceCell` has been initialized.
113 mutex_guard: MutexGuard<'a, OnceCell<T>>,
114 }
115
116 impl<'a, T> GlobalGuard<'a, T> {
117 pub(super) fn new(mutex_guard: MutexGuard<'a, OnceCell<T>>) -> Option<Self> {
118 // Use an eager map instead of `mutex_guard.get().map(|_| Self { mutex_guard })`
119 // as `.get().map(…)` tries to move `mutex_guard` while borrowing an ignored value.
120 match mutex_guard.get() {
121 Some(_) => Some(Self { mutex_guard }),
122 _ => None,
123 }
124 }
125
126 /// # Safety
127 ///
128 /// The value must be initialized.
129 pub(super) unsafe fn new_unchecked(mutex_guard: MutexGuard<'a, OnceCell<T>>) -> Self {
130 debug_assert!(
131 mutex_guard.get().is_some(),
132 "safety precondition violated: cell not initialized"
133 );
134 Self::new(mutex_guard).unwrap_unchecked()
135 }
136 }
137
138 impl<T> Deref for GlobalGuard<'_, T> {
139 type Target = T;
140 fn deref(&self) -> &Self::Target {
141 // SAFETY: `GlobalGuard` guarantees that the cell is initialized.
142 unsafe { self.mutex_guard.get().unwrap_unchecked() }
143 }
144 }
145
146 impl<T> DerefMut for GlobalGuard<'_, T> {
147 fn deref_mut(&mut self) -> &mut Self::Target {
148 // SAFETY: `GlobalGuard` guarantees that the cell is initialized.
149 unsafe { self.mutex_guard.get_mut().unwrap_unchecked() }
150 }
151 }
152}
153
154pub use global_guard::GlobalGuard;
155
156/// Guard that temporarily gives access to a `Global<T>`'s inner value.
157pub enum GlobalLockError<'a, T> {
158 /// The mutex is currently locked by another thread.
159 WouldBlock,
160
161 /// A panic occurred while a lock was held. This excludes initialization errors.
162 Poisoned { circumvent: GlobalGuard<'a, T> },
163
164 /// The initialization function panicked.
165 InitFailed,
166}
167
168// ----------------------------------------------------------------------------------------------------------------------------------------------
169// Tests
170
171#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
172mod tests {
173 use std::collections::HashMap;
174
175 use super::*;
176
177 static MAP: Global<HashMap<i32, &'static str>> = Global::default();
178 static VEC: Global<Vec<i32>> = Global::new(|| vec![1, 2, 3]);
179 static FAILED: Global<()> = Global::new(|| panic!("failed"));
180 static POISON: Global<i32> = Global::new(|| 36);
181
182 #[test]
183 fn test_global_map() {
184 {
185 let mut map = MAP.lock();
186 map.insert(2, "two");
187 map.insert(3, "three");
188 }
189
190 {
191 let mut map = MAP.lock();
192 map.insert(1, "one");
193 }
194
195 let map = MAP.lock();
196 assert_eq!(map.len(), 3);
197 assert_eq!(map.get(&1), Some(&"one"));
198 assert_eq!(map.get(&2), Some(&"two"));
199 assert_eq!(map.get(&3), Some(&"three"));
200 }
201
202 #[test]
203 fn test_global_vec() {
204 {
205 let mut vec = VEC.lock();
206 vec.push(4);
207 }
208
209 let vec = VEC.lock();
210 assert_eq!(*vec, &[1, 2, 3, 4]);
211 }
212
213 #[test]
214 fn test_global_would_block() {
215 let vec = VEC.lock();
216
217 let vec2 = VEC.try_lock();
218 assert!(matches!(vec2, Err(GlobalLockError::WouldBlock)));
219 }
220
221 #[test]
222 fn test_global_init_failed() {
223 let guard = FAILED.try_lock();
224 assert!(matches!(guard, Err(GlobalLockError::InitFailed)));
225
226 // Subsequent access still returns same error.
227 let guard = FAILED.try_lock();
228 assert!(matches!(guard, Err(GlobalLockError::InitFailed)));
229 }
230
231 #[test]
232 fn test_global_poison() {
233 let result = std::panic::catch_unwind(|| {
234 let guard = POISON.lock();
235 panic!("poison injection");
236 });
237 assert!(result.is_err());
238
239 let guard = POISON.try_lock();
240 let Err(GlobalLockError::Poisoned { circumvent }) = guard else {
241 panic!("expected GlobalLockError::Poisoned");
242 };
243 assert_eq!(*circumvent, 36);
244 }
245}