Skip to content

Commit 4865ec4

Browse files
committed
Add user singletons.
Allow to register user-defined engine singletons via #[class(singleton)]`.
1 parent e01d2ea commit 4865ec4

File tree

7 files changed

+276
-27
lines changed

7 files changed

+276
-27
lines changed

godot-core/src/obj/traits.rs

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

688+
/// Trait for user-defined singleton classes in Godot.
689+
///
690+
/// Implementing this trait allows accessing a registered singleton instance through [`singleton()`][Singleton::singleton].
691+
/// User singletons should be registered under their class name – otherwise some Godot components (for example GDScript before 4.4) might have trouble handling them,
692+
/// and the editor might crash when using `T::singleton()`.
693+
///
694+
/// There should be only one instance of a given singleton class in the engine, valid as long as the library is loaded.
695+
/// Therefore, user singletons are limited to classes with manual memory management (ones not inheriting from `RefCounted`).
696+
///
697+
/// # Registration
698+
///
699+
/// Godot-rust provides a way to register given class as an Engine Singleton with [`#[class(singleton)]`](../prelude/derive.GodotClass.html#user-engine-singletons).
700+
///
701+
/// Alternatively, user singleton can be registered manually:
702+
///
703+
/// ```no_run
704+
/// # use godot::prelude::*;
705+
/// #[derive(GodotClass)]
706+
/// #[class(init, base = Object)]
707+
/// struct MyEngineSingleton {}
708+
///
709+
/// // Provides blanket implementation allowing to use MyEngineSingleton::singleton().
710+
/// // Ensures that `MyEngineSingleton` is a valid singleton (i.e., a non-refcounted GodotClass).
711+
/// impl UserSingleton for MyEngineSingleton {}
712+
///
713+
/// #[gdextension]
714+
/// unsafe impl ExtensionLibrary for MyExtension {
715+
/// fn on_stage_init(stage: InitStage) {
716+
/// if matches!(stage, InitStage::MainLoop) {
717+
/// let obj = MyEngineSingleton::new_alloc();
718+
/// Engine::singleton()
719+
/// .register_singleton(&MyEngineSingleton::class_id().to_string_name(), &obj);
720+
/// }
721+
///
722+
/// fn on_stage_deinit(stage: InitStage) {
723+
/// if matches!(stage, InitStage::MainLoop) {
724+
/// let obj = MyEngineSingleton::singleton();
725+
/// Engine::singleton()
726+
/// .unregister_singleton(&MyEngineSingleton::class_id().to_string_name());
727+
/// obj.free();
728+
/// }
729+
/// }
730+
/// }
731+
/// }
732+
/// ```
733+
// For now exists mostly as a marker trait and a way to provide blanket implementation for `Singleton` trait.
734+
pub trait UserSingleton:
735+
GodotClass + Bounds<Declarer = bounds::DeclUser, Memory = bounds::MemManual>
736+
{
737+
}
738+
739+
impl<T> Singleton for T
740+
where
741+
T: UserSingleton + Inherits<crate::classes::Object>,
742+
{
743+
fn singleton() -> Gd<T> {
744+
// Note: with any safeguards enabled `singleton_unchecked` will panic if Singleton can't be retrieved.
745+
746+
// SAFETY: The caller must ensure that `class_name` corresponds to the actual class name of type `T`.
747+
// This is always true for `#[class(singleton)]`.
748+
unsafe {
749+
crate::classes::singleton_unchecked(&<T as GodotClass>::class_id().to_string_name())
750+
}
751+
}
752+
}
753+
688754
impl<T> NewAlloc for T
689755
where
690756
T: cap::GodotDefault + Bounds<Memory = bounds::MemManual>,
@@ -705,6 +771,7 @@ pub mod cap {
705771
use super::*;
706772
use crate::builtin::{StringName, Variant};
707773
use crate::meta::PropertyInfo;
774+
use crate::obj::{Base, Bounds, Gd};
708775
use crate::storage::{IntoVirtualMethodReceiver, VirtualMethodReceiver};
709776

710777
/// 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 immediately,
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 behaviors 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)