From 72ac6d48bb97955b255fe3a8578fc510dfd6609c Mon Sep 17 00:00:00 2001 From: Yarvin Date: Mon, 20 Jan 2025 18:26:17 +0100 Subject: [PATCH] Support associated types in `#[godot_dyn]` - Add support for traits with associated types for #[godot_dyn]`. - Refactor tests to account for both cases. --- godot-macros/src/class/godot_dyn.rs | 28 +++- itest/rust/src/object_tests/dyn_gd_test.rs | 150 +++++++++++++++------ 2 files changed, 132 insertions(+), 46 deletions(-) diff --git a/godot-macros/src/class/godot_dyn.rs b/godot-macros/src/class/godot_dyn.rs index 0aeca2bb8..ee6b16d08 100644 --- a/godot-macros/src/class/godot_dyn.rs +++ b/godot-macros/src/class/godot_dyn.rs @@ -32,26 +32,42 @@ pub fn attribute_godot_dyn(input_decl: venial::Item) -> ParseResult ); }; + 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 for #class_path { - fn dyn_upcast(&self) -> &(dyn #trait_path + 'static) { + impl ::godot::obj::AsDyn 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>())) ); }; diff --git a/itest/rust/src/object_tests/dyn_gd_test.rs b/itest/rust/src/object_tests/dyn_gd_test.rs index 777694287..227cd9d71 100644 --- a/itest/rust/src/object_tests/dyn_gd_test.rs +++ b/itest/rust/src/object_tests/dyn_gd_test.rs @@ -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::(); + // 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::(); + + 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) { @@ -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::(); + + 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::>(); + let actual = format!(".:{node:?}:."); + let expected = format!(".:DynGd {{ id: {id}, class: NodeHealth, trait: dyn InstanceIdProvider }}:."); + + assert_eq!(actual, expected); + + node.free(); } #[itest] @@ -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::(); - let original_id = original.instance_id(); - let refc = original.into_gd().upcast::(); + 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 = variant.to(); - assert_eq!(back.bind().get_hitpoints(), 11); + let back: DynGd = variant.to(); + assert_eq!(back.bind().get_hitpoints(), 100); assert_eq!(back.instance_id(), original_id); - let back: DynGd = variant.to(); - assert_eq!(back.dyn_bind().get_hitpoints(), 11); + let back: DynGd = variant.to(); + assert_eq!(back.dyn_bind().get_hitpoints(), 100); assert_eq!(back.instance_id(), original_id); let back: DynGd = 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 = variant.to(); - assert_eq!(back.bind().get_hitpoints(), 11); - assert_eq!(back.instance_id(), original_id); + let back: DynGd> = variant.to(); + assert_eq!(back.dyn_bind().get_id_dynamic(), original_id); + + let back: DynGd> = variant.to(); + assert_eq!(back.dyn_bind().get_id_dynamic(), original_id); - let back: Gd = variant.to(); + let back: DynGd> = variant.to(); + assert_eq!(back.dyn_bind().get_id_dynamic(), original_id); + + // Convert to different levels of Gd: + + let back: Gd = variant.to(); + assert_eq!(back.bind().get_hitpoints(), 100); assert_eq!(back.instance_id(), original_id); let back: Gd = 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::(); + let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn(); let b = foreign::NodeHealth::new_alloc().into_dyn(); let array: Array> = array![&a.upcast(), &b.upcast()]; @@ -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::(); - let obj = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::(); + let variant = node.to_variant(); - let variant = obj.to_variant(); - let back = variant.try_to::>(); + let back = variant.try_to::>(); + + // 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::>>(); + + let err = back.expect_err("DynGd::try_to() should have failed"); + let expected_err = format!("trait `dyn InstanceIdProvider` has not been registered with #[godot_dyn]: {node:?}"); assert_eq!(err.to_string(), expected_err); + + node.free(); } #[itest] @@ -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::>>(); + + 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` 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::(); { let copy = obj.clone(); @@ -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::().upcast::(); + let obj = obj + .into_dyn::>() + .upcast::(); let id = obj.dyn_bind().get_id_dynamic(); assert_eq!(id, original_id); @@ -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() } } @@ -455,5 +515,15 @@ struct RefcDynGdExporter { #[var] first: Option>, #[export] - second: Option>, + second: Option>>, +} + +// Implementation created only to register the DynGd `HealthWithAssociatedType` 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 + } }