Skip to content

match_class! now supports dynamic dispatch (@ dyn Trait pattern). #1256

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
52 changes: 51 additions & 1 deletion godot-core/src/classes/match_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
/// # // Hack to keep amount of SELECTED_CLASSES limited:
/// # type InputEventMouseButton = InputEventAction;
/// # type InputEventMouseMotion = InputEventAction;
/// # type InputEventKey = InputEventAction;
/// # trait InputEventTrait: 'static {}
/// // Basic syntax.
/// let event: Gd<InputEvent> = some_input();
///
/// let simple_dispatch: i32 = match_class! { event,
/// button @ InputEventMouseButton => 1,
/// motion @ InputEventMouseMotion => 2,
/// action @ InputEventAction => 3,
/// key @ InputEventKey => 4,
/// _ => 0, // Fallback.
/// };
///
Expand All @@ -50,14 +53,21 @@
/// // Qualified types supported:
/// action @ godot::classes::InputEventAction => 3,
///
/// // If you're only interested in the class and not the object,
/// // discard it with either `_` or `_variable`:
/// _ @ InputEventKey => 4,
///
/// // Dynamic dispatch supported:
/// dynamic @ dyn InputEventTrait => 5,
///
/// // Fallback with variable -- retrieves original Gd<InputEvent>.
/// original => 0,
/// // Can also be used with mut:
/// // mut original => 0,
/// // If the match arms have type (), we can also omit the fallback branch.
/// };
///
/// // event_type is now 0, 1, 2, or 3
/// // event_type is now 0, 1, 2, 3, 4 or 5.
/// ```
///
/// # Expression and control flow
Expand All @@ -79,6 +89,16 @@ macro_rules! match_class {
#[doc(hidden)]
#[macro_export]
macro_rules! match_class_muncher {
// mut variable @ dyn Trait => { ... }.
($subject:ident, mut $var:ident @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{
match $subject.try_dynify::<dyn $Tr>() {
Ok(mut $var) => $block,
Err(__obj) => {
$crate::match_class_muncher!(__obj, $($rest)*)
}
}
}};

// mut variable @ Class => { ... }.
($subject:ident, mut $var:ident @ $Ty:ty => $block:expr, $($rest:tt)*) => {{
match $subject.try_cast::<$Ty>() {
Expand All @@ -89,6 +109,16 @@ macro_rules! match_class_muncher {
}
}};

// variable @ dyn Trait => { ... }.
($subject:ident, $var:ident @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{
match $subject.try_dynify::<dyn $Tr>() {
Ok($var) => $block,
Err(__obj) => {
$crate::match_class_muncher!(__obj, $($rest)*)
}
}
}};

// variable @ Class => { ... }.
($subject:ident, $var:ident @ $Ty:ty => $block:expr, $($rest:tt)*) => {{
match $subject.try_cast::<$Ty>() {
Expand All @@ -99,6 +129,26 @@ macro_rules! match_class_muncher {
}
}};

// _ @ dyn Trait => { ... }.
($subject:ident, _ @ dyn $Tr:path => $block:expr, $($rest:tt)*) => {{
match $subject.try_dynify::<dyn $Tr>() {
Ok(_) => $block,
Err(__obj) => {
$crate::match_class_muncher!(__obj, $($rest)*)
}
}
}};

// _ @ Class => { ... }.
($subject:ident, _ @ $Ty:ty => $block:expr, $($rest:tt)*) => {{
match $subject.try_cast::<$Ty>() {
Ok(_) => $block,
Err(__obj) => {
$crate::match_class_muncher!(__obj, $($rest)*)
}
}
}};

// mut variable => { ... }.
($subject:ident, mut $var:ident => $block:expr $(,)?) => {{
let mut $var = $subject;
Expand Down
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
115 changes: 113 additions & 2 deletions itest/rust/src/engine_tests/match_class_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ fn match_class_basic_mut_dispatch() {
to_free.free();
}

#[itest]
fn match_class_basic_unnamed_dispatch() {
let node3d = Node3D::new_alloc();
let obj: Gd<Object> = node3d.upcast();
let to_free = obj.clone();

let result = match_class! { obj,
node @ Node2D => {
require_node2d(&node);
1
},
_ @ Node3D => 2,
node @ Node => {
require_node(&node);
3
},
_ => 4 // No comma.
};

assert_eq!(result, 2);
to_free.free();
}

#[itest]
fn match_class_shadowed_by_more_general() {
let node2d = Node2D::new_alloc();
Expand Down Expand Up @@ -172,13 +195,16 @@ fn match_class_unit_type() {
let mut val = 0;

match_class! { obj,
_ @ Node3D => {
val = 1;
},
mut node @ Node2D => {
require_mut_node2d(&mut node);
val = 1;
val = 2;
},
node @ Node => {
require_node(&node);
val = 2;
val = 3;
},
// No need for _ branch since all branches return ().
}
Expand All @@ -191,3 +217,88 @@ fn match_class_unit_type() {
// Nothing.
};
}

#[itest]
fn match_class_dyn_dispatch() {
// Test complex inline expression.
let result = match_class! {
ExampleRefCounted2::new_gd().upcast::<RefCounted>(),
ref_counted_1 @ dyn ExampleTraitFetch1 => ref_counted_1.dyn_bind().fetch(),
ref_counted_2 @ dyn ExampleTraitFetch2 => ref_counted_2.dyn_bind().fetch(),
_ignored => 3,
};

assert_eq!(result, 2);
}

#[itest]
fn match_class_mut_dyn_dispatch() {
// Test complex inline expression.
let mut result = 0;
match_class! {
ExampleRefCounted1::new_gd().upcast::<RefCounted>(),
mut ref_counted_1 @ dyn ExampleTraitMut1 => ref_counted_1.dyn_bind_mut().mutate(&mut result),
mut ref_counted_2 @ dyn ExampleTraitMut2 => ref_counted_2.dyn_bind_mut().mutate(&mut result),
};

assert_eq!(result, 1);
}

#[itest]
fn match_class_unnamed_dyn_dispatch() {
// Test complex inline expression.
let result = match_class! {
ExampleRefCounted1::new_gd().upcast::<RefCounted>(),
_ @ dyn ExampleTraitFetch1 => 1,
ref_counted_2 @ dyn ExampleTraitFetch2 => ref_counted_2.dyn_bind().fetch(),
_ignored => 3,
};

assert_eq!(result, 1);
}

// Example traits and nodes to use in the match class dynify testing.

trait ExampleTraitFetch1: 'static {
fn fetch(&self) -> i32 {
1
}
}

trait ExampleTraitFetch2: 'static {
fn fetch(&self) -> i32 {
2
}
}

trait ExampleTraitMut1: 'static {
fn mutate(&mut self, value: &mut i32) {
*value = 1;
}
}

trait ExampleTraitMut2: 'static {
fn mutate(&mut self, value: &mut i32) {
*value = 2;
}
}

#[derive(GodotClass)]
#[class(init)]
struct ExampleRefCounted1 {}

#[godot_dyn]
impl ExampleTraitFetch1 for ExampleRefCounted1 {}

#[godot_dyn]
impl ExampleTraitMut1 for ExampleRefCounted1 {}

#[derive(GodotClass)]
#[class(init)]
struct ExampleRefCounted2 {}

#[godot_dyn]
impl ExampleTraitFetch2 for ExampleRefCounted2 {}

#[godot_dyn]
impl ExampleTraitMut2 for ExampleRefCounted2 {}
Loading