From ca73bb75d81e3e7270c279689bd91d542afde081 Mon Sep 17 00:00:00 2001 From: Yarvin Date: Wed, 5 Nov 2025 19:56:45 +0100 Subject: [PATCH] Add user singletons. Allow to register user-defined engine singletons via #[class(singleton)]`. --- godot-core/src/obj/traits.rs | 70 +++++++++++++ godot-core/src/registry/class.rs | 41 ++++++-- godot-core/src/registry/plugin.rs | 35 ++++++- godot-macros/src/class/derive_godot_class.rs | 97 +++++++++++++++---- godot-macros/src/lib.rs | 41 +++++++- godot/src/prelude.rs | 2 +- itest/rust/src/object_tests/singleton_test.rs | 21 ++++ 7 files changed, 279 insertions(+), 28 deletions(-) diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 9d522259c..7168d9268 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -685,6 +685,75 @@ pub trait Singleton: GodotClass { fn singleton() -> Gd; } +/// Trait for user-defined singleton classes in Godot. +/// +/// Implementing this trait allows accessing a registered singleton instance through [`singleton()`][Singleton::singleton]. +/// User singletons should be registered under their class name – otherwise some Godot components (for example GDScript before 4.4) might have trouble handling them, +/// and the editor might crash when using `T::singleton()`. +/// +/// There should be only one instance of a given singleton class in the engine, valid as long as the library is loaded. +/// Therefore, user singletons are limited to classes with manual memory management (ones not inheriting from `RefCounted`). +/// +/// # Registration +/// +/// Godot-rust provides a way to register given class as an Engine Singleton with [`#[class(singleton)]`](../prelude/derive.GodotClass.html#user-engine-singletons). +/// +/// Alternatively, user singleton can be registered manually: +/// +/// ```no_run +/// # use godot::prelude::*; +/// # use godot::classes::Engine; +/// #[derive(GodotClass)] +/// #[class(init, base = Object)] +/// struct MyEngineSingleton {} +/// +/// // Provides blanket implementation allowing to use MyEngineSingleton::singleton(). +/// // Ensures that `MyEngineSingleton` is a valid singleton (i.e., a non-refcounted GodotClass). +/// impl UserSingleton for MyEngineSingleton {} +/// +/// struct MyExtension; +/// +/// #[gdextension] +/// unsafe impl ExtensionLibrary for MyExtension { +/// fn on_stage_init(stage: InitStage) { +/// if matches!(stage, InitStage::MainLoop) { +/// let obj = MyEngineSingleton::new_alloc(); +/// Engine::singleton() +/// .register_singleton(&MyEngineSingleton::class_id().to_string_name(), &obj); +/// } +/// } +/// +/// fn on_stage_deinit(stage: InitStage) { +/// if matches!(stage, InitStage::MainLoop) { +/// let obj = MyEngineSingleton::singleton(); +/// Engine::singleton() +/// .unregister_singleton(&MyEngineSingleton::class_id().to_string_name()); +/// obj.free(); +/// } +/// } +/// } +/// ``` +// For now exists mostly as a marker trait and a way to provide blanket implementation for `Singleton` trait. +pub trait UserSingleton: + GodotClass + Bounds +{ +} + +impl Singleton for T +where + T: UserSingleton + Inherits, +{ + fn singleton() -> Gd { + // Note: with any safeguards enabled `singleton_unchecked` will panic if Singleton can't be retrieved. + + // SAFETY: The caller must ensure that `class_name` corresponds to the actual class name of type `T`. + // This is always true for `#[class(singleton)]`. + unsafe { + crate::classes::singleton_unchecked(&::class_id().to_string_name()) + } + } +} + impl NewAlloc for T where T: cap::GodotDefault + Bounds, @@ -705,6 +774,7 @@ pub mod cap { use super::*; use crate::builtin::{StringName, Variant}; use crate::meta::PropertyInfo; + use crate::obj::{Base, Bounds, Gd}; use crate::storage::{IntoVirtualMethodReceiver, VirtualMethodReceiver}; /// Trait for all classes that are default-constructible from the Godot engine. diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 1715313ec..12562a74b 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -61,6 +61,7 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap, } /// Represents a class which is currently loaded and retained in memory -- including metadata. @@ -93,6 +94,8 @@ struct ClassRegistrationInfo { user_register_fn: Option, default_virtual_fn: Option, // Optional (set if there is at least one OnReady field) user_virtual_fn: Option, // Optional (set if there is a `#[godot_api] impl I*`) + register_singleton_fn: Option, + unregister_singleton_fn: Option, /// Godot low-level class creation parameters. godot_params: GodotCreationInfo, @@ -180,6 +183,8 @@ pub(crate) fn register_class< is_editor_plugin: false, dynify_fns_by_trait: HashMap::new(), component_already_filled: Default::default(), // [false; N] + register_singleton_fn: None, + unregister_singleton_fn: None, }); } @@ -215,10 +220,17 @@ pub fn auto_register_classes(init_level: InitLevel) { // but it is much slower and doesn't guarantee that all the dependent classes will be already loaded in most cases. register_classes_and_dyn_traits(&mut map, init_level); - // Editor plugins should be added to the editor AFTER all the classes has been registered. - // Adding EditorPlugin to the Editor before registering all the classes it depends on might result in crash. + // Before Godot 4.4.1 editor plugins were being added to the editor immediately, + // triggering their lifecycle methods – even before their dependencies (properties and whatnot) has been registered. + // During hot-reload Godot changes all GDExtension class instances into their base classes. + // These two behaviors combined were leading to crashes. + // 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). + // See also: (https://github.com/godot-rust/gdext/issues/1132) let mut editor_plugins: Vec = Vec::new(); + // Similarly to EnginePlugins – freshly instantiated engine singleton might depend on some not-yet-registered classes. + let mut singletons: Vec = Vec::new(); + // Actually register all the classes. for info in map.into_values() { #[cfg(feature = "debug-log")] @@ -228,15 +240,19 @@ pub fn auto_register_classes(init_level: InitLevel) { editor_plugins.push(info.class_name); } + if let Some(register_singleton_fn) = info.register_singleton_fn { + singletons.push(register_singleton_fn) + } + register_class_raw(info); out!("Class {class_name} loaded."); } - // Will imminently add given class to the editor. - // It is expected and beneficial behaviour while we load library for the first time - // but (for now) might lead to some issues during hot reload. - // See also: (https://github.com/godot-rust/gdext/issues/1132) + for register_singleton_fn in singletons { + register_singleton_fn() + } + for editor_plugin_class_name in editor_plugins { unsafe { interface_fn!(editor_add_plugin)(editor_plugin_class_name.string_sys()) }; } @@ -259,6 +275,7 @@ fn register_classes_and_dyn_traits( let loaded_class = LoadedClass { name: class_name, is_editor_plugin: info.is_editor_plugin, + unregister_singleton_fn: info.unregister_singleton_fn, }; let metadata = ClassMetadata {}; @@ -420,6 +437,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { register_properties_fn, free_fn, default_get_virtual_fn, + unregister_singleton_fn, + register_singleton_fn, is_tool, is_editor_plugin, is_internal, @@ -431,6 +450,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { c.default_virtual_fn = default_get_virtual_fn; c.register_properties_fn = Some(register_properties_fn); c.is_editor_plugin = is_editor_plugin; + c.register_singleton_fn = register_singleton_fn; + c.unregister_singleton_fn = unregister_singleton_fn; // Classes marked #[class(no_init)] are translated to "abstract" in Godot. This disables their default constructor. // "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) { out!("> Editor plugin removed"); } + // Similarly to EditorPlugin – given instance is being freed and will not be recreated + // during hot reload (a new, independent one will be created instead). + if let Some(unregister_singleton_fn) = class.unregister_singleton_fn { + unregister_singleton_fn(); + } + #[allow(clippy::let_unit_value)] let _: () = unsafe { interface_fn!(classdb_unregister_extension_class)( @@ -670,6 +697,8 @@ fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo { user_register_fn: None, default_virtual_fn: None, user_virtual_fn: None, + register_singleton_fn: None, + unregister_singleton_fn: None, godot_params: default_creation_info(), init_level: InitLevel::Scene, is_editor_plugin: false, diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index f03da5fae..93aa15de5 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -10,7 +10,10 @@ use std::{any, fmt}; use crate::init::InitLevel; use crate::meta::ClassId; -use crate::obj::{bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, UserClass}; +use crate::obj::{ + bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, NewAlloc, Singleton, UserClass, + UserSingleton, +}; use crate::registry::callbacks; use crate::registry::class::GodotGetVirtual; use crate::{classes, sys}; @@ -180,6 +183,12 @@ pub struct Struct { instance: sys::GDExtensionClassInstancePtr, ), + /// `#[class(singleton)]` + pub(crate) register_singleton_fn: Option, + + /// `#[class(singleton)]` + pub(crate) unregister_singleton_fn: Option, + /// Calls `__before_ready()`, if there is at least one `OnReady` field. Used if there is no `#[godot_api] impl` block /// overriding ready. pub(crate) default_get_virtual_fn: Option, @@ -209,6 +218,8 @@ impl Struct { raw: callbacks::register_user_properties::, }, free_fn: callbacks::free::, + register_singleton_fn: None, + unregister_singleton_fn: None, default_get_virtual_fn: None, is_tool: false, is_editor_plugin: false, @@ -257,6 +268,28 @@ impl Struct { self } + pub fn with_singleton(mut self) -> Self + where + T: UserSingleton + + Bounds + + NewAlloc + + Inherits, + { + self.register_singleton_fn = Some(|| { + crate::classes::Engine::singleton() + .register_singleton(&T::class_id().to_string_name(), &T::new_alloc()); + }); + + self.unregister_singleton_fn = Some(|| { + let singleton = T::singleton(); + crate::classes::Engine::singleton() + .unregister_singleton(&T::class_id().to_string_name()); + singleton.free(); + }); + + self + } + pub fn with_internal(mut self) -> Self { self.is_internal = true; self diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index e2e0db80c..3af6273c0 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -78,25 +78,13 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let godot_exports_impl = make_property_impl(class_name, &fields); - let godot_withbase_impl = if let Some(Field { name, ty, .. }) = &fields.base_field { - // Apply the span of the field's type so that errors show up on the field's type. - quote_spanned! { ty.span()=> - impl ::godot::obj::WithBaseField for #class_name { - fn to_gd(&self) -> ::godot::obj::Gd<#class_name> { - // By not referencing the base field directly here we ensure that the user only gets one error when the base - // field's type is wrong. - let base = <#class_name as ::godot::obj::WithBaseField>::base_field(self); - - base.__constructed_gd().cast() - } + let godot_withbase_impl = make_with_base_impl(&fields.base_field, class_name); - fn base_field(&self) -> &::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base> { - &self.#name - } - } - } + let (user_singleton_impl, singleton_init_level_const) = if struct_cfg.is_singleton { + modifiers.push(quote! { with_singleton::<#class_name> }); + make_singleton_impl(class_name) } else { - TokenStream::new() + (TokenStream::new(), TokenStream::new()) }; let (user_class_impl, has_default_virtual) = make_user_class_impl( @@ -164,6 +152,8 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { impl ::godot::obj::GodotClass for #class_name { type Base = #base_class; + #singleton_init_level_const + // Code duplicated in godot-codegen. fn class_id() -> ::godot::meta::ClassId { use ::godot::meta::ClassId; @@ -194,6 +184,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #deny_manual_init_macro #( #deprecations )* #( #errors )* + #user_singleton_impl #struct_docs_registration ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( @@ -206,6 +197,43 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { }) } +fn make_with_base_impl(base_field: &Option, class_name: &Ident) -> TokenStream { + if let Some(Field { name, ty, .. }) = base_field { + // Apply the span of the field's type so that errors show up on the field. + quote_spanned! { ty.span()=> + impl ::godot::obj::WithBaseField for #class_name { + fn to_gd(&self) -> ::godot::obj::Gd<#class_name> { + // By not referencing the base field directly here we ensure that the user only gets one error when the base + // field's type is wrong. + let base = <#class_name as ::godot::obj::WithBaseField>::base_field(self); + + base.__constructed_gd().cast() + } + + fn base_field(&self) -> &::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base> { + &self.#name + } + } + } + } else { + TokenStream::new() + } +} + +/// Generates registration for user singleton and proper INIT_LEVEL declaration. +/// +/// Before Godot4.4, built-in engine singleton -- required for registration -- wasn't available before `InitLevel::Scene`. +fn make_singleton_impl(class_name: &Ident) -> (TokenStream, TokenStream) { + ( + quote! { + impl ::godot::obj::UserSingleton for #class_name {} + }, + quote! { + const INIT_LEVEL: ::godot::init::InitLevel = ::godot::init::InitLevel::Scene; + }, + ) +} + /// Generates code for a decl-macro, which takes any item and prepends it with the visibility marker of the class. /// /// Used to access the visibility of the class in other proc-macros like `#[godot_api]`. @@ -301,6 +329,7 @@ struct ClassAttributes { base_ty: Ident, init_strategy: InitStrategy, is_tool: bool, + is_singleton: bool, is_internal: bool, rename: Option, deprecations: Vec, @@ -505,9 +534,10 @@ fn make_user_class_impl( /// Returns the name of the base and the default mode fn parse_struct_attributes(class: &venial::Struct) -> ParseResult { - let mut base_ty = ident("RefCounted"); + let mut base_ty = None; let mut init_strategy = InitStrategy::UserDefined; let mut is_tool = false; + let mut is_singleton = false; let mut is_internal = false; let mut rename: Option = None; let mut deprecations = vec![]; @@ -516,7 +546,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult ParseResult ParseResult ParseResult) { } } +/// Returns `Base` set by the user or default. +/// +/// Default base is `Object` for `#[class(singleton)]`, `RefCounted` otherwise. +fn base_field_or_default(mut base: Option, is_singleton: bool) -> Ident { + if let Some(base) = base.take() { + base + } else if is_singleton { + ident("Object") + } else { + ident("RefCounted") + } +} + fn validate_phantomvar_field(field: &Field, errors: &mut Vec) { let Some(field_var) = &field.var else { errors.push(error!( diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 0c591e06c..85c674574 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -100,7 +100,7 @@ use crate::util::{bail, ident, KvParser}; /// Unlike C++, Rust doesn't really have inheritance, but the GDExtension API lets us "inherit" /// from a Godot-provided engine class. /// -/// By default, classes created with this library inherit from `RefCounted`, like GDScript. +/// By default, non-singleton classes created with this library inherit from `RefCounted`, like GDScript. /// /// To specify a different class to inherit from, add `#[class(base = Base)]` as an annotation on /// your `struct`: @@ -484,6 +484,44 @@ use crate::util::{bail, ident, KvParser}; /// Even though this class is a `Node` and it has an init function, it still won't show up in the editor as a node you can add to a scene /// because we have added a `hidden` key to the class. This will also prevent it from showing up in documentation. /// +/// ## User Engine Singletons +/// +/// Non-refcounted classes can be registered as an engine singleton with `#[class(singleton)]`. +/// +/// ```no_run +/// # use godot::prelude::*; +/// # use godot::classes::Object; +/// #[derive(GodotClass)] +/// #[class(init, singleton)] +/// struct MySingleton { +/// my_field: i32, +/// // Object is default base for `#[class(singleton)]` instead of RefCounted. +/// base: Base, +/// } +/// +/// // Can be accessed like any other engine singleton from the main thread. +/// let val = MySingleton::singleton().bind().my_field; +/// ``` +/// +/// By default, engine singletons inherit from `Object`, always run in the editor (`#[class(tool)]`) +/// and will never be represented by a placeholder instances. +/// +/// During hot reload, user-defined engine singletons will be freed while unloading the library, and then freshly instantiated after class registration. +/// +/// GDScript will be prohibited from creating new instances of said class. +/// +/// User engine singletons must have an init: +/// +/// ```compile_fail +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(no_init, singleton)] +/// struct MySingleton { +/// my_field: i32, +/// } +/// ``` +/// +/// /// # Further field customization /// /// ## Fine-grained inference hints @@ -557,6 +595,7 @@ use crate::util::{bail, ident, KvParser}; alias = "base", alias = "init", alias = "no_init", + alias = "singleton", alias = "var", alias = "export", alias = "tool", diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 1188b155b..3a5ee2f38 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -18,7 +18,7 @@ pub use super::meta::error::{ConvertError, IoError}; pub use super::meta::{FromGodot, GodotConvert, ToGodot}; pub use super::obj::{ AsDyn, Base, DynGd, DynGdMut, DynGdRef, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId, - OnEditor, OnReady, + OnEditor, OnReady, UserSingleton, }; pub use super::register::property::{Export, PhantomVar, Var}; // Re-export macros. diff --git a/itest/rust/src/object_tests/singleton_test.rs b/itest/rust/src/object_tests/singleton_test.rs index 950c5ce77..ca2030091 100644 --- a/itest/rust/src/object_tests/singleton_test.rs +++ b/itest/rust/src/object_tests/singleton_test.rs @@ -8,6 +8,7 @@ use godot::builtin::GString; use godot::classes::{Input, Os}; use godot::obj::{Gd, Singleton}; +use godot::register::{godot_api, GodotClass}; use crate::framework::itest; @@ -44,3 +45,23 @@ fn singleton_is_operational() { let read_value = os.get_environment(&key); assert_eq!(read_value, value); } + +#[itest] +fn user_singleton() { + // Must be registered with the library and accessible at this point. + let value = SomeUserSingleton::singleton().bind().some_method(); + assert_eq!(value, 42); +} + +#[derive(GodotClass)] +// `#[class(tool, base = Object)]` is implied by `#[class(singleton)]`. +#[class(init, singleton)] +struct SomeUserSingleton {} + +#[godot_api] +impl SomeUserSingleton { + #[func] + fn some_method(&self) -> u32 { + 42 + } +}