gdext icon indicating copy to clipboard operation
gdext copied to clipboard

Provide easy way to register user singletons/autoloads

Open Yarwin opened this issue 1 year ago • 4 comments

Continuation of: https://github.com/godot-rust/book/pull/79#issuecomment-2689859445

Long story short, godot-rust could provide mechanism to simplify registering and using user-declared GDExtension singletons. Given mechanism must work both with editor/godot autloads and "pure" engine singletons; they must be retrievable via custom GameLoop as well

The user-facing API could look like that:

#[derive(GodotClass)]
#[class(init, base=Object)]
struct MyEditorSingleton {
    base: Base<Object>,
}

#[godot_api]
impl MyEditorSingleton {
    #[func]
    fn foo(&mut self) {
        godot_print!("foo");
    }
}

#[gd_singleton] // macro that creates&registers this object with the library. Works only for objects!
#[gd_singleton(init_level = ... )] // macro that creates&registers this object with the library on given non-default init level
impl UserSingleton for MyEditorSingleton {} // blanket implementation provides everything

or, for autoloads:

#[derive(GodotClass)]
#[class(init, base=Node)]
struct MyEditorSingleton {
    base: Base<Node>,
}

#[godot_api]
impl MyEditorSingleton {
    #[func]
    fn foo(&mut self) {
        godot_print!("foo");
    }
}

#[godot_api]
impl INode for MyEditorSingleton {
    fn enter_tree(&mut self) {
        self.register();
    }

    fn exit_tree(...) { 
    self.unregister();
    }
}
}

// adding #[gd_singleton] would result in compile error 
impl UserSingleton for MyEditorSingleton {} // blanket implementation provides everything

Afterwards one could use their singleton just like:


// Not very ergonomic, but magic auto-binding should be avoided 
MyEditorSingleton::singleton().bind_mut()…

UserSingleton trait could look like that:

UserSingleton trait
pub trait UserSingleton: GodotClass + Bounds<Declarer = bounds::DeclUser> + NewAlloc + Inherits<Object> + WithBaseField
{

    fn singleton() -> Gd<Self> {
        Engine::singleton()
            .get_singleton(&Self::class_name().to_string_name())
            .unwrap()
            .cast::<Self>()
    }

    fn register(&self) {
        Engine::singleton().register_singleton(&Self::class_name().to_string_name(), &self.to_gd());
    }

    fn unregister(&mut self) {
        Engine::singleton().unregister_singleton(&Self::class_name().to_string_name());
    }
}

Possible issues:

  • I had some crashes related to hot-reload while initializing singletons with the library in the past, testing is needed if this issue persist in G4.3 and 4.4

Namespace clashes – Godot 4.3 handles them nicely and even prevents creating new instances of given engine singleton. No further action is needed.

Image User Editor/Godot Autoload which is also registered as Engine Singleton will not be available via gdscript as an Engine Singleton (because it is not registered with the library itself, but later on during runtime) but only as an Autoload – thus avoiding the namespace clashes as well.

Additional notes:

Autoloads are global constants exposed to given script server as Variants and must exists in scene tree - in short, they must inherit nodes. They are persistent and not pruned in-between scene changes, unless user explicitly declare to do so. Rust GDExtension autoloads must be declared as scenes (scene with our node, duh) and then added as a godot autoload: https://docs.godotengine.org/en/latest/tutorials/scripting/singletons_autoload.html.

The Engine Singletons are almost the same thing – Godot keeps pointers to given singletons in hashmap<StringName, Object *>; engine singleton can be anything that inherits object (in godot context – just anything.).

In the past I did some digging and noted down implementation details & differences between Editor/Godot Autoload and Engine singleton: https://discord.com/channels/723850269347283004/1259547468379455601/1259664201022967828 (warning: might be a little incoherent)

Yarwin avatar Feb 28 '25 07:02 Yarwin

Thanks, this would definitely be great to add, as the current workflow is rather cumbersome!

API

Would UserSingleton even be needed as a public trait? It seems like registration/unregistration is probably the same. If not, it could be directly integrated into #[derive(GodotClass)], for example:

#[derive(GodotClass)]
#[class(init, base=Object, singleton)]
struct MyEditorSingleton {
    base: Base<Object>,
}
// With custom name:
#[derive(GodotClass)]
#[class(init, base=Object, singleton, rename = "OtherSingletonName")]
struct MyEditorSingleton {
    base: Base<Object>,
}

Hot-reload safety

We would need to make sure that loading/unloading works reliably in all cases. This can be tricky in case there are data dependencies, e.g. singleton A depends on singleton B. I don't know if we should maybe just outlaw this in an initial version, with a good error message when accessing singleton() before initialization, and then think about designs later.

Thread safety

Gd is not thread-safe. Eventually we should allow a cross-thread pointer (see #18), but until then, we might "support" it using the experimental-threads cop-out.

Bromeon avatar Feb 28 '25 11:02 Bromeon

I'd like this feature, or something like it. A couple of my projects function as reusable libraries, not games. I'm working on a Steam implementation. There is good reason have GodotRust Steamworks included in other people's GodotRust game code, but since Steam should probably be a singleton, that makes that challenging.

greenfox1505 avatar Mar 01 '25 19:03 greenfox1505

It would also be very usefull if gdext had a type-safe way to get singletons, registered from Rust, similar to how signals work currently. It could look something like this:

let my_editor_singleton = Engine::singleton().singletons().MyEditorSingleton(); // Maybe automatically rename the method to `snake_case`

Nikitf777 avatar Sep 22 '25 07:09 Nikitf777

It would also be very usefull if gdext had a type-safe way to get singletons, registered from Rust, similar to how signals work currently.

The big challenge here is that singletons (or classes in general) can be registered decentrally, in different files and modules. A proc-macro however can only pick up code of the item it applies to. This works for signals because they're all in the same #[godot_api] impl block, but it's also the reason why we don't currently support #[godot_api(secondary)] blocks with signals, or split across files, for that matter.

There might be solutions with a custom build step, e.g. running the initialization/registration logic of godot-rust and generating code based on that. This could also be useful for external APIs, see #372. However that's a whole different topic and would need quite a bit of upfront design 🤔

Bromeon avatar Sep 22 '25 07:09 Bromeon