Skip to content

Implement Gd::try_dynify. #1255

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

Merged
merged 1 commit into from
Aug 6, 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
36 changes: 35 additions & 1 deletion godot-core/src/obj/dyn_gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ use std::{fmt, ops};
/// // Now work with the abstract object as usual.
/// ```
///
/// Any `Gd<T>` where `T` is an engine class can attempt conversion to `DynGd<T, D>` with [`Gd::try_dynify()`] as well.
///
/// ```no_run
/// # use godot::prelude::*;
/// # use godot::classes::Node2D;
/// # // ShapeCast2D is marked as experimental and thus not included in the doctests.
/// # // We use this mock to showcase some real-world usage.
/// # struct FakeShapeCastCollider2D {}
///
/// # impl FakeShapeCastCollider2D {
/// # fn get_collider(&self, _idx: i32) -> Option<Gd<Node2D>> { Some(Node2D::new_alloc()) }
/// # }
///
/// trait Pushable { /* ... */ }
///
/// # let my_shapecast = FakeShapeCastCollider2D {};
/// # let idx = 1;
/// // We can try to convert `Gd<T>` into `DynGd<T, D>`.
/// let node: Option<DynGd<Node2D, dyn Pushable>> =
/// my_shapecast.get_collider(idx).and_then(
/// |obj| obj.try_dynify().ok()
/// );
///
/// // An object is returned after failed conversion, similarly to `Gd::try_cast()`.
/// # let some_node = Node::new_alloc();
/// match some_node.try_dynify::<dyn Pushable>() {
/// Ok(dyn_gd) => (),
/// Err(some_node) => godot_warn!("Failed to convert {some_node} into dyn Pushable!"),
/// }
/// ```
///
/// When converting from Godot back into `DynGd`, we say that the `dyn Health` trait object is _re-enriched_.
///
/// godot-rust achieves this thanks to the registration done by `#[godot_dyn]`: the library knows for which classes `Health` is implemented,
Expand Down Expand Up @@ -541,7 +572,10 @@ where
D: ?Sized + 'static,
{
fn try_from_godot(via: Self::Via) -> Result<Self, ConvertError> {
try_dynify_object(via)
match try_dynify_object(via) {
Ok(dyn_gd) => Ok(dyn_gd),
Err((from_godot_err, obj)) => Err(from_godot_err.into_error(obj)),
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions godot-core/src/obj/gd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::obj::{
OnEditor, RawGd, WithSignals,
};
use crate::private::{callbacks, PanicPayload};
use crate::registry::class::try_dynify_object;
use crate::registry::property::{object_export_element_type_string, Export, Var};
use crate::{classes, out};

Expand Down Expand Up @@ -512,6 +513,21 @@ impl<T: GodotClass> Gd<T> {
DynGd::<T, D>::from_gd(self)
}

/// Tries to upgrade to a `DynGd<T, D>` pointer, enabling the `D` abstraction.
///
/// If `T`'s dynamic class doesn't implement `AsDyn<D>`, `Err(self)` is returned, meaning you can reuse the original
/// object for further casts.
pub fn try_dynify<D>(self) -> Result<DynGd<T, D>, Self>
where
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
D: ?Sized + 'static,
{
match try_dynify_object(self) {
Ok(dyn_gd) => Ok(dyn_gd),
Err((_convert_err, obj)) => Err(obj),
}
}

/// Returns a callable referencing a method from this object named `method_name`.
///
/// This is shorter syntax for [`Callable::from_object_method(self, method_name)`][Callable::from_object_method].
Expand Down
8 changes: 4 additions & 4 deletions godot-core/src/registry/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{any, ptr};

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

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

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

Err(error.into_error(object))
Err((error, object))
}

/// 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].
Expand Down
65 changes: 65 additions & 0 deletions itest/rust/src/object_tests/dyn_gd_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,71 @@ fn dyn_gd_variant_conversions() {
node.free();
}

#[itest]
fn dyn_gd_object_conversions() {
let node = foreign::NodeHealth::new_alloc().upcast::<Node>();
let original_id = node.instance_id();

// Convert to different levels of DynGd:
let back: DynGd<Node, dyn Health> = node
.try_dynify()
.expect("Gd::try_dynify() should succeed.")
.cast();
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

let obj = back.into_gd().upcast::<Object>();
let back: DynGd<Object, dyn Health> =
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
assert_eq!(back.dyn_bind().get_hitpoints(), 100);
assert_eq!(back.instance_id(), original_id);

// Back to NodeHealth.
let node = back.cast::<foreign::NodeHealth>();
assert_eq!(node.bind().get_hitpoints(), 100);
assert_eq!(node.instance_id(), original_id);

// Convert to different DynGd.
let obj = node.into_gd().upcast::<Node>();
let back: DynGd<Node, dyn InstanceIdProvider<Id = InstanceId>> =
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);

let obj = back.into_gd().upcast::<Object>();
let back: DynGd<Object, dyn InstanceIdProvider<Id = InstanceId>> =
obj.try_dynify().expect("Gd::try_dynify() should succeed.");
assert_eq!(back.dyn_bind().get_id_dynamic(), original_id);

back.free()
}

#[itest]
fn dyn_gd_object_conversion_failures() {
// Unregistered trait conversion failure.
trait UnrelatedTrait {}

let node = foreign::NodeHealth::new_alloc().upcast::<Node>();
let original_id = node.instance_id();
let back = node.try_dynify::<dyn UnrelatedTrait>();
let node = back.expect_err("Gd::try_dynify() should have failed");

// `Gd::try_dynify()` should return the original instance on failure, similarly to `Gd::try_cast()`.
assert_eq!(original_id, node.instance_id());

// Unimplemented trait conversion failures.
let back = node.try_dynify::<dyn InstanceIdProvider<Id = i32>>();
let node = back.expect_err("Gd::try_dynify() should have failed");
assert_eq!(original_id, node.instance_id());

let obj = RefCounted::new_gd();
let original_id = obj.instance_id();
let back = obj.try_dynify::<dyn Health>();
let obj = back.expect_err("Gd::try_dynify() should have failed");
assert_eq!(original_id, obj.instance_id());

node.free();
}

#[itest]
fn dyn_gd_store_in_godot_array() {
let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn();
Expand Down
Loading