Skip to content

Commit 3391ae0

Browse files
committed
Add user singletons.
Allow to register user-defined engine singletons via #[class(singleton)]`.
1 parent 309881c commit 3391ae0

File tree

7 files changed

+234
-27
lines changed

7 files changed

+234
-27
lines changed

godot-core/src/obj/traits.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,30 @@ pub trait Singleton: GodotClass {
685685
fn singleton() -> Gd<Self>;
686686
}
687687

688+
/// Trait for user-defined singleton classes in Godot.
689+
///
690+
/// There should be only one instance of each singleton class in the engine, accessible through [`singleton()`][Singleton::singleton].
691+
// For now exists mostly as a marker trait and a way to provide blanket implementation for `Singleton` trait.
692+
pub trait UserSingleton:
693+
GodotClass + Bounds<Declarer = bounds::DeclUser, Memory = bounds::MemManual>
694+
{
695+
}
696+
697+
impl<T> Singleton for T
698+
where
699+
T: UserSingleton + Inherits<crate::classes::Object>,
700+
{
701+
fn singleton() -> Gd<T> {
702+
// Note: with any safeguards enabled `singleton_unchecked` will panic if Singleton can't be retrieved.
703+
704+
// SAFETY: The caller must ensure that `class_name` corresponds to the actual class name of type `T`.
705+
// This is always true for `#[class(singleton)]`.
706+
unsafe {
707+
crate::classes::singleton_unchecked(&<T as GodotClass>::class_id().to_string_name())
708+
}
709+
}
710+
}
711+
688712
impl<T> NewAlloc for T
689713
where
690714
T: cap::GodotDefault + Bounds<Memory = bounds::MemManual>,
@@ -705,6 +729,7 @@ pub mod cap {
705729
use super::*;
706730
use crate::builtin::{StringName, Variant};
707731
use crate::meta::PropertyInfo;
732+
use crate::obj::{Base, Bounds, Gd};
708733
use crate::storage::{IntoVirtualMethodReceiver, VirtualMethodReceiver};
709734

710735
/// Trait for all classes that are default-constructible from the Godot engine.

godot-core/src/registry/class.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap<any::TypeId, Ve
6161
pub struct LoadedClass {
6262
name: ClassId,
6363
is_editor_plugin: bool,
64+
unregister_singleton_fn: Option<fn()>,
6465
}
6566

6667
/// Represents a class which is currently loaded and retained in memory -- including metadata.
@@ -93,6 +94,8 @@ struct ClassRegistrationInfo {
9394
user_register_fn: Option<ErasedRegisterFn>,
9495
default_virtual_fn: Option<GodotGetVirtual>, // Optional (set if there is at least one OnReady field)
9596
user_virtual_fn: Option<GodotGetVirtual>, // Optional (set if there is a `#[godot_api] impl I*`)
97+
register_singleton_fn: Option<fn()>,
98+
unregister_singleton_fn: Option<fn()>,
9699

97100
/// Godot low-level class creation parameters.
98101
godot_params: GodotCreationInfo,
@@ -180,6 +183,8 @@ pub(crate) fn register_class<
180183
is_editor_plugin: false,
181184
dynify_fns_by_trait: HashMap::new(),
182185
component_already_filled: Default::default(), // [false; N]
186+
register_singleton_fn: None,
187+
unregister_singleton_fn: None,
183188
});
184189
}
185190

