Skip to content

Commit 13af9d6

Browse files
authored
Merge pull request #547 from godot-rust/feature/global-cell
`Global<T>` to store global variables ergonomically
2 parents c92de58 + f6dc092 commit 13af9d6

File tree

8 files changed

+348
-52
lines changed

8 files changed

+348
-52
lines changed

godot-core/src/builtin/meta/class_name.rs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,15 @@
88
use godot_ffi as sys;
99
use std::collections::HashMap;
1010
use std::ffi::CStr;
11-
use std::{fmt, sync};
12-
11+
use std::fmt;
1312
use std::hash::{Hash, Hasher};
1413

1514
use crate::builtin::*;
15+
use sys::Global;
1616

17-
// Why is this so ugly?
18-
// - Mutex: needed for global access (Sync).
19-
// - Option: needed to initialize lazily, because HashMap::new() is not const.
20-
// - Box: needed for pointer stability (HashMap insertion may invalidate pointers -- with_capacity() would be an alternative,
21-
// but we don't know how many classes).
22-
// In theory a static mut would do the job, however if we allow for manual class registration (at any time), we need to count with
23-
// later adjustments.
24-
// We may also consider OnceLock with a static per class, but that needs to be code-generated (for #[derive] and engine classes), and
25-
// any manually registered classes would need to replicate it later.
26-
static CACHED_STRING_NAMES: sync::Mutex<Option<HashMap<ClassName, Box<StringName>>>> =
27-
sync::Mutex::new(None);
17+
// Box is needed for pointer stability (HashMap insertion may invalidate pointers -- with_capacity() would be an alternative,
18+
// but we don't know how many classes).
19+
static CACHED_STRING_NAMES: Global<HashMap<ClassName, Box<StringName>>> = Global::default();
2820

