godot-rust / gdext

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

Add GdDyn<dyn Trait> type #631

Open MatrixDev opened 6 months ago

MatrixDev commented 6 months ago

Request

Currently it is impossible to make GodotClass object-safe because of the multiple Rust limitations. Maybe it is possible to create a lighter alternative that doesn't require Self: Sized with limited functionality that allows to use trait-objects.

Use-case:

I have multiple different objects that have a common trait. At this moment I don't see any rust-safe way to call anything common except #[func] (which only takes variants and also a big footgun for safety).

Example

trait MyTrait {
    // cannot be made with #[func] because of the rust-specific complex type
    fn do_something_common(&self, world: &mut World);
}

let gd1: Gd<MyClass> = todo!();
let gd2: GdDyn<dyn MyTrait> = gd1.to_dyn();

gd2.bind().do_something_common(...);
StatisMike commented 6 months ago

Don't know if exact duplicate, maybe just a different perspective on #426

MatrixDev commented 6 months ago

Don't know if exact duplicate, maybe just a different perspective on #426

I saw that issue but it talks about abstract classes from the Godot's point of view. I'm asking about rust specific implementation. It is much more limited and should be easier to implement.

Bromeon commented 6 months ago

Let's say you have two types that you want to treat polymorphically:

#[derive(GodotClass)]
#[class(init)]
struct Monster {
    hp: u16,
}

#[derive(GodotClass)]
#[class(init)]
struct Bullet {
    is_alive: bool,
}

You can use a Health trait to abstract over them, which could then be used as follows:

trait Health {
    fn hitpoints(&self) -> u16;
}

// Use polymorphically here
fn is_dead(entity: &dyn Health): bool {
    entity.hitpoints() == 0
}

1) impl on Gd<T>

To achieve that, you can implement Health directly for the Gd pointers:

impl Health for Gd<Monster> {
    fn hitpoints(&self) -> u16 {
        self.bind().hp
    }
}

impl Health for Gd<Bullet> {
    fn hitpoints(&self) -> u16 {
        if self.bind().is_alive { 1 } else { 0 }
    }
}

fn test_health() {
    let monster = Monster::new_gd();
    let bullet = Bullet::new_gd();

    let a = is_dead(&monster);
    let b = is_dead(&bullet);
}

2) impl on T

But you can also implement it on the types directly, moving the bind() call to the use site:

impl Health for Monster {
    fn hitpoints(&self) -> u16 {
        self.hp
    }
}

impl Health for Bullet {
    fn hitpoints(&self) -> u16 {
        if self.is_alive { 1 } else { 0 }
    }
}

fn test_health() {
    let monster = Monster::new_gd();
    let bullet = Bullet::new_gd();

    let a = is_dead(&*monster.bind());
    let b = is_dead(&*bullet.bind());
}

Concrete problem

Given both of the above are possible, it would be good to describe the problem we want to solve more clearly.

For example, what would GdDyn<dyn T> solve that Box<T> or Box<Gd<T>> wouldn't? It's a bit more ergonomic and may avoid double indirection, but conceptually it doesn't unlock new features, does it?

Where would the link to Godot be? Even getting a common base could be abstracted via extra method from the trait 🤔

MatrixDev commented 6 months ago

@Bromeon, sometimes I want to store objects based on their functionality, not concrete implementations.

Just as an example I have a platform (parent object) with multiple types of turrets. All turrets have a common functionality (for example shoot). Now I want to iterate all turrets and shoot with each of those.

Few remarks:

struct World {
...
}

struct TurretStats {
...
}

// both function have non godot-friendly parameters/results
trait Turret {
    fn get_stats_mut(&mut self) -> &mut TurretStats;
    fn shoot(&mut self, world: &mut World);
}

#[derive(GodotClass)]
#[class(base = Node3D)]
struct TurretType1 {
}

impl Turret for TurretType1 {
...
}

#[derive(GodotClass)]
#[class(base = Node3D)]
struct TurretType2 {
}

impl Turret for TurretType2 {
...
}

#[derive(GodotClass)]
#[class(base = Node3D)]
struct Platform {
    world: World,
    turrets: Vec<Gd<...>>, // what do I put here?
}

I can have Vec<Gd<Node3D>> but there is no way to get &dyn Trait from Gd<Node3D>. I can have Vec<Box<dyn Trait>> where impl Trait on Gd<T> but I can't return references from it because bind creates a temporary.

In short what I need is:

  1. some type GdDyn<T>
  2. that can be cast to GdDyn<dyn Trait> where T: Trait
  3. stored for later use
  4. called with bind_mut to receive impl DerefMut<Target=dyn Trait>
MatrixDev commented 6 months ago

Continuing with the above I think that GdDyn<T> should have following requirements / limitations:

  1. must only be contructable from existing Gd<T> where T: Bounds<Declarer=DeclUser>
  2. will have no trait bounds at all (or close to it), all guarantees should come from the original Gd<T>
  3. will need to store additional metadata like Manual / RefCounted or what else it might need to support drop, bind, bind_mut
  4. will implement bind and bind_mut
  5. can be made downcast-able to Gd<T> (something similar toAny)
  6. maybe with some magic it can also be made cast-able to Gd<B> where T: Inherits<B>
Dheatly23 commented 6 months ago

Unfortunately, RTTI with trait object (donwcast(...) -> T where T: Trait) is currently impossible (excluding mopa and such). Even if there is a GdDyn<dyn Trait>, it can't guarantee the pointed value does implement that trait. There is a nightly feature CoerceUnsized that may allow for that, and builtin types (Box/Rc/Arc) use it to ensure it's okay to do so (Arc<T> -> Arc<dyn Trait>).

