godot-rust / gdext

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

Multiple `#[godot_api] impl` blocks #925

Open 0x53A opened 4 days ago

0x53A commented 4 days ago

At the moment only one #[godot_api] impl block is allowed (and second one to implement the base class interface).

The error message with multiple blocks is

conflicting implementations of trait `ImplementsGodotApi` for type `knight::Knight`
conflicting implementation for `knight::Knight`

Of course, the workaround is trivial, just put everything into one block, so this isn't important, but it would be really nice to have.

For code organization, I have been experimenting with a pattern where I define and implement a relative fine-grained trait in a node then create a godot function to wrap that trait.

Example what I'd like to do:


// pub struct Knight derives from Area2D

// --
// Hittable 
// --

impl Hittable for Knight {
    fn on_hit( /* ... */) {
        // ...
    }
}

#[godot_api]
impl Knight  {
    #[func]
    pub fn get_hittable_trait(&mut self) -> Gd<HittableWrapper> {
        let wrapped = HittableWrapper { other: Box::new(self.to_gd()) };
        return Gd::from_object(wrapped);
    }
}

// --
// Healthy
// --
impl Healthy for Knight {
    fn get_health() -> u32 {
        return 42;
    }
}

#[godot_api]
impl Knight  {
    #[func]
    pub fn get_healthy_trait(&mut self) -> Gd<HealthyWrapper> {
        let wrapped = HealthyWrapper{ other: Box::new(self.to_gd()) };
        return Gd::from_object(wrapped);
    }
}

I don't yet know if this system of code organization makes sense, but that's beside the point.

My arguments why I would like to have multiple #[godot_api] blocks:

1) I'd like to have the matching godot function right next to the trait implementation, instead of multiple pages below it, disjointed 2) Later I'd like to generate some of this wrapping code by macro, which would also be much easier if it could be split into different blocks 3) having multiple blocks would also allow to split the Node implementation into multiple files, if one were inclined to do so. (Personally I tend to prefer fewer larger files over many smaller ones, but taste differs)

Bromeon commented 4 days ago

#[godot_api] annotated impl blocks are very different from regular Rust impl blocks. They define several extra traits and register all contained symbols. You can check the generated code with cargo expand to get an idea :slightly_smiling_face:

Supporting multiple impl blocks is probably quite hard to implement with the current trait-based approach, and has the potential to make global registration more complex. The benefit however is "only" cosmetic (code organization) -- while important, I'm not sure if it stands in a good balance with implementation and maintenance effort.

Are you interested in investigating the feasibility? We could of course give you some guidance in the code.

mivort commented 21 hours ago

The benefit however is "only" cosmetic (code organization) -- while important, I'm not sure if it stands in a good balance with implementation and maintenance effort.

I'd like to add one practical use case which comes to mind - I think it makes significantly easier to add stuff to multiple Godot classes with declarative macros, as it makes possible to keep the main #[godot_api] block, and then add macro_rules! which will be able to expand independently into #[godot_api(secondary)] blocks. The biggest advantage would be if there's no limit for the amount of #[godot_api(secondary) blocks and no requirement to enumerate them.

This can help to simplify project-specific declarative macros which add traits to Godot classes and automatically expose trait methods to the engine, and apply such macros selectively. This case might be covered by builder API in future, though.