diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 41ddd1d9d..070953a7e 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -553,6 +553,12 @@ pub mod cap { fn __godot_property_get_revert(&self, property: StringName) -> Option; } + #[doc(hidden)] + pub trait DynTrait: GodotClass { + #[doc(hidden)] + fn __register_dyn_traits(); + } + /// Auto-implemented for `#[godot_api] impl MyClass` blocks pub trait ImplementsGodotApi: GodotClass { #[doc(hidden)] diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 543c2b943..023ef8035 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -9,7 +9,8 @@ pub use crate::gen::classes::class_macros; pub use crate::obj::rtti::ObjectRtti; pub use crate::registry::callbacks; pub use crate::registry::plugin::{ - ClassPlugin, ErasedRegisterFn, ErasedRegisterRpcsFn, InherentImpl, PluginItem, + ClassPlugin, ErasedRegisterDynTraitFn, ErasedRegisterFn, ErasedRegisterRpcsFn, InherentImpl, + PluginItem, }; pub use crate::storage::{as_storage, Storage}; pub use sys::out; diff --git a/godot-core/src/registry/callbacks.rs b/godot-core/src/registry/callbacks.rs index d8fcc0411..de461cc11 100644 --- a/godot-core/src/registry/callbacks.rs +++ b/godot-core/src/registry/callbacks.rs @@ -345,6 +345,10 @@ pub fn register_user_properties(_class_builder: T::__register_exports(); } +pub fn register_user_dyn_traits(_class_builder: &mut dyn Any) { + T::__register_dyn_traits(); +} + pub fn register_user_methods_constants(_class_builder: &mut dyn Any) { // let class_builder = class_builder // .downcast_mut::>() diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 0a51785fb..490d9a9e3 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -11,7 +11,7 @@ use std::ptr; use crate::init::InitLevel; use crate::meta::ClassName; use crate::obj::{cap, GodotClass}; -use crate::private::{ClassPlugin, PluginItem}; +use crate::private::{ClassPlugin, ErasedRegisterDynTraitFn, PluginItem}; use crate::registry::callbacks; use crate::registry::plugin::{ErasedRegisterFn, InherentImpl}; use crate::{godot_error, sys}; @@ -47,7 +47,7 @@ struct ClassRegistrationInfo { user_register_fn: Option, default_virtual_fn: sys::GDExtensionClassGetVirtual, // Option (set if there is at least one OnReady field) user_virtual_fn: sys::GDExtensionClassGetVirtual, // Option (set if there is a `#[godot_api] impl I*`) - + register_dyn_trait_fn: Option, /// Godot low-level class creation parameters. #[cfg(before_api = "4.2")] godot_params: sys::GDExtensionClassCreationInfo, @@ -140,6 +140,7 @@ pub fn register_class< init_level: T::INIT_LEVEL, is_editor_plugin: false, component_already_filled: Default::default(), // [false; N] + register_dyn_trait_fn: None, }); } @@ -245,10 +246,12 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { is_instantiable, #[cfg(all(since_api = "4.3", feature = "docs"))] docs: _, + register_dyn_trait_fn, } => { c.parent_class_name = Some(base_class_name); c.default_virtual_fn = default_get_virtual_fn; c.register_properties_fn = Some(register_properties_fn); + c.register_dyn_trait_fn = Some(register_dyn_trait_fn); c.is_editor_plugin = is_editor_plugin; // Classes marked #[class(no_init)] are translated to "abstract" in Godot. This disables their default constructor. @@ -437,6 +440,10 @@ fn register_class_raw(mut info: ClassRegistrationInfo) { (register_fn.raw)(&mut class_builder); } + if let Some(register_dyn_trait) = info.register_dyn_trait_fn { + (register_dyn_trait.raw)(&mut class_builder); + } + if info.is_editor_plugin { unsafe { interface_fn!(editor_add_plugin)(class_name.string_sys()) }; } @@ -484,6 +491,7 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo { user_register_fn: None, default_virtual_fn: None, user_virtual_fn: None, + register_dyn_trait_fn: None, godot_params: default_creation_info(), init_level: InitLevel::Scene, is_editor_plugin: false, diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 55971fcbc..4e5379bef 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -44,6 +44,17 @@ impl fmt::Debug for ErasedRegisterFn { } } +#[derive(Copy, Clone)] +pub struct ErasedRegisterDynTraitFn { + pub raw: fn(&mut dyn Any), +} + +impl fmt::Debug for ErasedRegisterDynTraitFn { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:0>16x}", self.raw as usize) + } +} + #[derive(Copy, Clone)] pub struct ErasedRegisterRpcsFn { pub raw: fn(&mut dyn Any), @@ -96,6 +107,8 @@ pub enum PluginItem { /// Callback to library-generated function which registers properties in the `struct` definition. register_properties_fn: ErasedRegisterFn, + register_dyn_trait_fn: ErasedRegisterDynTraitFn, + free_fn: unsafe extern "C" fn( _class_user_data: *mut std::ffi::c_void, instance: sys::GDExtensionClassInstancePtr, diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index 55ef01a96..2e5992bdd 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -119,6 +119,8 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let is_tool = struct_cfg.is_tool; + let dyn_trait_impl = make_dyn_trait_impl(struct_cfg.dyn_traits, class_name); + Ok(quote! { impl ::godot::obj::GodotClass for #class_name { type Base = #base_class; @@ -147,6 +149,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #godot_exports_impl #user_class_impl #init_expecter + #dyn_trait_impl #( #deprecations )* ::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin { @@ -158,6 +161,9 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { register_properties_fn: #prv::ErasedRegisterFn { raw: #prv::callbacks::register_user_properties::<#class_name>, }, + register_dyn_trait_fn: #prv::ErasedRegisterDynTraitFn { + raw: #prv::callbacks::register_user_dyn_traits::<#class_name>, + }, free_fn: #prv::callbacks::free::<#class_name>, default_get_virtual_fn: #default_get_virtual_fn, is_tool: #is_tool, @@ -213,6 +219,7 @@ struct ClassAttributes { is_internal: bool, rename: Option, deprecations: Vec, + dyn_traits: Vec, } impl ClassAttributes { @@ -250,6 +257,29 @@ fn make_godot_init_impl(class_name: &Ident, fields: &Fields) -> TokenStream { } } +fn make_dyn_trait_impl(dyn_traits: Vec, class_name: &Ident) -> TokenStream { + // note – we assume that methods "register_traitname_dispatch" are explicitly imported by the user. It is common approach among such cases, for example see Enum Dispatch + let register_fns: Vec = dyn_traits + .iter() + .map(|t| { + let ident = ident(&format!( + "register_{}_dispatch", + t.to_string().to_lowercase() + )); + quote! { + #ident::<#class_name>() + } + }) + .collect(); + quote! { + impl ::godot::obj::cap::DynTrait for #class_name { + fn __register_dyn_traits() { + #(#register_fns);* + } + } + } +} + fn make_user_class_impl( class_name: &Ident, is_tool: bool, @@ -335,6 +365,7 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult = None; let mut deprecations = vec![]; + let mut dyn_traits = vec![]; // #[class] attribute on struct if let Some(mut parser) = KvParser::parse(&class.attributes, "class")? { @@ -382,6 +413,12 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult ParseResult, +} + +/// Creates non-dispatchable method for our wrapper. +/// Generated method on wrapper panics on use and informs user why they can't dynamically dispatch a method explicitly marked as non-dispatchable +fn create_non_dispatchable_method(signature: SignatureInfo) -> TokenStream { + let method_name = signature.method_name; + let ret = signature.ret_type; + let function_params: Vec = signature + .param_types + .iter() + .zip(signature.param_idents.iter()) + .map(|(ident, param)| quote! {#param: #ident}) + .collect(); + let receiver = match signature.receiver_type { + ReceiverType::Ref => quote! {&self}, + ReceiverType::Mut => quote! {&mut self}, + ReceiverType::Static => TokenStream::default(), + _ => unreachable!(), + }; + let panic_message = format!( + "error: the {} method cannot be invoked on a trait object", + method_name + ); + quote! { + fn #method_name(#receiver #(#function_params),*) -> #ret { + godot_error!(#panic_message); + panic!(#panic_message) + } + } +} + +/// Codegen for methods on our wrapper and dispatched closures +fn create_dispatch_method( + signature: SignatureInfo, + base_ty: &Ident, + trait_name: &Ident, +) -> ParseResult<(TokenStream, TokenStream, TokenStream)> { + let dispatch_func_name = ident(&format!("dispatch_{}", signature.method_name)); + let ret = signature.ret_type; + let param_types = signature.param_types; + let param_idents = signature.param_idents; + let method_name = signature.method_name; + let function_params: Vec = param_types + .iter() + .zip(param_idents.iter()) + .map(|(ident, param)| quote! {#param: #ident}) + .collect(); + let (mutability, receiver, bind) = match signature.receiver_type { + ReceiverType::Ref => ( + quote! { & }, + quote! { &self, }, + quote! { + let instance = base.cast::(); + let guard: GdRef = instance.bind(); + }, + ), + ReceiverType::Mut => ( + quote! { &mut }, + quote! { &mut self, }, + quote! { + let mut instance = base.cast::(); + let mut guard: GdMut = instance.bind_mut(); + }, + ), + _ => { + // return proper error to an user. + return bail!( + &method_name, + "error[E0038]: the trait cannot be made into an object.\n \ + note: for a trait to be \"object safe\" it needs to allow \ + building a vtable to allow the call to be resolvable dynamically; \ + for more information visit " + ); + } + }; + let fields = quote! {#dispatch_func_name: fn(Gd<#base_ty>, #(#param_types,)* fn(#mutability dyn #trait_name, #(#param_types),*) -> #ret) -> #ret}; + let declarations = quote! { + #dispatch_func_name: |base, #(#param_idents,)* closure| { + #bind + closure(#mutability *guard, #(#param_idents),*) + } + }; + let methods = quote! { + fn #method_name(#receiver #(#function_params),*) -> #ret { + unsafe {((*self.dispatch).#dispatch_func_name)(self.base.clone(), #(#param_idents,)* |dispatch: #mutability dyn #trait_name, #(#param_idents,)*| {dispatch.#method_name(#(#param_idents),*)})} + } + }; + + Ok((fields, declarations, methods)) +} + +/// Naive check for where Self: Sized +fn is_function_sized(f: &mut venial::Function) -> bool { + if let Some(where_clause) = f.where_clause.as_ref() { + return where_clause.items.items().any(|i| { + let left_side = i + .left_side + .iter() + .fold(String::new(), |acc, arg| format!("{acc}{arg}")); + if left_side != "Self" { + return false; + }; + i.bound.tokens.iter().any(|tt| { + let TokenTree::Ident(i) = tt else { + return false; + }; + *i == "Sized" + }) + }); + }; + false +} + +/// Checks if given associated function is explicitly non-dispatchable +/// Explicitly non-dispatchable functions have a `where Self: Sized` bound. +fn check_if_dispatchable( + f: &mut venial::Function, + signature_info: SignatureInfo, +) -> DynTraitMethod { + if is_function_sized(f) { + // This trait is object-safe, but this method can't be dispatched on a trait object. + return DynTraitMethod::NonDispatchable(signature_info); + } + DynTraitMethod::Dispatchable(signature_info) +} + +/// Creates signature for given function and checks if it is marked as non-dispatchable. +fn parse_associated_function(f: &mut venial::Function) -> DynTraitMethod { + let mut receiver_type: ReceiverType = ReceiverType::Static; + let signature = util::reduce_to_signature(f); + let num_params = signature.params.len(); + let mut param_idents = Vec::with_capacity(num_params); + let mut param_types = Vec::with_capacity(num_params); + + let ret_type = match signature.return_ty { + None => quote! { () }, + Some(ty) => ty.to_token_stream(), + }; + + for (arg, _) in signature.params.inner { + match arg { + venial::FnParam::Receiver(recv) => { + receiver_type = if recv.tk_mut.is_some() { + ReceiverType::Mut + } else if recv.tk_ref.is_some() { + ReceiverType::Ref + } else { + // Receiver is not present at all. + unreachable!() + }; + } + venial::FnParam::Typed(arg) => { + let ty = venial::TypeExpr { + tokens: arg.ty.tokens, + }; + + param_types.push(ty); + param_idents.push(arg.name); + } + } + } + + // we need to provide correct signature for given trait method + // even if it is not dispatchable. + let signature = SignatureInfo { + method_name: signature.name, + receiver_type, + param_idents, + param_types, + ret_type, + }; + check_if_dispatchable(f, signature) +} + +pub fn attribute_dyn_trait(input_decl: venial::Item) -> ParseResult { + let venial::Item::Trait(mut decl) = input_decl.clone() else { + bail!( + input_decl, + "#[dyn_trait] can only be applied on trait blocks", + )? + }; + + let trait_name = decl.name.clone(); + let dispatch_name = ident(&format! {"{}GdDispatch", decl.name}); + let mut wrapper_name = ident(&format!("{}GdDyn", decl.name)); + let mut base_ty = ident("Object"); + let attr = std::mem::take(&mut decl.attributes); + + // #[dyn_trait] + if let Some(mut parser) = KvParser::parse(&attr, "dyn_trait")? { + if let Ok(Some(name)) = parser.handle_ident("name") { + wrapper_name = name; + }; + if let Ok(Some(base)) = parser.handle_ident("base") { + base_ty = base; + }; + } + + let mut trait_dispatch = DynTraitDispatch { + base_ty, + trait_name, + dispatch_name, + wrapper_name, + methods_signatures: vec![], + }; + + for trait_member in decl.body_items.iter_mut() { + let venial::TraitMember::AssocFunction(f) = trait_member else { + bail!( + trait_member, + "error[E0038]: the trait cannot be made into an object\n\ + note: for a trait to be \"object safe\" it needs to allow building a vtable to allow the call to be resolvable dynamically; \ + for more information visit " + )? + }; + trait_dispatch + .methods_signatures + .push(parse_associated_function(f)); + } + create_dyn_trait_dispatch(trait_dispatch, decl.to_token_stream()) +} + +/// Codegen for `#[dyn_trait] for trait MyTrait` +fn create_dyn_trait_dispatch( + s: DynTraitDispatch, + initial: TokenStream, +) -> ParseResult { + let DynTraitDispatch { + base_ty, + trait_name, + dispatch_name, + wrapper_name, + methods_signatures, + } = s; + let registry_name = ident(&format!( + "{}_DISPATCH_REGISTRY", + trait_name.to_string().to_uppercase() + )); + let register_dispatch_name = ident(&format!( + "register_{}_dispatch", + trait_name.to_string().to_lowercase() + )); + let mut dispatch_fields: Vec = vec![]; + let mut dispatch_declarations: Vec = vec![]; + let mut wrapper_methods: Vec = vec![]; + + for signature in methods_signatures.into_iter() { + match signature { + DynTraitMethod::NonDispatchable(sig) => { + wrapper_methods.push(create_non_dispatchable_method(sig)); + } + DynTraitMethod::Dispatchable(sig) => { + let (fields, declarations, methods) = + create_dispatch_method(sig, &base_ty, &trait_name)?; + dispatch_fields.push(fields); + dispatch_declarations.push(declarations); + wrapper_methods.push(methods); + } + } + } + + let ret = quote! { + #initial + + static #registry_name: godot::sys::Global> = godot::sys::Global::default(); + + pub fn #register_dispatch_name() + where + T: Inherits<#base_ty> + GodotClass + godot::obj::Bounds + #trait_name + { + let name = T::class_name().to_string(); + let mut registry = #registry_name.lock(); + registry.entry(name).or_insert_with( + || #dispatch_name::new::() + ); + } + + struct #dispatch_name { + #(#dispatch_fields),* + } + + + impl #dispatch_name { + fn new() -> Self + where + T: Inherits<#base_ty> + GodotClass + godot::obj::Bounds + #trait_name + { + Self { + #(#dispatch_declarations),* + } + } + } + + #[derive(Debug)] + pub struct #wrapper_name { + pub base: Gd<#base_ty>, + dispatch: *const #dispatch_name + } + + impl #wrapper_name { + pub fn new(base: Gd<#base_ty>) -> Result { + let registry = #registry_name.lock(); + let Some(dispatch) = registry.get(&base.get_class().to_string()) else { + return Err("Given class is not registered as dynTrait object!") + }; + Ok(Self { + base, + dispatch: dispatch as *const #dispatch_name + }) + } + } + + impl #trait_name for #wrapper_name { + #(#wrapper_methods)* + } + + impl GodotConvert for #wrapper_name { + type Via = Gd<#base_ty>; + } + + impl ToGodot for #wrapper_name { + type ToVia<'v> = Gd<#base_ty> + where Self: 'v; + fn to_godot(&self) -> Self::ToVia<'_> { + self.base.clone() + } + } + + impl FromGodot for #wrapper_name { + fn try_from_godot(via: Self::Via) -> Result { + match #wrapper_name::new(via) { + Ok(s) => Ok(s), + Err(message) => Err(ConvertError::new(message)) + } + } + } + + }; + Ok(ret) +} diff --git a/godot-macros/src/class/mod.rs b/godot-macros/src/class/mod.rs index 7f800c137..2609aafa2 100644 --- a/godot-macros/src/class/mod.rs +++ b/godot-macros/src/class/mod.rs @@ -6,7 +6,9 @@ */ mod derive_godot_class; +mod dyn_trait; mod godot_api; + mod data_models { pub mod constant; pub mod field; @@ -32,4 +34,5 @@ pub(crate) use data_models::property::*; pub(crate) use data_models::rpc::*; pub(crate) use data_models::signal::*; pub(crate) use derive_godot_class::*; +pub(crate) use dyn_trait::*; pub(crate) use godot_api::*; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 414b77176..42c3aa493 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -684,6 +684,52 @@ pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream { translate(input, class::attribute_godot_api) } +/// Proc-macro attribute to be used with `trait` blocks that allows to use user-defined `GodotClass` as Trait Objects. +/// +/// ```no_run +/// # use godot::prelude::*; +/// +/// #[dyn_trait(name=MyTraitObjectName, base=RefCounted)] +/// trait GdDynTrait { +/// fn method(&self) -> GString; +/// fn non_dispatchable_method() where Self: Sized; +/// } +/// +/// #[derive(GodotClass)] +/// #[class(init, dyn_trait = (GdDynTrait))] +/// struct MyDynStruct { +/// field: i64, +/// base: Base, +/// } +/// +/// impl GdDynTrait for MyDynStruct { +/// fn method(&self) -> GString { +/// return GString::from("I am dynamic!"); +/// } +/// fn non_dispatchable_method() { +/// godot_print!("I can't be dispatched!"); +/// } +/// } +/// +///#[derive(GodotClass)] +/// #[class(init)] +/// struct OtherStruct { +/// base: Base, +/// } +/// #[godot_api] +/// impl OtherStruct { +/// #[func] +/// fn some_method(&self, other: MyTraitObjectName) { +/// godot_print!("hello {}", other.method()); +/// } +/// } +/// +/// ``` +#[proc_macro_attribute] +pub fn dyn_trait(meta: TokenStream, input: TokenStream) -> TokenStream { + translate_meta("dyn_trait", meta, input, class::attribute_dyn_trait) +} + /// Derive macro for [`GodotConvert`](../builtin/meta/trait.GodotConvert.html) on structs. /// /// This derive macro also derives [`ToGodot`](../builtin/meta/trait.ToGodot.html) and [`FromGodot`](../builtin/meta/trait.FromGodot.html). diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 62bebfd08..98d232499 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -173,7 +173,7 @@ pub mod init { /// Register/export Rust symbols to Godot: classes, methods, enums... pub mod register { pub use godot_core::registry::property; - pub use godot_macros::{godot_api, Export, GodotClass, GodotConvert, Var}; + pub use godot_macros::{dyn_trait, godot_api, Export, GodotClass, GodotConvert, Var}; /// Re-exports used by proc-macro API. #[doc(hidden)] diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 20cfe544b..962330db0 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -8,7 +8,7 @@ pub use super::register::property::{Export, Var}; // Re-export macros. -pub use super::register::{godot_api, Export, GodotClass, GodotConvert, Var}; +pub use super::register::{dyn_trait, godot_api, Export, GodotClass, GodotConvert, Var}; pub use super::builtin::__prelude_reexport::*; pub use super::builtin::math::FloatExt as _;