@@ -215,10 +220,17 @@ pub fn auto_register_classes(init_level: InitLevel) {
215220
// but it is much slower and doesn't guarantee that all the dependent classes will be already loaded in most cases.
216221
register_classes_and_dyn_traits(&mut map, init_level);
217222

218-
// Editor plugins should be added to the editor AFTER all the classes has been registered.
219-
// Adding EditorPlugin to the Editor before registering all the classes it depends on might result in crash.
223+
// Before Godot 4.4.1 editor plugins were being added to the editor imminently,
224+
// triggering their lifecycle methods – even before their dependencies (properties and whatnot) has been registered.
225+
// During hot-reload Godot changes all GDExtension class instances into their base classes.
226+
// These two behaviour combined were leading to crashes.
227+
// Since Godot 4.4.1 adding new EditorPlugin to the editor is being postponed until the end of the frame (i.e. after library registration).
228+
// See also: (https://github.com/godot-rust/gdext/issues/1132)
220229
let mut editor_plugins: Vec<ClassId> = Vec::new();
221230

231+
// Similarly to EnginePlugins – freshly instantiated engine singleton might depend on some not-yet-registered classes.
232+
let mut singletons: Vec<fn()> = Vec::new();
233+
222234
// Actually register all the classes.
223235
for info in map.into_values() {
224236
#[cfg(feature = "debug-log")]
@@ -228,15 +240,19 @@ pub fn auto_register_classes(init_level: InitLevel) {
228240
editor_plugins.push(info.class_name);
229241
}
230242

243+
if let Some(register_singleton_fn) = info.register_singleton_fn {
244+
singletons.push(register_singleton_fn)
245+
}
246+
231247
register_class_raw(info);
232248

233249
out!("Class {class_name} loaded.");
234250
}
235251

236-
// Will imminently add given class to the editor.
237-
// It is expected and beneficial behaviour while we load library for the first time
238-
// but (for now) might lead to some issues during hot reload.
239-
// See also: (https://github.com/godot-rust/gdext/issues/1132)
252+
for register_singleton_fn in singletons {
253+
register_singleton_fn()
254+
}
255+
240256
for editor_plugin_class_name in editor_plugins {
241257
unsafe { interface_fn!(editor_add_plugin)(editor_plugin_class_name.string_sys()) };
242258
}
@@ -259,6 +275,7 @@ fn register_classes_and_dyn_traits(
259275
let loaded_class = LoadedClass {
260276
name: class_name,
261277
is_editor_plugin: info.is_editor_plugin,
278+
unregister_singleton_fn: info.unregister_singleton_fn,
262279
};
263280
let metadata = ClassMetadata {};
264281

@@ -420,6 +437,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
420437
register_properties_fn,
421438
free_fn,
422439
default_get_virtual_fn,
440+
unregister_singleton_fn,
441+
register_singleton_fn,
423442
is_tool,
424443
is_editor_plugin,
425444
is_internal,
@@ -431,6 +450,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
431450
c.default_virtual_fn = default_get_virtual_fn;
432451
c.register_properties_fn = Some(register_properties_fn);
433452
c.is_editor_plugin = is_editor_plugin;
453+
c.register_singleton_fn = register_singleton_fn;
454+
c.unregister_singleton_fn = unregister_singleton_fn;
434455

435456
// Classes marked #[class(no_init)] are translated to "abstract" in Godot. This disables their default constructor.
436457
// "Abstract" is a misnomer -- it's not an abstract base class, but rather a "utility/static class" (although it can have instance
@@ -632,6 +653,12 @@ fn unregister_class_raw(class: LoadedClass) {
632653
out!("> Editor plugin removed");
633654
}
634655

656+
// Similarly to EditorPlugin – given instance is being freed and will not be recreated
657+
// during hot reload (a new, independent one will be created instead).
658+
if let Some(unregister_singleton_fn) = class.unregister_singleton_fn {
659+
unregister_singleton_fn();
660+
}
661+
635662
#[allow(clippy::let_unit_value)]
636663
let _: () = unsafe {
637664
interface_fn!(classdb_unregister_extension_class)(
@@ -670,6 +697,8 @@ fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo {
670697
user_register_fn: None,
671698
default_virtual_fn: None,
672699
user_virtual_fn: None,
700+
register_singleton_fn: None,
701+
unregister_singleton_fn: None,
673702
godot_params: default_creation_info(),
674703
init_level: InitLevel::Scene,
675704
is_editor_plugin: false,

godot-core/src/registry/plugin.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use std::{any, fmt};
1010

1111
use crate::init::InitLevel;
1212
use crate::meta::ClassId;
13-
use crate::obj::{bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, UserClass};
13+
use crate::obj::{
14+
bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, NewAlloc, Singleton, UserClass,
15+
UserSingleton,
16+
};
1417
use crate::registry::callbacks;
1518
use crate::registry::class::GodotGetVirtual;
1619
use crate::{classes, sys};
@@ -180,6 +183,12 @@ pub struct Struct {
180183
instance: sys::GDExtensionClassInstancePtr,
181184
),
182185

186+
/// `#[class(singleton)]`
187+
pub(crate) register_singleton_fn: Option<fn()>,
188+
189+
/// `#[class(singleton)]`
190+
pub(crate) unregister_singleton_fn: Option<fn()>,
191+
183192
/// Calls `__before_ready()`, if there is at least one `OnReady` field. Used if there is no `#[godot_api] impl` block
184193
/// overriding ready.
185194
pub(crate) default_get_virtual_fn: Option<GodotGetVirtual>,
@@ -209,6 +218,8 @@ impl Struct {
209218
raw: callbacks::register_user_properties::<T>,
210219
},
211220
free_fn: callbacks::free::<T>,
221+
register_singleton_fn: None,
222+
unregister_singleton_fn: None,
212223
default_get_virtual_fn: None,
213224
is_tool: false,
214225
is_editor_plugin: false,
@@ -257,6 +268,28 @@ impl Struct {
257268
self
258269
}
259270

271+
pub fn with_singleton<T>(mut self) -> Self
272+
where
273+
T: UserSingleton
274+
+ Bounds<Memory = bounds::MemManual, Declarer = bounds::DeclUser>
275+
+ NewAlloc
276+
+ Inherits<classes::Object>,
277+
{
278+
self.register_singleton_fn = Some(|| {
279+
crate::classes::Engine::singleton()
280+
.register_singleton(&T::class_id().to_string_name(), &T::new_alloc());
281+
});
282+
283+
self.unregister_singleton_fn = Some(|| {
284+
let singleton = T::singleton();
285+
crate::classes::Engine::singleton()
286+
.unregister_singleton(&T::class_id().to_string_name());
287+
singleton.free();
288+
});
289+
290+
self
291+
}
292+
260293
pub fn with_internal(mut self) -> Self {
261294
self.is_internal = true;
262295
self

0 commit comments

Comments
 (0)