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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions godot-macros/src/class/godot_dyn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,42 @@ pub fn attribute_godot_dyn(input_decl: venial::Item) -> ParseResult<TokenStream>
);
};

let mut associated_types = vec![];
for impl_member in &decl.body_items {
let venial::ImplMember::AssocType(associated_type) = impl_member else {
continue;
};
let Some(type_expr) = &associated_type.initializer_ty else {
continue;
};
let type_name = &associated_type.name;
associated_types.push(quote! { #type_name = #type_expr })
}

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

let class_path = &decl.self_ty;
let prv = quote! { ::godot::private };

//let dynify_fn = format_ident!("__dynify_{}", class_name);

let new_code = quote! {
#decl

impl ::godot::obj::AsDyn<dyn #trait_path> for #class_path {
fn dyn_upcast(&self) -> &(dyn #trait_path + 'static) {
impl ::godot::obj::AsDyn<dyn #trait_path #assoc_type_constraints> for #class_path {
fn dyn_upcast(&self) -> &(dyn #trait_path #assoc_type_constraints + 'static) {
self
}

fn dyn_upcast_mut(&mut self) -> &mut (dyn #trait_path + 'static) {
fn dyn_upcast_mut(&mut self) -> &mut (dyn #trait_path #assoc_type_constraints + 'static) {
self
}
}

::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin::new::<#class_path>(
#prv::PluginItem::DynTraitImpl(#prv::DynTraitImpl::new::<#class_path, dyn #trait_path>()))
#prv::PluginItem::DynTraitImpl(#prv::DynTraitImpl::new::<#class_path, dyn #trait_path #assoc_type_constraints>()))
);

};
Expand Down
150 changes: 110 additions & 40 deletions itest/rust/src/object_tests/dyn_gd_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,34 @@ fn dyn_gd_creation_bind() {

#[itest]
fn dyn_gd_creation_deref() {
let node = foreign::NodeHealth::new_alloc();
let original_id = node.instance_id();
let obj = Gd::from_object(RefcHealth { hp: 100 });
let original_id = obj.instance_id();

let mut node = node.into_dyn::<dyn Health>();
// Type can be safely inferred.
let mut obj = obj.into_dyn();

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

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

node.free();
#[itest]
fn dyn_gd_creation_deref_multiple_traits() {
let obj = foreign::NodeHealth::new_alloc();
let original_id = obj.instance_id();

// `dyn Health` must be explicitly declared if multiple AsDyn<...> trait implementations exist.
let mut obj = obj.into_dyn::<dyn Health>();

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 dyn Health) {
Expand Down Expand Up @@ -104,13 +120,25 @@ fn dyn_gd_downcast() {

#[itest]
fn dyn_gd_debug() {
let obj = Gd::from_object(RefcHealth { hp: 20 }).into_dyn();
let id = obj.instance_id();
let node = foreign::NodeHealth::new_alloc();
let id = node.instance_id();

let actual = format!(".:{obj:?}:.");
let expected = format!(".:DynGd {{ id: {id}, class: RefcHealth, trait: dyn Health }}:.");
let node = node.into_dyn::<dyn Health>();

let actual = format!(".:{node:?}:.");
let expected = format!(".:DynGd {{ id: {id}, class: NodeHealth, trait: dyn Health }}:.");

assert_eq!(actual, expected);

let node = node
.into_gd()
.into_dyn::<dyn InstanceIdProvider<Id = InstanceId>>();
let actual = format!(".:{node:?}:.");
let expected = format!(".:DynGd {{ id: {id}, class: NodeHealth, trait: dyn InstanceIdProvider<Id = InstanceId> }}:.");

assert_eq!(actual, expected);

node.free();
}

#[itest]
Expand Down Expand Up @@ -249,42 +277,51 @@ fn dyn_gd_pass_to_godot_api() {

#[itest]
fn dyn_gd_variant_conversions() {
let original = Gd::from_object(RefcHealth { hp: 11 }).into_dyn::<dyn Health>();
let original_id = original.instance_id();
let refc = original.into_gd().upcast::<RefCounted>();
let node = foreign::NodeHealth::new_alloc();
let original_id = node.instance_id();

let variant = refc.to_variant();
let variant = node.to_variant();

// Convert to different levels of DynGd:

let back: DynGd<RefcHealth, dyn Health> = variant.to();
assert_eq!(back.bind().get_hitpoints(), 11);
let back: DynGd<foreign::NodeHealth, dyn Health> = variant.to();
assert_eq!(back.bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

let back: DynGd<RefCounted, dyn Health> = variant.to();
assert_eq!(back.dyn_bind().get_hitpoints(), 11);
let back: DynGd<Node, dyn Health> = variant.to();
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

let back: DynGd<Object, dyn Health> = variant.to();
assert_eq!(back.dyn_bind().get_hitpoints(), 11);
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

// Convert to different levels of Gd:
// Convert to different DynGd:

let back: Gd<RefcHealth> = variant.to();
assert_eq!(back.bind().get_hitpoints(), 11);
assert_eq!(back.instance_id(), original_id);
let back: DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);

let back: DynGd<Node, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);

let back: Gd<RefcHealth> = variant.to();
let back: DynGd<Object, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);

// Convert to different levels of Gd:

let back: Gd<foreign::NodeHealth> = variant.to();
assert_eq!(back.bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

let back: Gd<Object> = variant.to();
assert_eq!(back.instance_id(), original_id);

node.free();
}

#[itest]
fn dyn_gd_store_in_godot_array() {
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::<dyn Health>();
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn();
let b = foreign::NodeHealth::new_alloc().into_dyn();

let array: Array<DynGd<Object, _>> = array![&a.upcast(), &b.upcast()];
Expand All @@ -298,21 +335,29 @@ fn dyn_gd_store_in_godot_array() {
#[itest]
fn dyn_gd_error_unregistered_trait() {
trait UnrelatedTrait {}
let node = foreign::NodeHealth::new_alloc().into_dyn::<dyn Health>();

let obj = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::<dyn Health>();
let variant = node.to_variant();

let variant = obj.to_variant();
let back = variant.try_to::<DynGd<RefcHealth, dyn UnrelatedTrait>>();
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn UnrelatedTrait>>();

// The conversion fails before a DynGd is created, so Display still operates on the Gd.
let node = node.into_gd();

let err = back.expect_err("DynGd::try_to() should have failed");
let expected_err = {
// The conversion fails before a DynGd is created, so Display still operates on the Gd.
let obj = obj.into_gd();
let expected_err =
format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: {node:?}");

format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: {obj:?}")
};
assert_eq!(err.to_string(), expected_err);

let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = i32>>>();

let err = back.expect_err("DynGd::try_to() should have failed");
let expected_err = format!("trait `dyn InstanceIdProvider<Id = i32>` has not been registered with #[godot_dyn]: {node:?}");

assert_eq!(err.to_string(), expected_err);

node.free();
}

#[itest]
Expand All @@ -327,11 +372,23 @@ fn dyn_gd_error_unimplemented_trait() {
err.to_string(),
format!("none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: {obj:?}")
);

let node = foreign::NodeHealth::new_alloc();
let variant = node.to_variant();
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = f32>>>();

let err = back.expect_err("DynGd::try_to() should have failed");
assert_eq!(
err.to_string(),
format!("none of the classes derived from `NodeHealth` have been linked to trait `dyn InstanceIdProvider<Id = f32>` with #[godot_dyn]: {node:?}")
);

node.free();
}

#[itest]
fn dyn_gd_free_while_dyn_bound() {
let mut obj: DynGd<_, dyn Health> = foreign::NodeHealth::new_alloc().into_dyn();
let mut obj = foreign::NodeHealth::new_alloc().into_dyn::<dyn Health>();

{
let copy = obj.clone();
Expand Down Expand Up @@ -359,7 +416,9 @@ fn dyn_gd_multiple_traits() {
let obj = foreign::NodeHealth::new_alloc();
let original_id = obj.instance_id();

let obj = obj.into_dyn::<dyn InstanceIdProvider>().upcast::<Node>();
let obj = obj
.into_dyn::<dyn InstanceIdProvider<Id = InstanceId>>()
.upcast::<Node>();
let id = obj.dyn_bind().get_id_dynamic();
assert_eq!(id, original_id);

Expand Down Expand Up @@ -434,14 +493,15 @@ impl Health for foreign::NodeHealth {
// ----------------------------------------------------------------------------------------------------------------------------------------------
// Check that one class can implement two or more traits.

// Pointless trait, but tests access to object.
trait InstanceIdProvider {
fn get_id_dynamic(&self) -> InstanceId;
type Id;
fn get_id_dynamic(&self) -> Self::Id;
}

#[godot_dyn]
impl InstanceIdProvider for foreign::NodeHealth {
fn get_id_dynamic(&self) -> InstanceId {
type Id = InstanceId;
fn get_id_dynamic(&self) -> Self::Id {
self.base().instance_id()
}
}
Expand All @@ -455,5 +515,15 @@ struct RefcDynGdExporter {
#[var]
first: Option<DynGd<Object, dyn Health>>,
#[export]
second: Option<DynGd<foreign::NodeHealth, dyn InstanceIdProvider>>,
second: Option<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = InstanceId>>>,
}

// Implementation created only to register the DynGd `HealthWithAssociatedType<HealthType=f32>` trait.
// Pointless trait, but tests proper conversion.
#[godot_dyn]
impl InstanceIdProvider for RefcDynGdExporter {
type Id = f32;
fn get_id_dynamic(&self) -> Self::Id {
42.0
}
}
Loading