godot-rust / gdext

Rust bindings for Godot 4
https://mastodon.gamedev.place/@GodotRust
Mozilla Public License 2.0
3.1k stars 197 forks source link

Builder API to register classes, functions, properties, signals #4

Open Bromeon opened 2 years ago

Bromeon commented 2 years ago

Functionality that is currently available through proc-macros should ideally be exposed in a programmatic builder API, too.

Some challenges:

Bromeon commented 12 months ago

While this feature is still far away (definitely not this year), here are already some ideas of how proc-macro APIs could map to the builder API. The philosophy is to keep the mapping somewhat intuitive, reusing names where possible and avoid too much magic.

For example, given:

#[derive(GodotClass)]
#[class(base=Node2D)]
struct MyClass {
    #[base]
    base: Base<Node2D>,

    #[onready]
    late_init: OnReady<PackedInt32Array>,

    #[export]
    integer: i32,
}

#[godot_api]
impl MyClass {
    #[func]
    fn regular() -> Gd<Other> {...}

    #[func(gd_self)]
    fn do_sth(this: Gd<Self>, arg: i32) {...}
}

#[godot_api]
impl INode2D for MyClass {
    fn init(base: Base<Node2D>) -> Self {...}
    fn ready(&mut self); {...}
    fn to_string() -> GString {...}
}

With builders and zero proc-macros, it could look something like:

struct MyClass {
    base: Base<Node2D>,
    late_init: OnReady<PackedInt32Array>,
    integer: i32,
}

impl MyClass {
    fn regular() -> Gd<Other> {...}
    fn do_sth(this: Gd<Self>, arg: i32) {...}
}

// NEW: explicit GodotClass 
impl GodotClass for MyClass {
    // base, memory etc
}

impl INode2D for MyClass {
    // NEW:
    fn register(b: &mut ClassBuilder<MyClass>) {
        // Fields
        b.export_var("integer").get(Self::get_integer).set(Self::set_integer);
        b.base(Self::get_base); // returns &mut Base
        b.onready(Self::get_late_init); // returns &mut OnReady

        // Different kinds of methods, e.g. static/instance/Gd<Self>
        b.func("regular").method(Self::regular);
        b.func("do_sth").method_gd_self(Self::do_sth);

        // Special methods (we need to tell which ones are overridden, compiler can't detect it)
        b.interface() // requires INode2D bound
         .overrides(IMethod::Init | IMethod::Ready | IMethod::ToString);
    }

    fn init(base: Base<Node2D>) -> Self {...}
    fn ready(&mut self); {...}
    fn to_string() -> GString {...}
}

// in a global registration hook:
fn register_classes(b: &mut LibraryBuilder) {
    b.add_class::<MyClass>();
    ...
}
StatisMike commented 10 months ago

This would be great! Additional problems like the inability to register methods coming from different trait impl wouldn't be a problem then - they could be just manually added in register().

Bromeon commented 7 months ago

Just for completeness, I'd also like to mention fully dynamic builder APIs.

Meaning, class methods are registered based on Callable and there would be no traits to fulfill. In essence, the whole builder API would be type-erased and lower-level, closely resembling the raw C APIs. Use case might be an editor plugin that doesn't expect each class to correspond to a Rust struct, but rather allows dynamic creation of different types and nodes -- via UI, config file or network. In other words, this would allow to add new classes and methods without writing new Rust code.

I don't think we should support this, at least not at the moment. But maybe we should also establish use cases for a typed builder API more clearly, to differentiate it better from features that the proc-macro API already provides.

mivort commented 1 month ago

I'm copying this message from Discord to provide some feedback on usage of builder API.


I'm currently using Godot 3/gdnative and rely heavily on the usage of traits to provide common interfaces to various game entities in type-safe manner. With current procedural macros approach, I would need to expose each interface function manually and hope not to make any mistakes in naming/signature (there won't be any compile-time warnings, and in some cases there won't be even a runtime one, as often I'm using it for duck-typing - if object has some method, it reacts to some action, otherwise it just ignores it). Procedural macro for exposing traits may help with this, but it seems to be more difficult to implement, with many caveats. Godot itself lacks any kind of proper interfaces (only quite limited inheritance), so it may introduce its own kind of traits/mixin system at some point, which would be nice, but at the moment of writing builder API paired with Rust traits provides a reliable replacement for that.

The gdnative builder API also helps to reduce a big chunk of boilerplate code in many cases (grouping of linked fields, things like inventory slots, stats with modifiers etc.), allowing to fully leverage the inline macro system and giving a big advantage for using Rust over GDScript/C#.


Both typed and untyped registration systems would perfectly work for the case above, as interface definition needs to be provided once and each class which would register this interface will have consistent signatures, properties etc.