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

Support associated types in #[godot_dyn] #1022

Merged
merged 1 commit into from
Jan 24, 2025

Conversation

Yarwin
Copy link
Contributor

@Yarwin Yarwin commented Jan 20, 2025

closes #1021.

WIP - adds support for associated types for #[godot_dyn] macro. Requires some more extensive unit testing - I'm not sure if there are any possible side effects.

(also my macros aren't great, feel free to critique them)

@Yarwin Yarwin marked this pull request as draft January 20, 2025 17:24
@Yarwin Yarwin force-pushed the add-assoc-type-support-to-dyn-gd branch 2 times, most recently from fdd5527 to 80062ff Compare January 20, 2025 17:27
@GodotRust
Copy link

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1022

1 similar comment
@GodotRust

This comment was marked as duplicate.

@Bromeon Bromeon added feature Adds functionality to the library c: register Register classes, functions and other symbols to GDScript labels Jan 20, 2025
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot, this was very quick 😲

In the future, it might also happen that such constraints are extended to associated consts (rust-lang/rust#92827), but I don't think this is happening soon. If someone uses nightly Rust and needs this, they would be welcome to submit another PR 🙂

Comment on lines 47 to 51
let associated_impl = if associated_types.is_empty() {
None
} else {
Some(quote! { <#(#associated_types, )*>})
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Impl" is a bit misleading, as there's no impl block involved.

Also, unless we need to distinguish Some/None in some logic, godot-rust typically uses the empty token stream.

Suggested change
let associated_impl = if associated_types.is_empty() {
None
} else {
Some(quote! { <#(#associated_types, )*>})
};
let assoc_type_constraints = if associated_types.is_empty() {
TokenStream::new()
} else {
quote! { < #(#associated_types),* > }
};

Please apply suggestion verbatim (also spacing + , changed).

@Bromeon Bromeon changed the title Add assoc type support to dyn gd Support associated types in #[godot_dyn] Jan 20, 2025
@Yarwin
Copy link
Contributor Author

Yarwin commented Jan 20, 2025

it seems that I'll pretty much double the tests in dyn_gd_test.rs; not optimal albeit necessary.

@Bromeon
Copy link
Member

Bromeon commented Jan 21, 2025

it seems that I'll pretty much double the tests in dyn_gd_test.rs; not optimal albeit necessary.

Why? Can't you just add another trait with an associated type, and implement it for one type with #[godot_dyn]? We don't need to re-test all the possible interactions, especially since this is a compile-time fix.

@Yarwin
Copy link
Contributor Author

Yarwin commented Jan 21, 2025

Hmm, you are right – I can reduce amount of tests, since I haven't spot any weird side effects 🤔 – I'll leave two implementors and reduce amount of test cases.

EDIT: done. I removed dyn_gd_multiple_traits since we are already check for multiple trait registrations & access to base via dyn trait (in foreign::NodeHealth).

@Yarwin Yarwin marked this pull request as ready for review January 21, 2025 09:52
@Yarwin Yarwin force-pushed the add-assoc-type-support-to-dyn-gd branch from e716815 to 8b71e60 Compare January 22, 2025 07:02
@Yarwin
Copy link
Contributor Author

Yarwin commented Jan 22, 2025

Rebased on current master, resolved conflicts

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation looks good, but tests decrease coverage slightly and are a bit harder to understand now. I made some suggestions on how to simplify things, while still testing the full associated type mechanics.

node.free();
}

#[itest]
fn dyn_gd_downcast() {
let original = Gd::from_object(RefcHealth { hp: 20 }).into_dyn();
let original: DynGd<RefcHealth, dyn Health> = Gd::from_object(RefcHealth { hp: 20 }).into_dyn();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these tests also checked type inference: when there's only one candidate of a trait, the compiler is smart enough to figure it out. Explicitly specifying types defeats this.

Would this still be necessary with my IdProvider proposal below? Would be cool if you could revert changes to existing tests regarding type inference; these should keep running as-is.

Comment on lines 488 to 494
trait HealthWithAssociatedType {
type HealthType;

fn get_report(&self) -> Self::HealthType;

fn set_report(&mut self, new: Self::HealthType);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can instead extend the InstanceIdProvider, e.g. like this?

trait IdProvider {
    type Id;
    fn get_id_dynamic(&self) -> Self::Id;
}

That would require less changes and keep the two traits easier to distinguish.

(Also, if you move it to its original declaration location, the diff is easier to inspect).

@Yarwin Yarwin force-pushed the add-assoc-type-support-to-dyn-gd branch from 8bc6057 to 3b3b3c6 Compare January 23, 2025 12:04
@Yarwin
Copy link
Contributor Author

Yarwin commented Jan 23, 2025

It is worth noting that type inference doesn't work – and results in very confusing errors – in most cases (sic!) if there are multiple possible DynGd<_, D> trait implementations.

In this example foreign::NodeHealth is implementor of two dyn_gd traits: Health and InstanceProvider. Trying to compile following code:

fn dyn_gd_creation_deref() {
    let obj = foreign::NodeHealth::new_alloc();
    let original_id = obj.instance_id();

    let mut obj = obj.into_dyn();

    let dyn_id = obj.instance_id();
    assert_eq!(dyn_id, original_id);

    deal_20_damage(&mut *obj.dyn_bind_mut());
    assert_eq!(obj.dyn_bind().get_hitpoints(), 80);

    obj.free();
}

fn deal_20_damage(h: &mut Health) {
    h.deal_damage(20);
}

trait Health {
    ()
}

Results in bunch of very confusing errors:

    Checking itest v0.0.0 (/home/irwin/apps/godot/opensource-contr/missing_docs/gdext/itest/rust)
error[E0597]: `obj` does not live long enough
  --> itest/rust/src/object_tests/dyn_gd_test.rs:68:26
   |
63 |     let mut obj = obj.into_dyn();
   |         ------- binding `obj` declared here
...
68 |     deal_20_damage(&mut *obj.dyn_bind_mut());
   |                          ^^^---------------
   |                          |
   |                          borrowed value does not live long enough
   |                          argument requires that `obj` is borrowed for `'static`
...
72 | }
   | - `obj` dropped here while still borrowed

error[E0716]: temporary value dropped while borrowed
  --> itest/rust/src/object_tests/dyn_gd_test.rs:68:26
   |
68 |     deal_20_damage(&mut *obj.dyn_bind_mut());
   |     ---------------------^^^^^^^^^^^^^^^^^^-- temporary value is freed at the end of this statement
   |     |                    |
   |     |                    creates a temporary value which is freed while still in use
   |     argument requires that borrow lasts for `'static`

error[E0502]: cannot borrow `obj` as immutable because it is also borrowed as mutable
  --> itest/rust/src/object_tests/dyn_gd_test.rs:69:16
   |
68 |     deal_20_damage(&mut *obj.dyn_bind_mut());
   |                          ------------------
   |                          |
   |                          mutable borrow occurs here
   |                          argument requires that `obj` is borrowed for `'static`
69 |     assert_eq!(obj.dyn_bind().get_hitpoints(), 80);
   |                ^^^ immutable borrow occurs here

error[E0505]: cannot move out of `obj` because it is borrowed
  --> itest/rust/src/object_tests/dyn_gd_test.rs:71:5
   |
63 |     let mut obj = obj.into_dyn();
   |         ------- binding `obj` declared here
...
68 |     deal_20_damage(&mut *obj.dyn_bind_mut());
   |                          ------------------
   |                          |
   |                          borrow of `obj` occurs here
   |                          argument requires that `obj` is borrowed for `'static`
...
71 |     obj.free();
   |     ^^^ move out of `obj` occurs here

It might be that &mut (dyn Health + 'static) - a return type declared in NodeHealth's AsDyn implementation for dyn_upcasts– is different from &mut Health and compiler tries to enforce/satisfy this lifetime bound by making a reference static?

This issue should be mentioned and documented in the book. The errors are unhelpful and confusing. It is next to impossible to know what is going on without knowledge about an inner implementation (which might require some digging – AsDyn is hidden behind macro invocation for example, checking descriptions of dyn_binds doesn't yield anything either…).

Current solutions to this problem:

  1. Just explicitly specify given dyn trait (recommended):
let mut obj = obj.into_dyn::<dyn Health>();
let mut obj: DynGd<_, dyn Health> = obj.into_dyn();. 
  1. Enforce lifetime bound on original trait:
trait Health: 'static {}
  1. Explicitly declare lifetime bound in function signature: fn deal_20_damage(h: &mut (dyn Health + 'static)) {…}. (ugh)

Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. I moved them to their own issue, since they have nothing to do with associated type.

Small things left, then this should be ready! Please also squash commits once ready from your side 🙂


let mut node = node.into_dyn::<dyn Health>();
// type can be safely inferred.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// type can be safely inferred.
// Type can be safely inferred.

let obj = foreign::NodeHealth::new_alloc();
let original_id = obj.instance_id();

// trait must be explicitly declared if multiple AsDyn<...> trait implementations exists.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// trait must be explicitly declared if multiple AsDyn<...> trait implementations exists.
// `dyn Health` must be explicitly declared if multiple AsDyn<...> trait implementations exist.

- Add support for traits with associated types for #[godot_dyn]`.
- Refactor tests to account for both cases.
@Yarwin Yarwin force-pushed the add-assoc-type-support-to-dyn-gd branch from 3b3b3c6 to 72ac6d4 Compare January 23, 2025 23:10
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot!

@Bromeon Bromeon added this pull request to the merge queue Jan 24, 2025
Merged via the queue into godot-rust:master with commit bf4ddd9 Jan 24, 2025
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: register Register classes, functions and other symbols to GDScript feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

#[godot_dyn]: Add support for associated types
3 participants