Skip to content

Commit a3e2cdf

Browse files
authored
Merge pull request #1255 from Yarwin/implement_gd_try_into_dyn
Implement `Gd::try_dynify`.
2 parents 88ab841 + 0a500a0 commit a3e2cdf

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

godot-core/src/obj/dyn_gd.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,37 @@ use std::{fmt, ops};
131131
/// // Now work with the abstract object as usual.
132132
/// ```
133133
///
134+
/// Any `Gd<T>` where `T` is an engine class can attempt conversion to `DynGd<T, D>` with [`Gd::try_dynify()`] as well.
135+
///
136+
/// ```no_run
137+
/// # use godot::prelude::*;
138+
/// # use godot::classes::Node2D;
139+
/// # // ShapeCast2D is marked as experimental and thus not included in the doctests.
140+
/// # // We use this mock to showcase some real-world usage.
141+
/// # struct FakeShapeCastCollider2D {}
142+
///
143+
/// # impl FakeShapeCastCollider2D {
144+
/// # fn get_collider(&self, _idx: i32) -> Option<Gd<Node2D>> { Some(Node2D::new_alloc()) }
145+
/// # }
146+
///
147+
/// trait Pushable { /* ... */ }
148+
///
149+
/// # let my_shapecast = FakeShapeCastCollider2D {};
150+
/// # let idx = 1;
151+
/// // We can try to convert `Gd<T>` into `DynGd<T, D>`.
152+
/// let node: Option<DynGd<Node2D, dyn Pushable>> =
153+
/// my_shapecast.get_collider(idx).and_then(
154+
/// |obj| obj.try_dynify().ok()
155+
/// );
156+
///
157+
/// // An object is returned after failed conversion, similarly to `Gd::try_cast()`.
158+
/// # let some_node = Node::new_alloc();
159+
/// match some_node.try_dynify::<dyn Pushable>() {
160+
/// Ok(dyn_gd) => (),
161+
/// Err(some_node) => godot_warn!("Failed to convert {some_node} into dyn Pushable!"),
162+
/// }
163+
/// ```
164+
///
134165
/// When converting from Godot back into `DynGd`, we say that the `dyn Health` trait object is _re-enriched_.
135166
///
136167
/// godot-rust achieves this thanks to the registration done by `#[godot_dyn]`: the library knows for which classes `Health` is implemented,
@@ -541,7 +572,10 @@ where
541572
D: ?Sized + 'static,
542573
{
543574
fn try_from_godot(via: Self::Via) -> Result<Self, ConvertError> {
544-
try_dynify_object(via)
575+
match try_dynify_object(via) {
576+
Ok(dyn_gd) => Ok(dyn_gd),
577+
Err((from_godot_err, obj)) => Err(from_godot_err.into_error(obj)),
578+
}
545579
}
546580
}
547581

godot-core/src/obj/gd.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use crate::obj::{
2222
OnEditor, RawGd, WithSignals,
2323
};
2424
use crate::private::{callbacks, PanicPayload};
25+
use crate::registry::class::try_dynify_object;
2526
use crate::registry::property::{object_export_element_type_string, Export, Var};
2627
use crate::{classes, out};
2728

@@ -512,6 +513,21 @@ impl<T: GodotClass> Gd<T> {
512513
DynGd::<T, D>::from_gd(self)
513514
}
514515

516+
/// Tries to upgrade to a `DynGd<T, D>` pointer, enabling the `D` abstraction.
517+
///
518+
/// If `T`'s dynamic class doesn't implement `AsDyn<D>`, `Err(self)` is returned, meaning you can reuse the original
519+
/// object for further casts.
520+
pub fn try_dynify<D>(self) -> Result<DynGd<T, D>, Self>
521+
where
522+
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
523+
D: ?Sized + 'static,
524+
{
525+
match try_dynify_object(self) {
526+
Ok(dyn_gd) => Ok(dyn_gd),
527+
Err((_convert_err, obj)) => Err(obj),
528+
}
529+
}
530+
515531
/// Returns a callable referencing a method from this object named `method_name`.
516532
///
517533
/// This is shorter syntax for [`Callable::from_object_method(self, method_name)`][Callable::from_object_method].

godot-core/src/registry/class.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::{any, ptr};
1111

