Skip to content

Commit 20baefb

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

File tree

7 files changed

+196
-21
lines changed

7 files changed

+196
-21
lines changed

godot-core/src/obj/traits.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,38 @@ 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 safeguards enabled `singleton_unchecked` will panic if Singleton can't be retrieved.
703+
#[cfg(not(safeguards_strict))]
704+
{
705+
// SAFETY: The caller must ensure that `class_name` corresponds to the actual class name of type `T`.
706+
// This is always true for proc-macro derived user singletons.
707+
unsafe {
708+
crate::classes::singleton_unchecked(&<T as GodotClass>::class_id().to_string_name())
709+
}
710+
}
711+
#[cfg(safeguards_strict)]
712+
{
713+
crate::classes::Engine::singleton()
714+
.get_singleton(&<T as GodotClass>::class_id().to_string_name())
715+
.unwrap_or_else(|| panic!("Singleton {} not found. User singleton must be registered under its class name.", T::class_id())).cast()
716+
}
717+
}
718+
}
719+
688720
impl<T> NewAlloc for T
689721
where
690722
T: cap::GodotDefault + Bounds<Memory = bounds::MemManual>,
@@ -705,6 +737,7 @@ pub mod cap {
705737
use super::*;
706738
use crate::builtin::{StringName, Variant};
707739
use crate::meta::PropertyInfo;
740+
use crate::obj::{Base, Bounds, Gd};
708741
use crate::storage::{IntoVirtualMethodReceiver, VirtualMethodReceiver};
709742

710743
/// Trait for all classes that are default-constructible from the Godot engine.
@@ -740,7 +773,7 @@ pub mod cap {
740773
// 1. Separate trait `GodotUserDefault` for user classes, which then proliferates through all APIs and makes abstraction harder.
741774
// 2. Repeatedly implementing __godot_default() that forwards to something like Gd::default_user_instance(). Possible, but this
742775
// will make the step toward builder APIs more difficult, as users would need to re-implement this as well.
743-
sys::strict_assert_eq!(
776+
debug_assert_eq!(
744777
std::any::TypeId::of::<<Self as Bounds>::Declarer>(),
745778
std::any::TypeId::of::<bounds::DeclUser>(),
746779
"__godot_default() called on engine class; must be overridden for engine classes"

godot-core/src/registry/class.rs

Lines changed: 33 additions & 2 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,15 @@ 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

223+
// Workaround for Godot before 4.4.1:
218224
// Editor plugins should be added to the editor AFTER all the classes has been registered.
219225
// Adding EditorPlugin to the Editor before registering all the classes it depends on might result in crash.
220226
let mut editor_plugins: Vec<ClassId> = Vec::new();
221227

228+
// User Singletons must be initialized only after all the other classes
229+
// (on which they might depend on in form of properties and whatnot).
230+
let mut singletons: Vec<fn()> = Vec::new();
231+
222232
// Actually register all the classes.
223233
for info in map.into_values() {
224234
#[cfg(feature = "debug-log")]
@@ -228,14 +238,22 @@ pub fn auto_register_classes(init_level: InitLevel) {
228238
editor_plugins.push(info.class_name);
229239
}
230240

241+
if let Some(register_singleton_fn) = info.register_singleton_fn {
242+
singletons.push(register_singleton_fn)
243+
}
244+
231245
register_class_raw(info);
232246

233247
out!("Class {class_name} loaded.");
234248
}
235249

236-
// Will imminently add given class to the editor.
250+
for register_singleton_fn in singletons {
251+
register_singleton_fn()
252+
}
253+
254+
// Will imminently add given class to the editor in Godot before 4.4.1.
237255
// 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.
256+
// but might lead to some issues during hot reload.
239257
// See also: (https://github.com/godot-rust/gdext/issues/1132)
240258
for editor_plugin_class_name in editor_plugins {
241259
unsafe { interface_fn!(editor_add_plugin)(editor_plugin_class_name.string_sys()) };
@@ -259,6 +277,7 @@ fn register_classes_and_dyn_traits(
259277
let loaded_class = LoadedClass {
260278
name: class_name,
261279
is_editor_plugin: info.is_editor_plugin,
280+
unregister_singleton_fn: info.unregister_singleton_fn,
262281
};
263282
let metadata = ClassMetadata {};
264283

@@ -420,6 +439,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
420439
register_properties_fn,
421440
free_fn,
422441
default_get_virtual_fn,
442+
unregister_singleton_fn,
443+
register_singleton_fn,
423444
is_tool,
424445
is_editor_plugin,
425446
is_internal,
@@ -431,6 +452,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) {
431452
c.default_virtual_fn = default_get_virtual_fn;
432453
c.register_properties_fn = Some(register_properties_fn);
433454
c.is_editor_plugin = is_editor_plugin;
455+
c.register_singleton_fn = register_singleton_fn;
456+
c.unregister_singleton_fn = unregister_singleton_fn;
434457

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

658+
// Similarly to EditorPlugin – given instance is being freed and will not be recreated
659+
// during hot reload (a new, independent one will be created instead).
660+
if let Some(unregister_singleton_fn) = class.unregister_singleton_fn {
661+
unregister_singleton_fn();
662+
}
663+
635664
#[allow(clippy::let_unit_value)]
636665
let _: () = unsafe {
637666
interface_fn!(classdb_unregister_extension_class)(
@@ -670,6 +699,8 @@ fn default_registration_info(class_name: ClassId) -> ClassRegistrationInfo {
670699
user_register_fn: None,
671700
default_virtual_fn: None,
672701
user_virtual_fn: None,
702+
register_singleton_fn: None,
703+
unregister_singleton_fn: None,
673704
godot_params: default_creation_info(),
674705
init_level: InitLevel::Scene,
675706
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

godot-macros/src/class/derive_godot_class.rs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,13 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
7878

7979
let godot_exports_impl = make_property_impl(class_name, &fields);
8080

81-
let godot_withbase_impl = if let Some(Field { name, ty, .. }) = &fields.base_field {
82-
// Apply the span of the field's type so that errors show up on the field's type.
83-
quote_spanned! { ty.span()=>
84-
impl ::godot::obj::WithBaseField for #class_name {
85-
fn to_gd(&self) -> ::godot::obj::Gd<#class_name> {
86-
// By not referencing the base field directly here we ensure that the user only gets one error when the base
87-
// field's type is wrong.
88-
let base = <#class_name as ::godot::obj::WithBaseField>::base_field(self);
89-
90-
base.__constructed_gd().cast()
91-
}
81+
let godot_withbase_impl = make_with_base_impl(&fields.base_field, class_name);
9282

93-
fn base_field(&self) -> &::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base> {
94-
&self.#name
95-
}
96-
}
97-
}
83+
let (user_singleton_impl, init_level_impl) = if struct_cfg.is_singleton {
84+
modifiers.push(quote! { with_singleton::<#class_name> });
85+
make_singleton_impl(class_name)
9886
} else {
99-
TokenStream::new()
87+
(TokenStream::new(), TokenStream::new())
10088
};
10189

10290
let (user_class_impl, has_default_virtual) = make_user_class_impl(
@@ -164,6 +152,8 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
164152
impl ::godot::obj::GodotClass for #class_name {
165153
type Base = #base_class;
166154

155+
#init_level_impl
156+
167157
// Code duplicated in godot-codegen.
168158
fn class_id() -> ::godot::meta::ClassId {
169159
use ::godot::meta::ClassId;
@@ -194,6 +184,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult<TokenStream> {
194184
#deny_manual_init_macro
195185
#( #deprecations )*
196186
#( #errors )*
187+
#user_singleton_impl
197188

198189
#struct_docs_registration
199190
::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<TokenStream> {
206197
})
207198
}
208199

200+
fn make_with_base_impl(base_field: &Option<Field>, class_name: &Ident) -> TokenStream {
201+
if let Some(Field { name, ty, .. }) = base_field {
202+
// Apply the span of the field's type so that errors show up on the field's type.
203+
quote_spanned! { ty.span()=>
204+
impl ::godot::obj::WithBaseField for #class_name {
205+
fn to_gd(&self) -> ::godot::obj::Gd<#class_name> {
206+
// By not referencing the base field directly here we ensure that the user only gets one error when the base
207+
// field's type is wrong.
208+
let base = <#class_name as ::godot::obj::WithBaseField>::base_field(self);
209+
210+
base.__constructed_gd().cast()
211+
}
212+
213+
fn base_field(&self) -> &::godot::obj::Base<<#class_name as ::godot::obj::GodotClass>::Base> {
214+
&self.#name
215+
}
216+
}
217+
}
218+
} else {
219+
TokenStream::new()
220+
}
221+
}
222+
223+
/// Generates registration for user singleton and proper INIT_LEVEL declaration.
224+
///
225+
/// Before Godot4.4 built-in Engine singleton – required for registration – wasn't available before `InitLevel::Scene`.
226+
fn make_singleton_impl(class_name: &Ident) -> (TokenStream, TokenStream) {
227+
(
228+
quote! {
229+
impl ::godot::obj::UserSingleton for #class_name {}
230+
},
231+
quote! {
232+
const INIT_LEVEL: ::godot::init::InitLevel = ::godot::init::InitLevel::Scene;
233+
},
234+
)
235+
}
236+
209237
/// Generates code for a decl-macro, which takes any item and prepends it with the visibility marker of the class.
210238
///
211239
/// Used to access the visibility of the class in other proc-macros like `#[godot_api]`.
@@ -301,6 +329,7 @@ struct ClassAttributes {
301329
base_ty: Ident,
302330
init_strategy: InitStrategy,
303331
is_tool: bool,
332+
is_singleton: bool,
304333
is_internal: bool,
305334
rename: Option<Ident>,
306335
deprecations: Vec<TokenStream>,
@@ -508,6 +537,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
508537
let mut base_ty = ident("RefCounted");
509538
let mut init_strategy = InitStrategy::UserDefined;
510539
let mut is_tool = false;
540+
let mut is_singleton = false;
511541
let mut is_internal = false;
512542
let mut rename: Option<Ident> = None;
513543
let mut deprecations = vec![];
@@ -531,6 +561,11 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
531561
is_tool = true;
532562
}
533563

564+
// #[class(singleton)]
565+
if parser.handle_alone("singleton")? {
566+
is_singleton = true;
567+
}
568+
534569
// Removed #[class(editor_plugin)]
535570
if let Some(key) = parser.handle_alone_with_span("editor_plugin")? {
536571
return bail!(
@@ -581,6 +616,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult<ClassAttribute
581616
base_ty,
582617
init_strategy,
583618
is_tool,
619+
is_singleton,
584620
is_internal,
585621
rename,
586622
deprecations,

godot-macros/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,26 @@ use crate::util::{bail, ident, KvParser};
481481
/// 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
482482
/// because we have added a `hidden` key to the class. This will also prevent it from showing up in documentation.
483483
///
484+
/// ## User Engine Singletons
485+
///
486+
/// Non-refcounted classes can be registered as an engine singleton with `#[class(singleton)]`.
487+
///
488+
/// ```no_run
489+
/// # use godot::prelude::*;
490+
/// #[derive(GodotClass)]
491+
/// #[class(init, singleton, base=Object)]
492+
/// struct MySingleton {
493+
/// my_field: i32,
494+
/// }
495+
///
496+
/// // Can be accessed as any other engine singleton.
497+
/// let val = MySingleton::singleton().bind().my_field;
498+
/// ```
499+
///
500+
/// Engine Singletons always run in the editor (similarly to `#[class(tool)]`) and will never be represented by a placeholder instances.
501+
///
502+
/// GDScript will be prohibited from creating new instances of said class.
503+
///
484504
/// # Further field customization
485505
///
486506
/// ## Fine-grained inference hints
@@ -554,6 +574,7 @@ use crate::util::{bail, ident, KvParser};
554574
alias = "base",
555575
alias = "init",
556576
alias = "no_init",
577+
alias = "singleton",
557578
alias = "var",
558579
alias = "export",
559580
alias = "tool",

0 commit comments

Comments
 (0)