Skip to content

Commit bf4ddd9

Browse files
authored
Merge pull request #1022 from Yarwin/add-assoc-type-support-to-dyn-gd
Support associated types in `#[godot_dyn]`
2 parents 97b66ec + 72ac6d4 commit bf4ddd9

File tree

2 files changed

+132
-46
lines changed

2 files changed

+132
-46
lines changed

godot-macros/src/class/godot_dyn.rs

+22-6
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,42 @@ pub fn attribute_godot_dyn(input_decl: venial::Item) -> ParseResult<TokenStream>
3232
);
3333
};
3434

35+
let mut associated_types = vec![];
36+
for impl_member in &decl.body_items {
37+
let venial::ImplMember::AssocType(associated_type) = impl_member else {
38+
continue;
39+
};
40+
let Some(type_expr) = &associated_type.initializer_ty else {
41+
continue;
42+
};
43+
let type_name = &associated_type.name;
44+
associated_types.push(quote! { #type_name = #type_expr })
45+
}
46+
47+
let assoc_type_constraints = if associated_types.is_empty() {
48+
TokenStream::new()
49+
} else {
50+
quote! { < #(#associated_types),* > }
51+
};
52+
3553
let class_path = &decl.self_ty;
3654
let prv = quote! { ::godot::private };
3755

38-
//let dynify_fn = format_ident!("__dynify_{}", class_name);
39-
4056
let new_code = quote! {
4157
#decl
4258

43-
impl ::godot::obj::AsDyn<dyn #trait_path> for #class_path {
44-
fn dyn_upcast(&self) -> &(dyn #trait_path + 'static) {
59+
impl ::godot::obj::AsDyn<dyn #trait_path #assoc_type_constraints> for #class_path {
60+
fn dyn_upcast(&self) -> &(dyn #trait_path #assoc_type_constraints + 'static) {
4561
self
4662
}
4763

48-
fn dyn_upcast_mut(&mut self) -> &mut (dyn #trait_path + 'static) {
64+
fn dyn_upcast_mut(&mut self) -> &mut (dyn #trait_path #assoc_type_constraints + 'static) {
4965
self
5066
}
5167
}
5268

5369
::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin::new::<#class_path>(
54-
#prv::PluginItem::DynTraitImpl(#prv::DynTraitImpl::new::<#class_path, dyn #trait_path>()))
70+
#prv::PluginItem::DynTraitImpl(#prv::DynTraitImpl::new::<#class_path, dyn #trait_path #assoc_type_constraints>()))
5571
);
5672

5773
};

itest/rust/src/object_tests/dyn_gd_test.rs

+110-40
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,34 @@ fn dyn_gd_creation_bind() {
4141

4242
#[itest]
4343
fn dyn_gd_creation_deref() {
44-
let node = foreign::NodeHealth::new_alloc();
45-
let original_id = node.instance_id();
44+
let obj = Gd::from_object(RefcHealth { hp: 100 });
45+
let original_id = obj.instance_id();
4646

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

49-
let dyn_id = node.instance_id();
50+
let dyn_id = obj.instance_id();
5051
assert_eq!(dyn_id, original_id);
5152

52-
deal_20_damage(&mut *node.dyn_bind_mut());
53-
assert_eq!(node.dyn_bind().get_hitpoints(), 80);
53+
deal_20_damage(&mut *obj.dyn_bind_mut());
54+
assert_eq!(obj.dyn_bind().get_hitpoints(), 80);
55+
}
5456

55-
node.free();
57+
#[itest]
58+
fn dyn_gd_creation_deref_multiple_traits() {
59+
let obj = foreign::NodeHealth::new_alloc();
60+
let original_id = obj.instance_id();
61+
62+
// `dyn Health` must be explicitly declared if multiple AsDyn<...> trait implementations exist.
63+
let mut obj = obj.into_dyn::<dyn Health>();
64+
65+
let dyn_id = obj.instance_id();
66+
assert_eq!(dyn_id, original_id);
67+
68+
deal_20_damage(&mut *obj.dyn_bind_mut());
69+
assert_eq!(obj.dyn_bind().get_hitpoints(), 80);
70+
71+
obj.free();
5672
}
5773

5874
fn deal_20_damage(h: &mut dyn Health) {
@@ -104,13 +120,25 @@ fn dyn_gd_downcast() {
104120

105121
#[itest]
106122
fn dyn_gd_debug() {
107-
let obj = Gd::from_object(RefcHealth { hp: 20 }).into_dyn();
108-
let id = obj.instance_id();
123+
let node = foreign::NodeHealth::new_alloc();
124+
let id = node.instance_id();
109125

110-
let actual = format!(".:{obj:?}:.");
111-
let expected = format!(".:DynGd {{ id: {id}, class: RefcHealth, trait: dyn Health }}:.");
126+
let node = node.into_dyn::<dyn Health>();
127+
128+
let actual = format!(".:{node:?}:.");
129+
let expected = format!(".:DynGd {{ id: {id}, class: NodeHealth, trait: dyn Health }}:.");
112130

113131
assert_eq!(actual, expected);
132+
133+
let node = node
134+
.into_gd()
135+
.into_dyn::<dyn InstanceIdProvider<Id = InstanceId>>();
136+
let actual = format!(".:{node:?}:.");
137+
let expected = format!(".:DynGd {{ id: {id}, class: NodeHealth, trait: dyn InstanceIdProvider<Id = InstanceId> }}:.");
138+
139+
assert_eq!(actual, expected);
140+
141+
node.free();
114142
}
115143

116144
#[itest]
@@ -249,42 +277,51 @@ fn dyn_gd_pass_to_godot_api() {
249277

250278
#[itest]
251279
fn dyn_gd_variant_conversions() {
252-
let original = Gd::from_object(RefcHealth { hp: 11 }).into_dyn::<dyn Health>();
253-
let original_id = original.instance_id();
254-
let refc = original.into_gd().upcast::<RefCounted>();
280+
let node = foreign::NodeHealth::new_alloc();
281+
let original_id = node.instance_id();
255282

256-
let variant = refc.to_variant();
283+
let variant = node.to_variant();
257284

258285
// Convert to different levels of DynGd:
259286

260-
let back: DynGd<RefcHealth, dyn Health> = variant.to();
261-
assert_eq!(back.bind().get_hitpoints(), 11);
287+
let back: DynGd<foreign::NodeHealth, dyn Health> = variant.to();
288+
assert_eq!(back.bind().get_hitpoints(), 100);
262289
assert_eq!(back.instance_id(), original_id);
263290

264-
let back: DynGd<RefCounted, dyn Health> = variant.to();
265-
assert_eq!(back.dyn_bind().get_hitpoints(), 11);
291+
let back: DynGd<Node, dyn Health> = variant.to();
292+
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
266293
assert_eq!(back.instance_id(), original_id);
267294

268295
let back: DynGd<Object, dyn Health> = variant.to();
269-
assert_eq!(back.dyn_bind().get_hitpoints(), 11);
296+
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
270297
assert_eq!(back.instance_id(), original_id);
271298

272-
// Convert to different levels of Gd:
299+
// Convert to different DynGd:
273300

274-
let back: Gd<RefcHealth> = variant.to();
275-
assert_eq!(back.bind().get_hitpoints(), 11);
276-
assert_eq!(back.instance_id(), original_id);
301+
let back: DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
302+
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);
303+
304+
let back: DynGd<Node, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
305+
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);
277306

278-
let back: Gd<RefcHealth> = variant.to();
307+
let back: DynGd<Object, dyn InstanceIdProvider<Id = InstanceId>> = variant.to();
308+
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);
309+
310+
// Convert to different levels of Gd:
311+
312+
let back: Gd<foreign::NodeHealth> = variant.to();
313+
assert_eq!(back.bind().get_hitpoints(), 100);
279314
assert_eq!(back.instance_id(), original_id);
280315

281316
let back: Gd<Object> = variant.to();
282317
assert_eq!(back.instance_id(), original_id);
318+
319+
node.free();
283320
}
284321

285322
#[itest]
286323
fn dyn_gd_store_in_godot_array() {
287-
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::<dyn Health>();
324+
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn();
288325
let b = foreign::NodeHealth::new_alloc().into_dyn();
289326

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

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

304-
let variant = obj.to_variant();
305-
let back = variant.try_to::<DynGd<RefcHealth, dyn UnrelatedTrait>>();
342+
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn UnrelatedTrait>>();
343+
344+
// The conversion fails before a DynGd is created, so Display still operates on the Gd.
345+
let node = node.into_gd();
306346

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

312-
format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: {obj:?}")
313-
};
351+
assert_eq!(err.to_string(), expected_err);
352+
353+
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = i32>>>();
354+
355+
let err = back.expect_err("DynGd::try_to() should have failed");
356+
let expected_err = format!("trait `dyn InstanceIdProvider<Id = i32>` has not been registered with #[godot_dyn]: {node:?}");
314357

315358
assert_eq!(err.to_string(), expected_err);
359+
360+
node.free();
316361
}
317362

318363
#[itest]
@@ -327,11 +372,23 @@ fn dyn_gd_error_unimplemented_trait() {
327372
err.to_string(),
328373
format!("none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: {obj:?}")
329374
);
375+
376+
let node = foreign::NodeHealth::new_alloc();
377+
let variant = node.to_variant();
378+
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = f32>>>();
379+
380+
let err = back.expect_err("DynGd::try_to() should have failed");
381+
assert_eq!(
382+
err.to_string(),
383+
format!("none of the classes derived from `NodeHealth` have been linked to trait `dyn InstanceIdProvider<Id = f32>` with #[godot_dyn]: {node:?}")
384+
);
385+
386+
node.free();
330387
}
331388

332389
#[itest]
333390
fn dyn_gd_free_while_dyn_bound() {
334-
let mut obj: DynGd<_, dyn Health> = foreign::NodeHealth::new_alloc().into_dyn();
391+
let mut obj = foreign::NodeHealth::new_alloc().into_dyn::<dyn Health>();
335392

336393
{
337394
let copy = obj.clone();
@@ -359,7 +416,9 @@ fn dyn_gd_multiple_traits() {
359416
let obj = foreign::NodeHealth::new_alloc();
360417
let original_id = obj.instance_id();
361418

362-
let obj = obj.into_dyn::<dyn InstanceIdProvider>().upcast::<Node>();
419+
let obj = obj
420+
.into_dyn::<dyn InstanceIdProvider<Id = InstanceId>>()
421+
.upcast::<Node>();
363422
let id = obj.dyn_bind().get_id_dynamic();
364423
assert_eq!(id, original_id);
365424

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

437-
// Pointless trait, but tests access to object.
438496
trait InstanceIdProvider {
439-
fn get_id_dynamic(&self) -> InstanceId;
497+
type Id;
498+
fn get_id_dynamic(&self) -> Self::Id;
440499
}
441500

442501
#[godot_dyn]
443502
impl InstanceIdProvider for foreign::NodeHealth {
444-
fn get_id_dynamic(&self) -> InstanceId {
503+
type Id = InstanceId;
504+
fn get_id_dynamic(&self) -> Self::Id {
445505
self.base().instance_id()
446506
}
447507
}
@@ -455,5 +515,15 @@ struct RefcDynGdExporter {
455515
#[var]
456516
first: Option<DynGd<Object, dyn Health>>,
457517
#[export]
458-
second: Option<DynGd<foreign::NodeHealth, dyn InstanceIdProvider>>,
518+
second: Option<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = InstanceId>>>,
519+
}
520+
521+
// Implementation created only to register the DynGd `HealthWithAssociatedType<HealthType=f32>` trait.
522+
// Pointless trait, but tests proper conversion.
523+
#[godot_dyn]
524+
impl InstanceIdProvider for RefcDynGdExporter {
525+
type Id = f32;
526+
fn get_id_dynamic(&self) -> Self::Id {
527+
42.0
528+
}
459529
}

0 commit comments

Comments
 (0)