1212
use crate::classes::ClassDb;
1313
use crate::init::InitLevel;
14-
use crate::meta::error::{ConvertError, FromGodotError};
14+
use crate::meta::error::FromGodotError;
1515
use crate::meta::ClassName;
1616
use crate::obj::{cap, DynGd, Gd, GodotClass};
1717
use crate::private::{ClassPlugin, PluginItem};
@@ -324,14 +324,14 @@ pub fn auto_register_rpcs<T: GodotClass>(object: &mut T) {
324324
/// lifted, but would need quite a bit of extra machinery to work.
325325
pub(crate) fn try_dynify_object<T: GodotClass, D: ?Sized + 'static>(
326326
mut object: Gd<T>,
327-
) -> Result<DynGd<T, D>, ConvertError> {
327+
) -> Result<DynGd<T, D>, (FromGodotError, Gd<T>)> {
328328
let typeid = any::TypeId::of::<D>();
329329
let trait_name = sys::short_type_name::<D>();
330330

331331
// Iterate all classes that implement the trait.
332332
let dyn_traits_by_typeid = global_dyn_traits_by_typeid();
333333
let Some(relations) = dyn_traits_by_typeid.get(&typeid) else {
334-
return Err(FromGodotError::UnregisteredDynTrait { trait_name }.into_error(object));
334+
return Err((FromGodotError::UnregisteredDynTrait { trait_name }, object));
335335
};
336336

337337
// TODO maybe use 2nd hashmap instead of linear search.
@@ -348,7 +348,7 @@ pub(crate) fn try_dynify_object<T: GodotClass, D: ?Sized + 'static>(
348348
class_name: object.dynamic_class_string().to_string(),
349349
};
350350

351-
Err(error.into_error(object))
351+
Err((error, object))
352352
}
353353

354354
/// Responsible for creating hint_string for [`DynGd<T, D>`][crate::obj::DynGd] properties which works with [`PropertyHint::NODE_TYPE`][crate::global::PropertyHint::NODE_TYPE] or [`PropertyHint::RESOURCE_TYPE`][crate::global::PropertyHint::RESOURCE_TYPE].

itest/rust/src/object_tests/dyn_gd_test.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,71 @@ fn dyn_gd_variant_conversions() {
342342
node.free();
343343
}
344344

345+
#[itest]
346+
fn dyn_gd_object_conversions() {
347+
let node = foreign::NodeHealth::new_alloc().upcast::<Node>();
348+
let original_id = node.instance_id();
349+
350+
// Convert to different levels of DynGd:
351+
let back: DynGd<Node, dyn Health> = node
352+
.try_dynify()
353+
.expect("Gd::try_dynify() should succeed.")
354+
.cast();
355+
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
356+
assert_eq!(back.instance_id(), original_id);
357+
358+
let obj = back.into_gd().upcast::<Object>();
359+
let back: DynGd<Object, dyn Health> =
360+
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
361+
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
362+
assert_eq!(back.instance_id(), original_id);
363+
364+
// Back to NodeHealth.
365+
let node = back.cast::<foreign::NodeHealth>();
366+
assert_eq!(node.bind().get_hitpoints(), 100);
367+
assert_eq!(node.instance_id(), original_id);
368+
369+
// Convert to different DynGd.
370+
let obj = node.into_gd().upcast::<Node>();
371+
let back: DynGd<Node, dyn InstanceIdProvider<Id = InstanceId>> =
372+
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
373+
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);
374+
375+
let obj = back.into_gd().upcast::<Object>();
376+
let back: DynGd<Object, dyn InstanceIdProvider<Id = InstanceId>> =
377+
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
378+
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);
379+
380+
back.free()
381+
}
382+
383+
#[itest]
384+
fn dyn_gd_object_conversion_failures() {
385+
// Unregistered trait conversion failure.
386+
trait UnrelatedTrait {}
387+
388+
let node = foreign::NodeHealth::new_alloc().upcast::<Node>();
389+
let original_id = node.instance_id();
390+
let back = node.try_dynify::<dyn UnrelatedTrait>();
391+
let node = back.expect_err("Gd::try_dynify() should have failed");
392+
393+
// `Gd::try_dynify()` should return the original instance on failure, similarly to `Gd::try_cast()`.
394+
assert_eq!(original_id, node.instance_id());
395+
396+
// Unimplemented trait conversion failures.
397+
let back = node.try_dynify::<dyn InstanceIdProvider<Id = i32>>();
398+
let node = back.expect_err("Gd::try_dynify() should have failed");
399+
assert_eq!(original_id, node.instance_id());
400+
401+
let obj = RefCounted::new_gd();
402+
let original_id = obj.instance_id();
403+
let back = obj.try_dynify::<dyn Health>();
404+
let obj = back.expect_err("Gd::try_dynify() should have failed");
405+
assert_eq!(original_id, obj.instance_id());
406+
407+
node.free();
408+
}
409+
345410
#[itest]
346411
fn dyn_gd_store_in_godot_array() {
347412
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn();

0 commit comments

Comments
 (0)