Skip to content

Commit f6dc092

Browse files
committed
Add Global::try_lock(), replace another occurrence
1 parent 7c93cb0 commit f6dc092

File tree

2 files changed

+115
-31
lines changed

2 files changed

+115
-31
lines changed

godot-core/src/registry.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,17 @@ use crate::private::as_storage;
1515
use crate::storage::InstanceStorage;
1616
use godot_ffi as sys;
1717

18-
use sys::interface_fn;
18+
use sys::{interface_fn, Global, GlobalGuard, GlobalLockError};
1919

2020
use std::any::Any;
2121
use std::collections::HashMap;
22-
use std::sync::{Mutex, MutexGuard, TryLockError};
2322
use std::{fmt, ptr};
2423

2524
// Needed for class unregistering. The variable is populated during class registering. There is no actual concurrency here, because Godot
2625
// calls register/unregister in the main thread. Mutex is just casual way to ensure safety in this non-performance-critical path.
2726
// Note that we panic on concurrent access instead of blocking (fail-fast approach). If that happens, most likely something changed on Godot
2827
// side and analysis required to adopt these changes.
29-
// For now, don't use Global<T> because we need try_lock(), which isn't yet provided.
30-
static LOADED_CLASSES: Mutex<Option<HashMap<InitLevel, Vec<ClassName>>>> = Mutex::new(None);
28+
static LOADED_CLASSES: Global<HashMap<InitLevel, Vec<ClassName>>> = Global::default();
3129

3230
// TODO(bromeon): some information coming from the proc-macro API is deferred through PluginComponent, while others is directly
3331
// translated to code. Consider moving more code to the PluginComponent, which allows for more dynamic registration and will
@@ -275,9 +273,7 @@ pub fn auto_register_classes(init_level: InitLevel) {
275273
fill_class_info(elem.component.clone(), class_info);
276274
});
277275