EDIT: Oh you want cast from Gd<T> to GdDyn<dyn Trait>. I guess that's one way to guarantee that value implement trait. But my point still stands.

Let's say i got a Gd<Node> for example, how is it going to be cast to GdDyn? There are 2 possible route:

  1. Try to convert to every concrete type that implements trait, then recast them to GdDyn. I guess that works, but it's very unergonomic.
  2. Somehow smuggle GdDyn with multiple dispatch 🤷‍♂️. I am not sure how it's done and i don't think it's the right approach.

You see, Gd<T> points to any type that is T or it's decendants. So in a sense, T is a trait, even though it's not obvious. The only exception is user-defined types, which can be bind() to obtain non-DST borrow.

Yarwin commented 3 days ago

Since this issue lags on I'll share my code for handling dynamic dispatch in godot-rust context:

long story short; given trait:

pub trait MyTrait {
    fn method_s(&mut self);
    fn method_a(&mut self, arg: Arg);
    fn method_b(&mut self, arg_b: ArgB, arg_c: ArgC);
}

one can create wrapper that allows to cast Gd (object, resource, node, anything) to dyn MyTrait. The boilerplate is:

type MyTraitGdDispatchSelf = fn(Gd<GType>, fn(&mut dyn MyTrait));
type MyTraitGdDispatchA = fn(Gd<GType>, Arg, fn(&mut dyn MyTrait, Arg));
type MyTraitGdDispatchB = fn(Gd<GType>, ArgB, ArgC, fn(&mut dyn MyTrait, ArgB, ArgC));

pub struct MyTraitGdDispatch {
    dispatch_self: MyTraitGdDispatchSelf,
    dispatch_a: MyTraitGdDispatchA,
    dispatch_b: MyTraitGdDispatchB,
}

impl MyTraitGdDispatch {
    fn new<T>() -> Self
        where
            T: Inherits<GType> + GodotClass + Bounds<Declarer = DeclUser> + MyTrait
    {
        Self {
            dispatch_self: |base, closure| {
                    let mut instance = base.cast::<T>();
                    let mut guard: GdMut<T> = instance.bind_mut();
                    closure(&mut *guard)
                },
            dispatch_a: |base, arg, closure| {
                let mut instance = base.cast::<T>();
                let mut guard: GdMut<T> = instance.bind_mut();
                closure(&mut *guard, arg)
            },
            dispatch_b: |base, arg_b, arg_c, closure| {
                let mut instance = base.cast::<T>();
                let mut guard: GdMut<T> = instance.bind_mut();
                closure(&mut *guard, arg_b, arg_c)
            },
        }
    }
}

static mut DISPATCH_REGISTRY: Option<HashMap<GString, MyTraitGdDispatch>> = None;
pub fn dispatch_registry() -> &'static HashMap<GString, MyTraitGdDispatch> {

    unsafe {
        if DISPATCH_REGISTRY.is_none() {
            DISPATCH_REGISTRY = Some(HashMap::new());
        }
        DISPATCH_REGISTRY.as_ref().unwrap()
    }
}

pub fn register_dispatch<T>(name: GString)
    where
        T: Inherits<GType> + GodotClass + Bounds<Declarer = DeclUser> + MyTrait
{
    unsafe {
        if DISPATCH_REGISTRY.is_none() {
            DISPATCH_REGISTRY = Some(HashMap::new());
        }
        DISPATCH_REGISTRY.as_mut().unwrap().entry(name).or_insert_with(
            || MyTraitGdDispatch::new::<T>()
        );
    }
}
pub struct MyTraitGdDyn {
    pub base: Gd<GType>,
    dispatch: *const MyTraitCursedGdDispatch
}

impl MyTraitGdDyn {
    pub fn new(base: Gd<GType>) -> Self {
        unsafe {
            let dispatch = &dispatch_registry()[&base.get_class()] as *const MyTraitGdDispatch;

        Self {
            base: base.clone(),
            dispatch
        }
        }
    }
}

impl MyTrait for MyTraitGdDyn {
    fn method_s(&mut self) {
        unsafe { ((*self.dispatch).dispatch_self)(self.base.clone(), |d: &mut dyn MyTrait| { d.method_s() }) }
    }
    fn method_a(&mut self, arg: Arg) {
        unsafe { ((*self.dispatch).dispatch_a)(self.base.clone(), arg, |d: &mut dyn MyTrait, arg| { d.method_a(arg) }) }
    }
    fn method_b(&mut self, arg_b: ArgB, arg_c: ArgC) {
        unsafe { ((*self.dispatch).dispatch_b)(self.base.clone(), arg, |d: &mut dyn MyTrait, arg| { d.method_b(arg_b, arg_c) }) }
    }
}

Afterwards one just needs to register given dispatch using https://godot-rust.github.io/docs/gdext/master/godot/init/trait.ExtensionLibrary.html#method.on_level_init or in their main loop or even the instance's _init using something along the lines of register_dispatch<MyClass>(MyClass::class_name().to_gstring()). Additionally ToGodot/FromGodot/GodotConvert can be implemented.

I can make proc-macro for that if anybody is interested, albeit registering given dispatch will still be an user responsibility.

In my project I'm using such abstraction mostly for various Command Objects (that can be passed to executor from my gdext library or gdscript).