Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions godot-core/src/obj/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,30 @@ pub trait Singleton: GodotClass {
fn singleton() -> Gd<Self>;
}

/// Trait for user-defined singleton classes in Godot.
///
/// There should be only one instance of each singleton class in the engine, accessible through [`singleton()`][Singleton::singleton].
// For now exists mostly as a marker trait and a way to provide blanket implementation for `Singleton` trait.
pub trait UserSingleton:
GodotClass + Bounds<Declarer = bounds::DeclUser, Memory = bounds::MemManual>
{
}
Comment on lines +692 to +695
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why limit to MemManual?

If #522 is the reason, should we try to fix that at some point? 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, honestly I haven't been checking this case – RefCounted Engine Singletons just didn't make sense to me 🤔. Engine Singleton should be valid as long as library is loaded (as opposed to RefCounted which are valid as long as something keeps reference to them – and in this case engine should always keep a reference to it) and must be pruned in-between hot reloads.

I can look into supporting this case as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would Object still be automatically-managed (library destroys it on unload)? If yes, that would be OK to start with, and we may indeed not need to support RefCounted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would Object still be automatically-managed (library destroys it on unload)

yep, that's implemented


impl<T> Singleton for T
where
T: UserSingleton + Inherits<crate::classes::Object>,
{
fn singleton() -> Gd<T> {
// 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(&<T as GodotClass>::class_id().to_string_name())
}
}
}

impl<T> NewAlloc for T
where
T: cap::GodotDefault + Bounds<Memory = bounds::MemManual>,
Expand All @@ -705,6 +729,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.
Expand Down
41 changes: 35 additions & 6 deletions godot-core/src/registry/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ fn global_dyn_traits_by_typeid() -> GlobalGuard<'static, HashMap<any::TypeId, Ve
pub struct LoadedClass {
name: ClassId,
is_editor_plugin: bool,
unregister_singleton_fn: Option<fn()>,
}

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

/// Godot low-level class creation parameters.
godot_params: GodotCreationInfo,
Expand Down Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -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 imminently,
// 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 behaviour 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<ClassId> = Vec::new();

// Similarly to EnginePlugins – freshly instantiated engine singleton might depend on some not-yet-registered classes.
let mut singletons: Vec<fn()> = Vec::new();

// Actually register all the classes.
for info in map.into_values() {
#[cfg(feature = "debug-log")]
Expand All @@ -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()) };
}
Expand All @@ -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 {};

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)(
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 34 additions & 1 deletion godot-core/src/registry/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -180,6 +183,12 @@ pub struct Struct {
instance: sys::GDExtensionClassInstancePtr,
),

/// `#[class(singleton)]`
pub(crate) register_singleton_fn: Option<fn()>,

/// `#[class(singleton)]`
pub(crate) unregister_singleton_fn: Option<fn()>,

/// 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<GodotGetVirtual>,
Expand Down Expand Up @@ -209,6 +218,8 @@ impl Struct {
raw: callbacks::register_user_properties::<T>,
},
free_fn: callbacks::free::<T>,
register_singleton_fn: None,
unregister_singleton_fn: None,
default_get_virtual_fn: None,
is_tool: false,
is_editor_plugin: false,
Expand Down Expand Up @@ -257,6 +268,28 @@ impl Struct {
self
}

pub fn with_singleton<T>(mut self) -> Self
where
T: UserSingleton
+ Bounds<Memory = bounds::MemManual, Declarer = bounds::DeclUser>
+ NewAlloc
+ Inherits<classes::Object>,
{
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
Expand Down
Loading
Loading