2921
/// Name of a class registered with Godot.
3022
///
@@ -80,8 +72,7 @@ impl ClassName {
8072

8173
// Takes a closure because the mutex guard protects the reference; so the &StringName cannot leave the scope.
8274
fn with_string_name<R>(&self, func: impl FnOnce(&StringName) -> R) -> R {
83-
let mut guard = CACHED_STRING_NAMES.lock().unwrap();
84-
let map = guard.get_or_insert_with(HashMap::new);
75+
let mut map = CACHED_STRING_NAMES.lock();
8576

8677
let value = map
8778
.entry(*self)

godot-core/src/builtin/meta/registration/constant.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl IntegerConstant {
2727
}
2828
}
2929

30-
fn register(&self, class_name: &ClassName, enum_name: &StringName, is_bitfield: bool) {
30+
fn register(&self, class_name: ClassName, enum_name: &StringName, is_bitfield: bool) {
3131
unsafe {
3232
interface_fn!(classdb_register_extension_class_integer_constant)(
3333
sys::get_library(),
@@ -56,7 +56,7 @@ pub enum ConstantKind {
5656
}
5757

5858
impl ConstantKind {
59-
fn register(&self, class_name: &ClassName) {
59+
fn register(&self, class_name: ClassName) {
6060
match self {
6161
ConstantKind::Integer(integer) => {
6262
integer.register(class_name, &StringName::default(), false)
@@ -87,6 +87,6 @@ impl ExportConstant {
8787
}
8888

8989
pub fn register(&self) {
90-
self.kind.register(&self.class_name)
90+
self.kind.register(self.class_name)
9191
}
9292
}

godot-core/src/engine/mod.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77

88
//! Godot engine classes and methods.
99
10-
// Re-exports of generated symbols
1110
use crate::builtin::{GString, NodePath};
1211
use crate::obj::dom::EngineDomain;
1312
use crate::obj::{Gd, GodotClass, Inherits, InstanceId};
13+
use std::collections::HashSet;
1414

15+
// Re-exports of generated symbols
1516
pub use crate::gen::central::global;
1617
pub use crate::gen::classes::*;
1718
pub use crate::gen::utilities;
@@ -246,15 +247,13 @@ pub(crate) fn ensure_object_alive(
246247

247248
#[cfg(debug_assertions)]
248249
pub(crate) fn ensure_object_inherits(
249-
derived: &ClassName,
250-
base: &ClassName,
250+
derived: ClassName,
251+
base: ClassName,
251252
instance_id: InstanceId,
252253
) -> bool {
253-
// TODO static cache.
254-
255254
if derived == base
256-
|| base == &Object::class_name() // always true
257-
|| ClassDb::singleton().is_parent_class(derived.to_string_name(), base.to_string_name())
255+
|| base == Object::class_name() // for Object base, anything inherits by definition
256+
|| is_derived_base_cached(derived, base)
258257
{
259258
return true;
260259
}
@@ -265,6 +264,30 @@ pub(crate) fn ensure_object_inherits(
265264
)
266265
}
267266

267+
/// Checks if `derived` inherits from `base`, using a cache for _successful_ queries.
268+
#[cfg(debug_assertions)]
269+
fn is_derived_base_cached(derived: ClassName, base: ClassName) -> bool {
270+
use sys::Global;
271+
static CACHE: Global<HashSet<(ClassName, ClassName)>> = Global::default();
272+
273+
let mut cache = CACHE.lock();
274+
let key = (derived, base);
275+
if cache.contains(&key) {
276+
return true;
277+
}
278+
279+
// Query Godot API (takes linear time in depth of inheritance tree).
280+
let is_parent_class =
281+
ClassDb::singleton().is_parent_class(derived.to_string_name(), base.to_string_name());
282+
283+
// Insert only successful queries. Those that fail are on the error path already and don't need to be fast.
284+
if is_parent_class {
285+
cache.insert(key);
286+
}
287+
288+
is_parent_class
289+
}
290+
268291
// Separate function, to avoid constructing string twice
269292
// Note that more optimizations than that likely make no sense, as loading is quite expensive
270293
fn load_impl<T>(path: &GString) -> Option<Gd<T>>

godot-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub mod private {
5151
pub use crate::gen::classes::class_macros;
5252
pub use crate::registry::{callbacks, ClassPlugin, ErasedRegisterFn, PluginComponent};
5353
pub use crate::storage::as_storage;
54-
pub use godot_ffi::out;
54+
pub use sys::out;
5555

5656
use crate::{log, sys};
5757

godot-core/src/obj/rtti.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ impl ObjectRtti {
4444
#[inline]
4545
pub fn check_type<T: GodotClass>(&self) -> InstanceId {
4646
#[cfg(debug_assertions)]
47-
crate::engine::ensure_object_inherits(&self.class_name, &T::class_name(), self.instance_id);
47+
crate::engine::ensure_object_inherits(self.class_name, T::class_name(), self.instance_id);
4848

4949
self.instance_id
5050
}

godot-core/src/registry.rs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,27 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
#![allow(dead_code)] // FIXME
9-
8+
use crate::builtin::meta::ClassName;
9+
use crate::builtin::StringName;
1010
use crate::init::InitLevel;
1111
use crate::log;
1212
use crate::obj::*;
13+
use crate::out;
1314
use crate::private::as_storage;
1415
use crate::storage::InstanceStorage;
1516
use godot_ffi as sys;
1617

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

19-
use crate::builtin::meta::ClassName;
20-
use crate::builtin::StringName;
21-
use crate::out;
2220
use std::any::Any;
2321
use std::collections::HashMap;
24-
use std::sync::{Mutex, MutexGuard, TryLockError};
2522
use std::{fmt, ptr};
2623

27-
// For now, that variable is needed for class unregistering. It's populated during class
28-
// registering. There is no actual concurrency here, because Godot call register/unregister in main
29-
// thread - Mutex is just casual way to ensure safety in this performance non-critical path.
30-
// Note that we panic on concurrent access instead of blocking - that's fail fast approach. If that
31-
// happen, most likely something changed on Godot side and analysis required to adopt these changes.
32-
static LOADED_CLASSES: Mutex<Option<HashMap<InitLevel, Vec<ClassName>>>> = Mutex::new(None);
24+
// Needed for class unregistering. The variable is populated during class registering. There is no actual concurrency here, because Godot
25+
// calls register/unregister in the main thread. Mutex is just casual way to ensure safety in this non-performance-critical path.
26+
// Note that we panic on concurrent access instead of blocking (fail-fast approach). If that happens, most likely something changed on Godot
27+
// side and analysis required to adopt these changes.
28+
static LOADED_CLASSES: Global<HashMap<InitLevel, Vec<ClassName>>> = Global::default();
3329

3430
// TODO(bromeon): some information coming from the proc-macro API is deferred through PluginComponent, while others is directly
3531
// translated to code. Consider moving more code to the PluginComponent, which allows for more dynamic registration and will
@@ -182,6 +178,7 @@ struct ClassRegistrationInfo {
182178
godot_params: sys::GDExtensionClassCreationInfo,
183179
#[cfg(since_api = "4.2")]
184180
godot_params: sys::GDExtensionClassCreationInfo2,
181+
#[allow(dead_code)] // Currently unused; may be useful for diagnostics in the future.
185182
init_level: InitLevel,
186183
is_editor_plugin: bool,
187184
}
@@ -276,9 +273,7 @@ pub fn auto_register_classes(init_level: InitLevel) {
276273
fill_class_info(elem.component.clone(), class_info);
277274
});
278275

279-
let mut loaded_classes_guard = get_loaded_classes_with_mutex();
280-
let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default);
281-
276+
let mut loaded_classes_by_level = global_loaded_classes();
282277
for info in map.into_values() {
283278
out!(
284279
"Register class: {} at level `{init_level:?}`",
@@ -298,26 +293,25 @@ pub fn auto_register_classes(init_level: InitLevel) {
298293
}
299294

300295
pub fn unregister_classes(init_level: InitLevel) {
301-
let mut loaded_classes_guard = get_loaded_classes_with_mutex();
302-
let loaded_classes_by_level = loaded_classes_guard.get_or_insert_with(HashMap::default);
296+
let mut loaded_classes_by_level = global_loaded_classes();
303297
let loaded_classes_current_level = loaded_classes_by_level
304298
.remove(&init_level)
305299
.unwrap_or_default();
306300
out!("Unregistering classes of level {init_level:?}...");
307301
for class_name in loaded_classes_current_level.iter().rev() {
308-
unregister_class_raw(class_name);
302+
unregister_class_raw(*class_name);
309303
}
310304
}
311305

312-
fn get_loaded_classes_with_mutex() -> MutexGuard<'static, Option<HashMap<InitLevel, Vec<ClassName>>>>
313-
{
306+
fn global_loaded_classes() -> GlobalGuard<'static, HashMap<InitLevel, Vec<ClassName>>> {
314307
match LOADED_CLASSES.try_lock() {
315308
Ok(it) => it,
316309
Err(err) => match err {
317-
TryLockError::Poisoned(_err) => panic!(
318-
"LOADED_CLASSES poisoned. seems like class registration or deregistration panicked."
310+
GlobalLockError::Poisoned {..} => panic!(
311+
"global lock for loaded classes poisoned; class registration or deregistration may have panicked"
319312
),
320-
TryLockError::WouldBlock => panic!("unexpected concurrent access detected to 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"),
321315
},
322316
}
323317
}
@@ -494,7 +488,7 @@ fn register_class_raw(mut info: ClassRegistrationInfo) {
494488
assert!(!info.is_editor_plugin);
495489
}
496490

497-
fn unregister_class_raw(class_name: &ClassName) {
491+
fn unregister_class_raw(class_name: ClassName) {
498492
out!("Unregister class: {class_name}");
499493
unsafe {
500494
#[allow(clippy::let_unit_value)]

0 commit comments

Comments
 (0)