278-
let mut loaded_classes_guard = get_loaded_classes_with_mutex();
279-
let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default);
280-
276+
let mut loaded_classes_by_level = global_loaded_classes();
281277
for info in map.into_values() {
282278
out!(
283279
"Register class: {} at level `{init_level:?}`",
@@ -297,8 +293,7 @@ pub fn auto_register_classes(init_level: InitLevel) {
297293
}
298294

299295
pub fn unregister_classes(init_level: InitLevel) {
300-
let mut loaded_classes_guard = get_loaded_classes_with_mutex();
301-
let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default);
296+
let mut loaded_classes_by_level = global_loaded_classes();
302297
let loaded_classes_current_level = loaded_classes_by_level
303298
.remove(&init_level)
304299
.unwrap_or_default();
@@ -308,15 +303,15 @@ pub fn unregister_classes(init_level: InitLevel) {
308303
}
309304
}
310305

311-
fn get_loaded_classes_with_mutex() -> MutexGuard<'static, Option<HashMap<InitLevel, Vec<ClassName>>>>
312-
{
306+
fn global_loaded_classes() -> GlobalGuard<'static, HashMap<InitLevel, Vec<ClassName>>> {
313307
match LOADED_CLASSES.try_lock() {
314308
Ok(it) => it,
315309
Err(err) => match err {
316-
TryLockError::Poisoned(_err) => panic!(
310+
GlobalLockError::Poisoned {..} => panic!(
317311
"global lock for loaded classes poisoned; class registration or deregistration may have panicked"
318312
),
319-
TryLockError::WouldBlock => panic!("unexpected concurrent access to global lock for loaded classes"),
313+
GlobalLockError::WouldBlock => panic!("unexpected concurrent access to global lock for loaded classes"),
314+
GlobalLockError::InitFailed => unreachable!("global lock for loaded classes not initialized"),
320315
},
321316
}
322317
}

godot-ffi/src/global.rs

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
use std::ops::{Deref, DerefMut};
9-
use std::sync::{Mutex, MutexGuard};
9+
use std::sync::{Mutex, MutexGuard, TryLockError};
1010

1111
/// Ergonomic global variables.
1212
///
@@ -19,8 +19,8 @@ use std::sync::{Mutex, MutexGuard};
1919
/// - Ergonomic access through guards to both `&T` and `&mut T`.
2020
/// - Completely safe usage. Almost completely safe implementation (besides `unreachable_unchecked`).
2121
///
22-
/// There are two main methods: [`new()`](Self::new) and [`lock()`](Self::lock). Additionally, [`default()`](Self::default) is provided
23-
/// for convenience.
22+
/// There are two methods for construction: [`new()`](Self::new) and [`default()`](Self::default).
23+
/// For access, you should primarily use [`lock()`](Self::lock). There is also [`try_lock()`](Self::try_lock) for special cases.
2424
pub struct Global<T> {
2525
// When needed, this could be changed to use RwLock and separate read/write guards.
2626
value: Mutex<InitState<T>>,
@@ -55,24 +55,57 @@ impl<T> Global<T> {
5555
/// If the initialization function panics. Once that happens, the global is considered poisoned and all future calls to `lock()` will panic.
5656
/// This can currently not be recovered from.
5757
pub fn lock(&self) -> GlobalGuard<'_, T> {
58-
let guard = self.ensure_init();
59-
debug_assert!(matches!(*guard, InitState::Initialized(_)));
58+
let mutex_guard = self
59+
.value
60+
.lock()
61+
.expect("Global<T> poisoned; a thread has panicked while holding a lock to it");
6062

61-
GlobalGuard { guard }
63+
let guard = Self::ensure_init(mutex_guard, true)
64+
.unwrap_or_else(|| panic!("previous Global<T> initialization failed due to panic"));
65+
66+
debug_assert!(matches!(*guard.mutex_guard, InitState::Initialized(_)));
67+
guard
6268
}
6369

64-
fn ensure_init(&self) -> MutexGuard<'_, InitState<T>> {
65-
let mut guard = self.value.lock().expect("lock poisoned");
66-
let pending_state = match &mut *guard {
70+
/// Non-panicking access with error introspection.
71+
pub fn try_lock(&self) -> Result<GlobalGuard<'_, T>, GlobalLockError<'_, T>> {
72+
let guard = match self.value.try_lock() {
73+
Ok(mutex_guard) => Self::ensure_init(mutex_guard, false),
74+
Err(TryLockError::WouldBlock) => {
75+
return Err(GlobalLockError::WouldBlock);
76+
}
77+
Err(TryLockError::Poisoned(poisoned)) => {
78+
return Err(GlobalLockError::Poisoned {
79+
circumvent: GlobalGuard {
80+
mutex_guard: poisoned.into_inner(),
81+
},
82+
});
83+
}
84+
};
85+
86+
match guard {
87+
None => Err(GlobalLockError::InitFailed),
88+
Some(guard) => {
89+
debug_assert!(matches!(*guard.mutex_guard, InitState::Initialized(_)));
90+
Ok(guard)
91+
}
92+
}
93+
}
94+
95+
fn ensure_init(
96+
mut mutex_guard: MutexGuard<'_, InitState<T>>,
97+
may_panic: bool,
98+
) -> Option<GlobalGuard<'_, T>> {
99+
let pending_state = match &mut *mutex_guard {
67100
InitState::Initialized(_) => {
68-
return guard;
101+
return Some(GlobalGuard { mutex_guard });
69102
}
70103
InitState::TransientInitializing => {
71104
// SAFETY: only set inside this function and all paths (panic + return) leave the enum in a different state.
72105
unsafe { std::hint::unreachable_unchecked() };
73106
}
74107
InitState::Failed => {
75-
panic!("previous Global<T> initialization failed due to panic")
108+
return None;
76109
}
77110
state @ InitState::Pending(_) => {
78111
std::mem::replace(state, InitState::TransientInitializing)
@@ -87,15 +120,21 @@ impl<T> Global<T> {
87120
// Unwinding should be safe here, as there is no unsafe code relying on it.
88121
let init_fn = std::panic::AssertUnwindSafe(init_fn);
89122
match std::panic::catch_unwind(init_fn) {
90-
Ok(value) => *guard = InitState::Initialized(value),
123+
Ok(value) => *mutex_guard = InitState::Initialized(value),
91124
Err(e) => {
92125
eprintln!("panic during Global<T> initialization");
93-
*guard = InitState::Failed;
94-
std::panic::resume_unwind(e);
126+
*mutex_guard = InitState::Failed;
127+
128+
if may_panic {
129+
std::panic::resume_unwind(e);
130+
} else {
131+
// Note: this currently swallows panic.
132+
return None;
133+
}
95134
}
96135
};
97136

98-
guard
137+
Some(GlobalGuard { mutex_guard })
99138
}
100139
}
101140

@@ -104,23 +143,38 @@ impl<T> Global<T> {
104143

105144
/// Guard that temporarily gives access to a `Global<T>`'s inner value.
106145
pub struct GlobalGuard<'a, T> {
107-
guard: MutexGuard<'a, InitState<T>>,
146+
mutex_guard: MutexGuard<'a, InitState<T>>,
108147
}
109148

110149
impl<T> Deref for GlobalGuard<'_, T> {
111150
type Target = T;
112151

113152
fn deref(&self) -> &Self::Target {
114-
self.guard.unwrap_ref()
153+
self.mutex_guard.unwrap_ref()
115154
}
116155
}
117156

118157
impl<T> DerefMut for GlobalGuard<'_, T> {
119158
fn deref_mut(&mut self) -> &mut Self::Target {
120-
self.guard.unwrap_mut()
159+
self.mutex_guard.unwrap_mut()
121160
}
122161
}
123162

163+
// ----------------------------------------------------------------------------------------------------------------------------------------------
164+
// Errors
165+
166+
/// Guard that temporarily gives access to a `Global<T>`'s inner value.
167+
pub enum GlobalLockError<'a, T> {
168+
/// The mutex is currently locked by another thread.
169+
WouldBlock,
170+
171+
/// A panic occurred while a lock was held. This excludes initialization errors.
172+
Poisoned { circumvent: GlobalGuard<'a, T> },
173+
174+
/// The initialization function panicked.
175+
InitFailed,
176+
}
177+
124178
// ----------------------------------------------------------------------------------------------------------------------------------------------
125179
// Internals
126180

@@ -163,6 +217,8 @@ mod tests {
163217

164218
static MAP: Global<HashMap<i32, &'static str>> = Global::default();
165219
static VEC: Global<Vec<i32>> = Global::new(|| vec![1, 2, 3]);
220+
static FAILED: Global<()> = Global::new(|| panic!("failed"));
221+
static POISON: Global<i32> = Global::new(|| 36);
166222

167223
#[test]
168224
fn test_global_map() {
@@ -194,4 +250,37 @@ mod tests {
194250
let vec = VEC.lock();
195251
assert_eq!(*vec, &[1, 2, 3, 4]);
196252
}
253+
254+
#[test]
255+
fn test_global_would_block() {
256+
let vec = VEC.lock();
257+
258+
let vec2 = VEC.try_lock();
259+
assert!(matches!(vec2, Err(GlobalLockError::WouldBlock)));
260+
}
261+
262+
#[test]
263+
fn test_global_init_failed() {
264+
let guard = FAILED.try_lock();
265+
assert!(matches!(guard, Err(GlobalLockError::InitFailed)));
266+
267+
// Subsequent access still returns same error.
268+
let guard = FAILED.try_lock();
269+
assert!(matches!(guard, Err(GlobalLockError::InitFailed)));
270+
}
271+
272+
#[test]
273+
fn test_global_poison() {
274+
let result = std::panic::catch_unwind(|| {
275+
let guard = POISON.lock();
276+
panic!("poison injection");
277+
});
278+
assert!(result.is_err());
279+
280+
let guard = POISON.try_lock();
281+
let Err(GlobalLockError::Poisoned { circumvent }) = guard else {
282+
panic!("expected GlobalLockError::Poisoned");
283+
};
284+
assert_eq!(*circumvent, 36);
285+
}
197286
}

0 commit comments

Comments
 (0)