Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GdDyn<dyn Trait> type #631

Open
Tracked by #697
MatrixDev opened this issue Feb 27, 2024 · 8 comments · May be fixed by #930 or #953
Open
Tracked by #697

Add GdDyn<dyn Trait> type #631

MatrixDev opened this issue Feb 27, 2024 · 8 comments · May be fixed by #930 or #953
Labels
c: core Core components feature Adds functionality to the library
Milestone

Comments

@MatrixDev
Copy link

MatrixDev commented Feb 27, 2024

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
Copy link
Contributor

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

@MatrixDev
Copy link
Author

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 Bromeon added feature Adds functionality to the library c: core Core components labels Feb 27, 2024
@Bromeon
Copy link
Member

Bromeon commented Mar 5, 2024

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
Copy link
Author

@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:

  • turrets are added dynamically
  • there can be a lot of turret types
  • I need to pass non #[func]-friendly arguments
  • I need to return non #[func]-friendly arguments
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
Copy link
Author

MatrixDev commented Mar 5, 2024

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
Copy link
Contributor

Dheatly23 commented Mar 11, 2024

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
Copy link
Contributor

Yarwin commented Sep 13, 2024

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).

@Yarwin Yarwin linked a pull request Oct 26, 2024 that will close this issue
@Bromeon Bromeon added this to the 0.2.x milestone Nov 9, 2024
@Bromeon
Copy link
Member

Bromeon commented Nov 24, 2024

Took far too long, but I finally wrapped up my proof-of-concept of DynGd<Class, dyn Trait> in #953 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components feature Adds functionality to the library
Projects
None yet
5 participants