godot-rust / gdnative

Rust bindings for Godot 3
https://godot-rust.github.io
MIT License
3.61k stars 210 forks source link

Use inventory or linkme to register scripts automatically #350

Closed ghost closed 1 year ago

ghost commented 4 years ago

While using the bindings, one might create a new script type but forget to register it in init. It's a minor annoyance, but one that often leads to confusing errors when the new type is used, and is not handled by the compiler. Implementing automatic script registration improves usability and reduces the chances of runtime bugs.

Provisions for automatic script registration seem to exist in the bindings for D, Kotlin, and Nim.

Currently, there are two crates in the Rust ecosystem that enable relatively easy automatic registration: inventory and linkme. The former is implemented through module initialization/teardown functions. The latter is based on link_section attributes. Both allow us to collect a list of fn(&mut InitHandle) -> () pointers before nativescript_init and use them to automatically register all script types that derived NativeClass. The expected user-facing API is the same regardless of the implementation detail:

// The derive macro will automatically insert the relevant code.
#[derive(NativeClass)]
struct Foo;

// A separate attribute can be used for types with manual `NativeClass` impls.
#[gdnative::register]
struct Bar;

impl NativeClass for Bar {
    /* - snip - */
}

// No callbacks necessary. It just works!
godot_gdnative_init!();
godot_nativescript_init!();
godot_gdnative_terminate!();

Manual registration should still be possible for types with manual NativeClass impls:

struct ILikeBoilerplateCode;

impl NativeClass for ILikeBoilerplateCode {
    /* - snip - */
}

fn init(handle: gdnative::init::InitHandle) {
    handle.add_class::<ILikeBoilerplateCode>();
}

godot_gdnative_init!();
godot_nativescript_init!(init);
godot_gdnative_terminate!();

Implementation options

As mentioned before, there are two crates that can be used. They have slightly different pros/cons from each other, that are given here. In any case, this will be an implementation detail, and we should be able to swap implementations freely at a later time.

Both crates need some support with platform compatibility before they can be used in godot-rust.

inventory

Pros:

Cons:

linkme

Pros:

Cons:

Considerations

Compatibility

It's possible to maintain compatibility with current manually registering code by making InitHandle track registered types, and ignore types that are already registered.

Tool scripts

A #[gdnative::tool] attribute may be added for tool scripts, alongside gdnative::register.

ghost commented 4 years ago

Added the "help wanted" tag for ctor / linkme platform support.

ghost commented 4 years ago

Update: iOS is now supported by inventory.

Bauxitedev commented 3 years ago

In my game that uses godot-rust I've tried using the inventory crate, and it seems to work pretty well. This is what I came up with so far...

For every NativeClass I only need this bit of code to register it... (which could easily be turned into a derive macro to reduce boilerplate)

inventory::submit! {
    AutoRegisterInfo::new("MyClass", 0, |handle| {
        handle.add_class::<MyClass>();
    })
}

Using a simple struct AutoRegisterInfo, that holds a closure for automatic class registration, and a priority for ordering purposes (the order of registrations seem to matter sometimes? I've had it crash on me before when the order was wrong)...


pub struct AutoRegisterInfo {
    pub name: String,  //Only used for debugging
    pub priority: i32, //Use this to put registrations in the right order (higher = first)
    pub register: fn(InitHandle),
}

impl AutoRegisterInfo {
    pub fn new(name: &str, priority: i32, register: fn(InitHandle)) -> Self {
        Self {
            name: name.to_owned(),
            priority,
            register,
        }
    }
}
inventory::collect!(AutoRegisterInfo);

Then, in lib.rs, I loop over all registrations and call them, ordered by priority...

godot_init!(init);

fn init(handle: InitHandle) {
    for info in inventory::iter::<AutoRegisterInfo>
        .into_iter()
        .sorted_by_key(|info| -info.priority)
    {
        println!("Registering class {}", info.name);
        (info.register)(handle);
    }

}

By the way, as a con for inventory, you mention "inventory currently uses ctor under the hood, which is life-before-main." I wonder, what does "life-before-main" mean exactly? And why is it a bad thing?

Bromeon commented 2 years ago

Both linkme and inventory have been archived, leaving room for speculation:

Update: unarchived, since the underlying rustc bug https://github.com/rust-lang/rust/pull/95604 was resolved.

Bogay commented 2 years ago

Hello, does this improvement also applies to godot_test! / godot_itest!? They also need to be declared and invoked manually.

Bromeon commented 2 years ago

I'm planning to do something like that for GDExtension. Not sure about GDNative -- the added value at this point is relatively little, as so many tests already exist, and this would burden the library with more dependencies.