Skip to content

Commit d54797c

Browse files
authored
Merge pull request #1399 from Yarwin/add-user-singletons
Allow to register user defined engine singletons.
2 parents bdf89c4 + b8ce131 commit d54797c

File tree

7 files changed

+278
-28
lines changed

7 files changed

+278
-28
lines changed

godot-core/src/obj/traits.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,74 @@ 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, a user singleton can be registered manually:
702+
///
703+
/// ```no_run
704+
/// # use godot::prelude::*;
705+
/// # use godot::classes::Engine;
706+
/// #[derive(GodotClass)]
707+
/// #[class(init, base = Object)]
708+
/// struct MyEngineSingleton {}
709+
///
710+
/// // Provides blanket implementation allowing to use MyEngineSingleton::singleton().
711+
/// // Ensures that `MyEngineSingleton` is a valid singleton (i.e., a non-refcounted GodotClass).
712+
/// impl UserSingleton for MyEngineSingleton {}
713+
///
714+
/// struct MyExtension;
715+
///
716+
/// #[gdextension]
717+
/// unsafe impl ExtensionLibrary for MyExtension {
718+
/// fn on_stage_init(stage: InitStage) {
719+
/// if stage == InitStage::MainLoop {
720+
/// let obj = MyEngineSingleton::new_alloc();
721+
/// Engine::singleton()
722+
/// .register_singleton(&MyEngineSingleton::class_id().to_string_name(), &obj);
723+
/// }
724+
/// }
725+
///
726+
/// fn on_stage_deinit(stage: InitStage) {
727+
/// if stage == InitStage::MainLoop {
728+
/// let obj = MyEngineSingleton::singleton();
729+
/// Engine::singleton()
730+
/// .unregister_singleton(&MyEngineSingleton::class_id().to_string_name());
731+
/// obj.free();
732+
/// }
733+
/// }
734+
/// }
735+
/// ```
736+
// For now exists mostly as a marker trait and a way to provide blanket implementation for `Singleton` trait.
737+
pub trait UserSingleton:
738+
GodotClass + Bounds<Declarer = bounds::DeclUser, Memory = bounds::MemManual>
739+
{
740+
}
741+
742+
impl<T> Singleton for T
743+
where
744+
T: UserSingleton + Inherits<crate::classes::Object>,
745+
{
746+
fn singleton() -> Gd<T> {
747+
// Note: Under any safeguards level `singleton_unchecked` will panic if Singleton can't be retrieved.
748+
749+
let class_name = <T as GodotClass>::class_id().to_string_name();
750+
// SAFETY: The caller must ensure that `class_name` corresponds to the actual class name of type `T`.
751+
// This is always true for `#[class(singleton)]`.
752+
unsafe { crate::classes::singleton_unchecked(&class_name) }
753+
}
754+
}
755+
688756
impl<T> NewAlloc for T
689757
where
690758
T: cap::GodotDefault + Bounds<Memory = bounds::MemManual>,
@@ -705,6 +773,7 @@ pub mod cap {
705773
use super::*;
706774
use crate::builtin::{StringName, Variant};
707775
use crate::meta::PropertyInfo;
776+
use crate::obj::{Base, Bounds, Gd};
708777
use crate::storage::{IntoVirtualMethodReceiver, VirtualMethodReceiver};
709778

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

godot-core/src/registry/class.rs

Lines changed: 36 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,18 @@ 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 added to the editor immediately, triggering their lifecycle methods –- even before their
224+
// dependencies (e.g. properties) have been registered.
225+
// During hot-reload, Godot erases all GDExtension instance bindings (the "rust part"), effectively changing them to the base classes.
226+
// These two behaviors combined were leading to crashes.
227+
//
228+
// 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).
229+
// See also: https://github.com/godot-rust/gdext/issues/1132.
220230
let mut editor_plugins: Vec<ClassId> = Vec::new();
221231

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

244+
if let Some(register_singleton_fn) = info.register_singleton_fn {
245+
singletons.push(register_singleton_fn)
246+
}
247+
231248
register_class_raw(info);
232249

233250
out!("Class {class_name} loaded.");
234251
}
235252

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)
253+
for register_singleton_fn in singletons {
254+
register_singleton_fn()
255+
}
256+
240257
for editor_plugin_class_name in editor_plugins {
241258
unsafe { interface_fn!(editor_add_plugin)(editor_plugin_class_name.string_sys()) };
242259
}
@@ -259,6 +276,7 @@ fn register_classes_and_dyn_traits(
259276
let loaded_class = LoadedClass {
260277
name: class_name,
261278
is_editor_plugin: info.is_editor_plugin,
279+
unregister_singleton_fn: info.unregister_singleton_fn,
262280
};
263281
let metadata = ClassMetadata {};
264282

@@ -420,6 +438,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
420438
register_properties_fn,
421439
free_fn,
422440
default_get_virtual_fn,
441+
unregister_singleton_fn,
442+
register_singleton_fn,
423443
is_tool,
424444
is_editor_plugin,
425445
is_internal,
@@ -431,6 +451,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
431451
c.default_virtual_fn = default_get_virtual_fn;
432452
c.register_properties_fn = Some(register_properties_fn);
433453
c.is_editor_plugin = is_editor_plugin;
454+
c.register_singleton_fn = register_singleton_fn;
455+
c.unregister_singleton_fn = unregister_singleton_fn;
434456

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

657+
// Similarly to EditorPlugin – given instance is being freed and will not be recreated
658+
// during hot reload (a new, independent one will be created instead).
659+
if let Some(unregister_singleton_fn) = class.unregister_singleton_fn {
660+
unregister_singleton_fn();
661+
}
662+
635663
#[allow(clippy::let_unit_value)]
636664
let _: () = unsafe {
637665
interface_fn!(classdb_unregister_extension_class)(
@@ -670,6 +698,8 @@ fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo {
670698
user_register_fn: None,
671699
default_virtual_fn: None,
672700
user_virtual_fn: None,
701+
register_singleton_fn: None,
702+
unregister_singleton_fn: None,
673703
godot_params: default_creation_info(),
674704
init_level: InitLevel::Scene,
675705
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)