diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index db24f4291..39c54f461 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -109,6 +109,22 @@ use sys::{ffi_methods, interface_fn, GodotFfi}; /// compiler will enforce this as long as you use only Rust threads, but it cannot protect against /// concurrent modification on other threads (e.g. created through GDScript). /// +/// # Element type safety +/// +/// We provide a richer set of element types than Godot, for convenience and stronger invariants in your _Rust_ code. +/// This, however, means that the Godot representation of such arrays is not capable of incorporating the additional "Rust-side" information. +/// This can lead to situations where GDScript code or the editor UI can insert values that do not fulfill the Rust-side invariants. +/// The library offers some best-effort protection in Debug mode, but certain errors may only occur on element access, in the form of panics. +/// +/// Concretely, the following types lose type information when passed to Godot. If you want 100% bullet-proof arrays, avoid those. +/// - Non-`i64` integers: `i8`, `i16`, `i32`, `u8`, `u16`, `u32`. (`u64` is unsupported). +/// - Non-`f64` floats: `f32`. +/// - Non-null objects: [`Gd`][crate::obj::Gd]. +/// Godot generally allows `null` in arrays due to default-constructability, e.g. when using `resize()`. +/// The Godot-faithful (but less convenient) alternative is to use `Option>` element types. +/// - Objects with dyn-trait association: [`DynGd`][crate::obj::DynGd]. +/// Godot doesn't know Rust traits and will only see the `T` part. +/// /// # Godot docs /// /// [`Array[T]` (stable)](https://docs.godotengine.org/en/stable/classes/class_array.html) diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index c8adabcce..ee7bd5450 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -7,7 +7,7 @@ //! Runtime checks and inspection of Godot classes. -use crate::builtin::GString; +use crate::builtin::{GString, StringName}; use crate::classes::{ClassDb, Object}; use crate::meta::{CallContext, ClassName}; use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId}; @@ -19,13 +19,27 @@ pub(crate) fn debug_string( ty: &str, ) -> std::fmt::Result { if let Some(id) = obj.instance_id_or_none() { - let class: GString = obj.raw.as_object().get_class(); + let class: StringName = obj.dynamic_class_string(); write!(f, "{ty} {{ id: {id}, class: {class} }}") } else { write!(f, "{ty} {{ freed obj }}") } } +pub(crate) fn debug_string_with_trait( + obj: &Gd, + f: &mut std::fmt::Formatter<'_>, + ty: &str, + trt: &str, +) -> std::fmt::Result { + if let Some(id) = obj.instance_id_or_none() { + let class: StringName = obj.dynamic_class_string(); + write!(f, "{ty} {{ id: {id}, class: {class}, trait: {trt} }}") + } else { + write!(f, "{ty} {{ freed obj }}") + } +} + pub(crate) fn display_string( obj: &Gd, f: &mut std::fmt::Formatter<'_>, diff --git a/godot-core/src/meta/error/call_error.rs b/godot-core/src/meta/error/call_error.rs index af99c059c..65bb5aa9c 100644 --- a/godot-core/src/meta/error/call_error.rs +++ b/godot-core/src/meta/error/call_error.rs @@ -188,9 +188,8 @@ impl CallError { expected: VariantType, ) -> Self { // Note: reason is same wording as in FromVariantError::description(). - let reason = format!( - "parameter #{param_index} conversion -- expected type {expected:?}, got {actual:?}" - ); + let reason = + format!("parameter #{param_index} -- cannot convert from {actual:?} to {expected:?}"); Self::new(call_ctx, reason, None) } diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 136e74589..24e8eede6 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -181,6 +181,15 @@ pub(crate) enum FromGodotError { /// InvalidEnum is also used by bitfields. InvalidEnum, + /// Cannot map object to `dyn Trait` because none of the known concrete classes implements it. + UnimplementedDynTrait { + trait_name: String, + class_name: String, + }, + + /// Cannot map object to `dyn Trait` because none of the known concrete classes implements it. + UnregisteredDynTrait { trait_name: String }, + /// `InstanceId` cannot be 0. ZeroInstanceId, } @@ -235,6 +244,21 @@ impl fmt::Display for FromGodotError { } Self::InvalidEnum => write!(f, "invalid engine enum value"), Self::ZeroInstanceId => write!(f, "`InstanceId` cannot be 0"), + Self::UnimplementedDynTrait { + trait_name, + class_name, + } => { + write!( + f, + "none of the classes derived from `{class_name}` have been linked to trait `{trait_name}` with #[godot_dyn]" + ) + } + FromGodotError::UnregisteredDynTrait { trait_name } => { + write!( + f, + "trait `{trait_name}` has not been registered with #[godot_dyn]" + ) + } } } } @@ -313,11 +337,11 @@ impl fmt::Display for FromVariantError { match self { Self::BadType { expected, actual } => { // Note: wording is the same as in CallError::failed_param_conversion_engine() - write!(f, "expected type {expected:?}, got {actual:?}") + write!(f, "cannot convert from {actual:?} to {expected:?}") } Self::BadValue => write!(f, "value cannot be represented in target type's domain"), Self::WrongClass { expected } => { - write!(f, "expected class {expected}") + write!(f, "cannot convert to class {expected}") } } } diff --git a/godot-core/src/meta/sealed.rs b/godot-core/src/meta/sealed.rs index 38906c845..37ab1b08b 100644 --- a/godot-core/src/meta/sealed.rs +++ b/godot-core/src/meta/sealed.rs @@ -11,7 +11,7 @@ use crate::builtin::*; use crate::meta; use crate::meta::traits::{ArrayElement, GodotNullableFfi, GodotType}; -use crate::obj::{Gd, GodotClass, RawGd}; +use crate::obj::{DynGd, Gd, GodotClass, RawGd}; pub trait Sealed {} impl Sealed for Aabb {} @@ -64,6 +64,7 @@ impl Sealed for Variant {} impl Sealed for Array {} impl Sealed for Gd {} impl Sealed for RawGd {} +impl Sealed for DynGd {} impl Sealed for meta::ObjectArg {} impl Sealed for Option where diff --git a/godot-core/src/obj/dyn_gd.rs b/godot-core/src/obj/dyn_gd.rs index 55809f9f1..0663a16dc 100644 --- a/godot-core/src/obj/dyn_gd.rs +++ b/godot-core/src/obj/dyn_gd.rs @@ -5,9 +5,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::builtin::Variant; +use crate::meta::error::ConvertError; +use crate::meta::{FromGodot, GodotConvert, ToGodot}; use crate::obj::guards::DynGdRef; use crate::obj::{bounds, AsDyn, Bounds, DynGdMut, Gd, GodotClass, Inherits}; -use std::ops; +use crate::registry::class::try_dynify_object; +use crate::{meta, sys}; +use std::{fmt, ops}; /// Smart pointer integrating Rust traits via `dyn` dispatch. /// @@ -77,6 +82,24 @@ use std::ops; /// guard.deal_damage(120); /// assert!(!guard.is_alive()); /// ``` +/// +/// # Polymorphic `dyn` re-enrichment +/// +/// When passing `DynGd` to Godot, you will lose the `D` part of the type inside the engine, because Godot doesn't know about Rust traits. +/// The trait methods won't be accessible through GDScript, either. +/// +/// If you now receive the same object back from Godot, you can easily obtain it as `Gd` -- but what if you need the original `DynGd`? +/// If `T` is concrete (i.e. directly implements `D`), then [`Gd::into_dyn()`] is of course possible. But in reality, you may have a polymorphic +/// base class such as `RefCounted` and want to ensure that trait object `D` dispatches to the correct subclass, without manually checking every +/// possible candidate. +/// +/// To stay with the above example: let's say `Health` is implemented for both `Monster` and `Knight` classes. You now receive a +/// `DynGd`, which can represent either of the two classes. How can this work without trying to downcast to both? +/// +/// godot-rust has a mechanism to re-enrich the `DynGd` with the correct trait object. Thanks to `#[godot_dyn]`, the library knows for which +/// classes `Health` is implemented, and it can query the dynamic type of the object. Based on that type, it can find the `impl Health` +/// implementation matching the correct class. Behind the scenes, everything is wired up correctly so that you can restore the original `DynGd` +/// even after it has passed through Godot. pub struct DynGd where // T does _not_ require AsDyn here. Otherwise, it's impossible to upcast (without implementing the relation for all base classes). @@ -130,7 +153,7 @@ where // Certain methods "overridden" from deref'ed Gd here, so they're more idiomatic to use. // Those taking self by value, like free(), must be overridden. - /// Upcast to a Godot base, while retaining the `D` trait object. + /// **Upcast** to a Godot base, while retaining the `D` trait object. /// /// This is useful when you want to gather multiple objects under a common Godot base (e.g. `Node`), but still enable common functionality. /// The common functionality is still accessible through `D` even when upcasting. @@ -147,6 +170,68 @@ where } } + /// **Downcast** to a more specific Godot class, while retaining the `D` trait object. + /// + /// If `T`'s dynamic type is not `Derived` or one of its subclasses, `Err(self)` is returned, meaning you can reuse the original + /// object for further casts. + /// + /// See also [`Gd::try_cast()`]. + pub fn try_cast(self) -> Result, Self> + where + Derived: Inherits, + { + match self.obj.try_cast::() { + Ok(obj) => Ok(DynGd { + obj, + erased_obj: self.erased_obj, + }), + Err(obj) => Err(DynGd { + obj, + erased_obj: self.erased_obj, + }), + } + } + + /// ⚠️ **Downcast:** to a more specific Godot class, while retaining the `D` trait object. + /// + /// See also [`Gd::cast()`]. + /// + /// # Panics + /// If the class' dynamic type is not `Derived` or one of its subclasses. Use [`Self::try_cast()`] if you want to check the result. + pub fn cast(self) -> DynGd + where + Derived: Inherits, + { + self.try_cast().unwrap_or_else(|from_obj| { + panic!( + "downcast from {from} to {to} failed; instance {from_obj:?}", + from = T::class_name(), + to = Derived::class_name(), + ) + }) + } + + /// Unsafe fast downcasts, no trait bounds. + /// + /// # Safety + /// The caller must ensure that the dynamic type of the object is `Derived` or a subclass of `Derived`. + // Not intended for public use. The lack of bounds simplifies godot-rust implementation, but adds another unsafety layer. + #[deny(unsafe_op_in_unsafe_fn)] + pub(crate) unsafe fn cast_unchecked(self) -> DynGd + where + Derived: GodotClass, + { + let cast_obj = self.obj.owned_cast::(); + + // SAFETY: ensured by safety invariant. + let cast_obj = unsafe { cast_obj.unwrap_unchecked() }; + + DynGd { + obj: cast_obj, + erased_obj: self.erased_obj, + } + } + /// Downgrades to a `Gd` pointer, abandoning the `D` abstraction. #[must_use] pub fn into_gd(self) -> Gd { @@ -159,6 +244,9 @@ where T: GodotClass + Bounds, D: ?Sized, { + /// Destroy the manually-managed Godot object. + /// + /// See [`Gd::free()`] for semantics and panics. pub fn free(self) { self.obj.free() } @@ -178,6 +266,37 @@ where } } +impl PartialEq for DynGd +where + T: GodotClass, + D: ?Sized, +{ + fn eq(&self, other: &Self) -> bool { + self.obj == other.obj + } +} + +impl Eq for DynGd +where + T: GodotClass, + D: ?Sized, +{ +} + +impl std::hash::Hash for DynGd +where + T: GodotClass, + D: ?Sized, +{ + /// ⚠️ Hashes this object based on its instance ID. + /// + /// # Panics + /// When `self` is dead. + fn hash(&self, state: &mut H) { + self.obj.hash(state); + } +} + impl ops::Deref for DynGd where T: GodotClass, @@ -200,6 +319,27 @@ where } } +impl fmt::Debug for DynGd +where + T: GodotClass, + D: ?Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let trt = sys::short_type_name::(); + crate::classes::debug_string_with_trait::(self, f, "DynGd", &trt) + } +} + +impl fmt::Display for DynGd +where + T: GodotClass, + D: ?Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + crate::classes::display_string(self, f) + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Type erasure @@ -229,4 +369,77 @@ where } // ---------------------------------------------------------------------------------------------------------------------------------------------- -// Integration with Godot traits +// Integration with Godot traits -- most are directly delegated to Gd. + +impl GodotConvert for DynGd +where + T: GodotClass, + D: ?Sized, +{ + type Via = Gd; +} + +impl ToGodot for DynGd +where + T: GodotClass, + D: ?Sized, +{ + type ToVia<'v> + = as ToGodot>::ToVia<'v> + where + D: 'v; + + fn to_godot(&self) -> Self::ToVia<'_> { + self.obj.to_godot() + } + + fn to_variant(&self) -> Variant { + self.obj.to_variant() + } +} + +impl FromGodot for DynGd +where + T: GodotClass, + D: ?Sized + 'static, +{ + fn try_from_godot(via: Self::Via) -> Result { + try_dynify_object(via) + } +} + +impl<'r, T, D> meta::AsArg> for &'r DynGd +where + T: GodotClass, + D: ?Sized + 'static, +{ + fn into_arg<'cow>(self) -> meta::CowArg<'cow, DynGd> + where + 'r: 'cow, // Original reference must be valid for at least as long as the returned cow. + { + meta::CowArg::Borrowed(self) + } +} + +impl meta::ParamType for DynGd +where + T: GodotClass, + D: ?Sized + 'static, +{ + type Arg<'v> = meta::CowArg<'v, DynGd>; + + fn owned_to_arg<'v>(self) -> Self::Arg<'v> { + meta::CowArg::Owned(self) + } + + fn arg_to_ref<'r>(arg: &'r Self::Arg<'_>) -> &'r Self { + arg.cow_as_ref() + } +} + +impl meta::ArrayElement for DynGd +where + T: GodotClass, + D: ?Sized + 'static, +{ +} diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index eaf5a0e65..8c219294c 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -9,8 +9,7 @@ use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::ops::{Deref, DerefMut}; use godot_ffi as sys; - -use sys::{static_assert_eq_size_align, VariantType}; +use sys::{static_assert_eq_size_align, SysPtr as _, VariantType}; use crate::builtin::{Callable, NodePath, StringName, Variant}; use crate::global::PropertyHint; @@ -278,6 +277,27 @@ impl Gd { self.raw.is_instance_valid() } + /// Returns the dynamic class name of the object as `StringName`. + /// + /// This method retrieves the class name of the object at runtime, which can be different from [`T::class_name()`] if derived + /// classes are involved. + /// + /// Unlike [`Object::get_class()`], this returns `StringName` instead of `GString` and needs no `Inherits` bound. + pub(crate) fn dynamic_class_string(&self) -> StringName { + unsafe { + StringName::new_with_string_uninit(|ptr| { + let success = sys::interface_fn!(object_get_class_name)( + self.obj_sys().as_const(), + sys::get_library(), + ptr, + ); + + let success = sys::conv::bool_from_sys(success); + assert!(success, "failed to get class name for object {self:?}"); + }) + } + } + /// **Upcast:** convert into a smart pointer to a base class. Always succeeds. /// /// Moves out of this value. If you want to create _another_ smart pointer instance, @@ -300,6 +320,13 @@ impl Gd { .expect("Upcast failed. This is a bug; please report it.") } + /// Equivalent to [`upcast::()`][Self::upcast], but without bounds. + // Not yet public because it might need _mut/_ref overloads, and 6 upcast methods are a bit much... + pub(crate) fn upcast_object(self) -> Gd { + self.owned_cast() + .expect("Upcast to Object failed. This is a bug; please report it.") + } + /// **Upcast shared-ref:** access this object as a shared reference to a base class. /// /// This is semantically equivalent to multiple applications of [`Self::deref()`]. Not really useful on its own, but combined with @@ -407,8 +434,9 @@ impl Gd { }) } - /// Returns `Ok(cast_obj)` on success, `Err(self)` on error - fn owned_cast(self) -> Result, Self> + /// Returns `Ok(cast_obj)` on success, `Err(self)` on error. + // Visibility: used by DynGd. + pub(crate) fn owned_cast(self) -> Result, Self> where U: GodotClass, { @@ -707,6 +735,7 @@ impl FromGodot for Gd { } } +// Keep in sync with DynGd. impl GodotType for Gd { // Some #[doc(hidden)] are repeated despite already declared in trait; some IDEs suggest in auto-complete otherwise. type Ffi = RawGd; diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 9ac5c4f50..dea6ad9af 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -9,7 +9,7 @@ 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, ErasedDynGd, ErasedRegisterFn, ErasedRegisterRpcsFn, InherentImpl, PluginItem, }; pub use crate::storage::{as_storage, Storage}; pub use sys::out; @@ -23,6 +23,7 @@ use crate::meta::CallContext; use crate::sys; use std::sync::{atomic, Arc, Mutex}; use sys::Global; + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 678781b36..764763c72 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -6,24 +6,50 @@ */ use std::collections::HashMap; -use std::ptr; +use std::{any, ptr}; use crate::init::InitLevel; +use crate::meta::error::{ConvertError, FromGodotError}; use crate::meta::ClassName; -use crate::obj::{cap, GodotClass}; +use crate::obj::{cap, DynGd, Gd, GodotClass}; use crate::private::{ClassPlugin, PluginItem}; use crate::registry::callbacks; -use crate::registry::plugin::{ErasedRegisterFn, InherentImpl}; -use crate::{godot_error, sys}; +use crate::registry::plugin::{ErasedDynifyFn, ErasedRegisterFn, InherentImpl}; +use crate::{classes, godot_error, sys}; use sys::{interface_fn, out, Global, GlobalGuard, GlobalLockError}; -// Needed for class unregistering. The variable is populated during class registering. There is no actual concurrency here, because Godot -// calls register/unregister in the main thread. Mutex is just casual way to ensure safety in this non-performance-critical path. -// Note that we panic on concurrent access instead of blocking (fail-fast approach). If that happens, most likely something changed on Godot -// side and analysis required to adopt these changes. -static LOADED_CLASSES: Global< - HashMap>, //. -> = Global::default(); +/// Returns a lock to a global map of loaded classes, by initialization level. +/// +/// Needed for class unregistering. The `static` is populated during class registering. There is no actual concurrency here, because Godot +/// calls register/unregister in the main thread. Mutex is just casual way to ensure safety in this non-performance-critical path. +/// Note that we panic on concurrent access instead of blocking (fail-fast approach). If that happens, most likely something changed on Godot +/// side and analysis required to adopt these changes. +fn global_loaded_classes_by_init_level( +) -> GlobalGuard<'static, HashMap>> { + static LOADED_CLASSES_BY_INIT_LEVEL: Global< + HashMap>, //. + > = Global::default(); + + lock_or_panic(&LOADED_CLASSES_BY_INIT_LEVEL, "loaded classes") +} + +/// Returns a lock to a global map of loaded classes, by class name. +/// +/// Complementary mechanism to the on-registration hooks like `__register_methods()`. This is used for runtime queries about a class, for +/// information which isn't stored in Godot. Example: list related `dyn Trait` implementations. +fn global_loaded_classes_by_name() -> GlobalGuard<'static, HashMap> { + static LOADED_CLASSES_BY_NAME: Global> = Global::default(); + + lock_or_panic(&LOADED_CLASSES_BY_NAME, "loaded classes (by name)") +} + +fn global_dyn_traits_by_typeid( +) -> GlobalGuard<'static, HashMap>> { + static DYN_TRAITS_BY_TYPEID: Global>> = + Global::default(); + + lock_or_panic(&DYN_TRAITS_BY_TYPEID, "dyn traits") +} // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -35,6 +61,17 @@ pub struct LoadedClass { is_editor_plugin: bool, } +/// Represents a class which is currently loaded and retained in memory -- including metadata. +// +// Currently empty, but should already work for per-class queries. +pub struct ClassMetadata {} + +/// Represents a `dyn Trait` implemented (and registered) for a class. +pub struct DynToClassRelation { + implementing_class_name: ClassName, + erased_dynify_fn: ErasedDynifyFn, +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- #[derive(Debug)] @@ -60,8 +97,11 @@ struct ClassRegistrationInfo { init_level: InitLevel, is_editor_plugin: bool, + /// One entry for each `dyn Trait` implemented (and registered) for this class. + dynify_fns_by_trait: HashMap, + /// Used to ensure that each component is only filled once. - component_already_filled: [bool; 3], + component_already_filled: [bool; 4], } impl ClassRegistrationInfo { @@ -73,6 +113,7 @@ impl ClassRegistrationInfo { PluginItem::Struct { .. } => 0, PluginItem::InherentImpl(_) => 1, PluginItem::ITraitImpl { .. } => 2, + PluginItem::DynTraitImpl { .. } => 3, }; if self.component_already_filled[index] { @@ -139,6 +180,7 @@ pub fn register_class< godot_params, init_level: T::INIT_LEVEL, is_editor_plugin: false, + dynify_fns_by_trait: HashMap::new(), component_already_filled: Default::default(), // [false; N] }); } @@ -169,34 +211,62 @@ pub fn auto_register_classes(init_level: InitLevel) { fill_class_info(elem.item.clone(), class_info); }); - let mut loaded_classes_by_level = global_loaded_classes(); - for info in map.into_values() { + let mut loaded_classes_by_level = global_loaded_classes_by_init_level(); + let mut loaded_classes_by_name = global_loaded_classes_by_name(); + let mut dyn_traits_by_typeid = global_dyn_traits_by_typeid(); + + for mut info in map.into_values() { let class_name = info.class_name; out!("Register class: {class_name} at level `{init_level:?}`"); + let loaded_class = LoadedClass { name: class_name, is_editor_plugin: info.is_editor_plugin, }; + let metadata = ClassMetadata {}; + + // Transpose Class->Trait relations to Trait->Class relations. + for (trait_type_id, dynify_fn) in info.dynify_fns_by_trait.drain() { + dyn_traits_by_typeid + .entry(trait_type_id) + .or_default() + .push(DynToClassRelation { + implementing_class_name: class_name, + erased_dynify_fn: dynify_fn, + }); + } + loaded_classes_by_level .entry(init_level) .or_default() .push(loaded_class); + loaded_classes_by_name.insert(class_name, metadata); + register_class_raw(info); - out!("Class {class_name} loaded"); + + out!("Class {class_name} loaded."); } out!("All classes for level `{init_level:?}` auto-registered."); } pub fn unregister_classes(init_level: InitLevel) { - let mut loaded_classes_by_level = global_loaded_classes(); + let mut loaded_classes_by_level = global_loaded_classes_by_init_level(); + let mut loaded_classes_by_name = global_loaded_classes_by_name(); + // TODO clean up dyn traits + let loaded_classes_current_level = loaded_classes_by_level .remove(&init_level) .unwrap_or_default(); - out!("Unregistering classes of level {init_level:?}..."); - for class_name in loaded_classes_current_level.into_iter().rev() { - unregister_class_raw(class_name); + + out!("Unregister classes of level {init_level:?}..."); + for class in loaded_classes_current_level.into_iter().rev() { + // Remove from other map. + loaded_classes_by_name.remove(&class.name); + + // Unregister from Godot. + unregister_class_raw(class); } } @@ -212,17 +282,52 @@ pub fn auto_register_rpcs(object: &mut T) { } } -fn global_loaded_classes() -> GlobalGuard<'static, HashMap>> { - match LOADED_CLASSES.try_lock() { - Ok(it) => it, - Err(err) => match err { - GlobalLockError::Poisoned {..} => panic!( - "global lock for loaded classes poisoned; class registration or deregistration may have panicked" - ), - GlobalLockError::WouldBlock => panic!("unexpected concurrent access to global lock for loaded classes"), - GlobalLockError::InitFailed => unreachable!("global lock for loaded classes not initialized"), - }, +/// Tries to upgrade a polymorphic `Gd` to `DynGd`, where the `T` -> `D` relation is only present via derived objects. +/// +/// This works without direct `T: AsDyn` because it considers `object`'s dynamic type `Td : Inherits`. +/// +/// Only direct relations are considered, i.e. the `Td: AsDyn` must be fulfilled (and registered). If any intermediate base class of `Td` +/// implements the trait `D`, this will not consider it. Base-derived conversions are theoretically possible, but need quite a bit of extra +/// machinery. +pub(crate) fn try_dynify_object( + object: Gd, +) -> Result, ConvertError> { + let typeid = any::TypeId::of::(); + let trait_name = sys::short_type_name::(); + + // 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)); + }; + + // TODO maybe use 2nd hashmap instead of linear search. + // (probably not pair of typeid/classname, as that wouldn't allow the above check). + let dynamic_class = object.dynamic_class_string(); + + for relation in relations { + if dynamic_class == relation.implementing_class_name.to_string_name() { + let erased = (relation.erased_dynify_fn)(object.upcast_object()); + + // Must succeed, or was registered wrong. + let dyn_gd_object = erased.boxed.downcast::>(); + + // SAFETY: the relation ensures that the **unified** (for storage) pointer was of type `DynGd`. + let dyn_gd_object = unsafe { dyn_gd_object.unwrap_unchecked() }; + + // SAFETY: the relation ensures that the **original** pointer was of type `DynGd`. + let dyn_gd_t = unsafe { dyn_gd_object.cast_unchecked::() }; + + return Ok(dyn_gd_t); + } } + + let error = FromGodotError::UnimplementedDynTrait { + trait_name, + class_name: dynamic_class.to_string(), + }; + + Err(error.into_error(object)) } /// Populate `c` with all the relevant data from `component` (depending on component type). @@ -343,6 +448,21 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { c.godot_params.property_get_revert_func = user_property_get_revert_fn; c.user_virtual_fn = Some(get_virtual_fn); } + PluginItem::DynTraitImpl { + dyn_trait_typeid, + erased_dynify_fn, + } => { + let prev = c + .dynify_fns_by_trait + .insert(dyn_trait_typeid, erased_dynify_fn); + + assert!( + prev.is_none(), + "Duplicate registration of {:?} for class {}", + dyn_trait_typeid, + c.class_name + ); + } } // out!("| reg (after): {c:?}"); // out!(); @@ -360,6 +480,8 @@ fn fill_into(dst: &mut Option, src: Option) -> Result<(), ()> { /// Registers a class with given the dynamic type information `info`. fn register_class_raw(mut info: ClassRegistrationInfo) { + // Some metadata like dynify fns are already emptied at this point. Only consider registrations for Godot. + // First register class... validate_class_constraints(&info); @@ -470,6 +592,19 @@ fn unregister_class_raw(class: LoadedClass) { out!("Class {class_name} unloaded"); } +fn lock_or_panic(global: &'static Global, ctx: &str) -> GlobalGuard<'static, T> { + match global.try_lock() { + Ok(it) => it, + Err(err) => match err { + GlobalLockError::Poisoned { .. } => panic!( + "global lock for {ctx} poisoned; class registration or deregistration may have panicked" + ), + GlobalLockError::WouldBlock => panic!("unexpected concurrent access to global lock for {ctx}"), + GlobalLockError::InitFailed => unreachable!("global lock for {ctx} not initialized"), + }, + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Substitutes for Default impl @@ -487,6 +622,7 @@ fn default_registration_info(class_name: ClassName) -> ClassRegistrationInfo { godot_params: default_creation_info(), init_level: InitLevel::Scene, is_editor_plugin: false, + dynify_fns_by_trait: HashMap::new(), component_already_filled: Default::default(), // [false; N] } } diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index db191c774..c4051baab 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -9,9 +9,10 @@ use crate::docs::*; use crate::init::InitLevel; use crate::meta::ClassName; -use crate::sys; +use crate::obj::Gd; +use crate::{classes, sys}; use std::any::Any; -use std::fmt; +use std::{any, fmt}; // TODO(bromeon): some information coming from the proc-macro API is deferred through PluginItem, while others is directly // translated to code. Consider moving more code to the PluginItem, which allows for more dynamic registration and will // be easier for a future builder API. @@ -55,6 +56,8 @@ impl fmt::Debug for ErasedRegisterRpcsFn { } } +pub type ErasedDynifyFn = fn(Gd) -> ErasedDynGd; + #[derive(Clone, Debug)] pub struct InherentImpl { /// Callback to library-generated function which registers functions and constants in the `impl` block. @@ -239,4 +242,16 @@ pub enum PluginItem { ) -> sys::GDExtensionBool, >, }, + + DynTraitImpl { + /// TypeId of the `dyn Trait` object. + dyn_trait_typeid: any::TypeId, + + /// Function that converts a `Gd` to a type-erased `DynGd` (with the latter erased for common storage). + erased_dynify_fn: fn(Gd) -> ErasedDynGd, + }, +} + +pub struct ErasedDynGd { + pub boxed: Box, } diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 9ac02f157..cfe018e6e 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -188,13 +188,13 @@ pub fn unqualified_type_name() -> &'static str { */ /// Like [`std::any::type_name`], but returns a short type name without module paths. -pub fn short_type_name() -> String { +pub fn short_type_name() -> String { let full_name = std::any::type_name::(); strip_module_paths(full_name) } /// Like [`std::any::type_name_of_val`], but returns a short type name without module paths. -pub fn short_type_name_of_val(val: &T) -> String { +pub fn short_type_name_of_val(val: &T) -> String { let full_name = std::any::type_name_of_val(val); strip_module_paths(full_name) } diff --git a/godot-macros/src/class/godot_dyn.rs b/godot-macros/src/class/godot_dyn.rs index b758f9319..db8d95fb8 100644 --- a/godot-macros/src/class/godot_dyn.rs +++ b/godot-macros/src/class/godot_dyn.rs @@ -6,7 +6,7 @@ */ use crate::util::bail; -use crate::ParseResult; +use crate::{util, ParseResult}; use proc_macro2::TokenStream; use quote::quote; @@ -33,6 +33,10 @@ pub fn attribute_godot_dyn(input_decl: venial::Item) -> ParseResult }; let class_path = &decl.self_ty; + let class_name_obj = util::class_name_obj(class_path); //&util::extract_typename(class_path)); + let prv = quote! { ::godot::private }; + + //let dynify_fn = format_ident!("__dynify_{}", class_name); let new_code = quote! { #decl @@ -46,6 +50,28 @@ pub fn attribute_godot_dyn(input_decl: venial::Item) -> ParseResult self } } + + ::godot::sys::plugin_add!(__GODOT_PLUGIN_REGISTRY in #prv; #prv::ClassPlugin { + class_name: #class_name_obj, + item: #prv::PluginItem::DynTraitImpl { + dyn_trait_typeid: std::any::TypeId::of::(), + erased_dynify_fn: { + fn dynify_fn(obj: ::godot::obj::Gd<::godot::classes::Object>) -> #prv::ErasedDynGd { + let obj = unsafe { obj.try_cast::<#class_path>().unwrap_unchecked() }; + let obj = obj.into_dyn::(); + let obj = obj.upcast::<::godot::classes::Object>(); + + #prv::ErasedDynGd { + boxed: Box::new(obj), + } + } + + dynify_fn + } + }, + init_level: <#class_path as ::godot::obj::GodotClass>::INIT_LEVEL, + }); + }; Ok(new_code) diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 1ec2ffe79..06d8d30d3 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -212,7 +212,7 @@ fn validate_self(original_impl: &venial::Impl, attr: &str) -> ParseResult } /// Gets the right-most type name in the path. -fn extract_typename(ty: &venial::TypeExpr) -> Option { +pub(crate) fn extract_typename(ty: &venial::TypeExpr) -> Option { match ty.as_path() { Some(mut path) => path.segments.pop(), _ => None, diff --git a/itest/rust/src/builtin_tests/containers/variant_test.rs b/itest/rust/src/builtin_tests/containers/variant_test.rs index 841359070..ed095bb57 100644 --- a/itest/rust/src/builtin_tests/containers/variant_test.rs +++ b/itest/rust/src/builtin_tests/containers/variant_test.rs @@ -130,6 +130,22 @@ fn variant_bad_conversions() { .expect_err("`nil` should not convert to `Dictionary`"); } +#[itest] +fn variant_bad_conversion_error_message() { + let variant = 123.to_variant(); + + let err = variant + .try_to::() + .expect_err("i32 -> GString conversion should fail"); + assert_eq!(err.to_string(), "cannot convert from INT to STRING: 123"); + + // TODO this error isn't great, but unclear whether it can be improved. If not, document. + let err = variant + .try_to::>() + .expect_err("i32 -> Gd conversion should fail"); + assert_eq!(err.to_string(), "`Gd` cannot be null: null"); +} + #[itest] fn variant_array_bad_conversions() { let i32_array: Array = array![1, 2, 160, -40]; diff --git a/itest/rust/src/object_tests/dyn_gd_test.rs b/itest/rust/src/object_tests/dyn_gd_test.rs index 8b9815c18..4c440e301 100644 --- a/itest/rust/src/object_tests/dyn_gd_test.rs +++ b/itest/rust/src/object_tests/dyn_gd_test.rs @@ -83,6 +83,81 @@ fn dyn_gd_upcast() { node.free(); } +#[itest] +fn dyn_gd_downcast() { + let original = Gd::from_object(RefcHealth { hp: 20 }).into_dyn(); + let mut object = original.upcast::(); + + object.dyn_bind_mut().deal_damage(7); + + let failed = object.try_cast::(); + let object = failed.expect_err("DynGd::try_cast() succeeded, but should have failed"); + + let refc = object.cast::(); + assert_eq!(refc.dyn_bind().get_hitpoints(), 13); + + let back = refc + .try_cast::() + .expect("DynGd::try_cast() should have succeeded"); + assert_eq!(back.bind().get_hitpoints(), 13); +} + +#[itest] +fn dyn_gd_debug() { + let obj = Gd::from_object(RefcHealth { hp: 20 }).into_dyn(); + let id = obj.instance_id(); + + let actual = format!(".:{obj:?}:."); + let expected = format!(".:DynGd {{ id: {id}, class: RefcHealth, trait: dyn Health }}:."); + + assert_eq!(actual, expected); +} + +#[itest] +fn dyn_gd_display() { + let obj = Gd::from_object(RefcHealth { hp: 55 }).into_dyn(); + + let actual = format!("{obj}"); + let expected = "RefcHealth(hp=55)"; + + assert_eq!(actual, expected); +} + +#[itest] +fn dyn_gd_eq() { + let gd = Gd::from_object(RefcHealth { hp: 55 }); + let a = gd.clone().into_dyn(); + let b = gd.into_dyn(); + let c = b.clone(); + + assert_eq!(a, b); + assert_eq!(a, c); + assert_eq!(b, c); + + let x = Gd::from_object(RefcHealth { hp: 55 }).into_dyn(); + + assert_ne!(a, x); +} + +#[itest] +fn dyn_gd_hash() { + use godot::sys::hash_value; + + let gd = Gd::from_object(RefcHealth { hp: 55 }); + let a = gd.clone().into_dyn(); + let b = gd.into_dyn(); + let c = b.clone(); + + assert_eq!(hash_value(&a), hash_value(&b)); + assert_eq!(hash_value(&a), hash_value(&c)); + assert_eq!(hash_value(&b), hash_value(&c)); + + let x = Gd::from_object(RefcHealth { hp: 55 }).into_dyn(); + + // Not guaranteed, but exceedingly likely. + assert_ne!(hash_value(&a), hash_value(&x)); +} + #[itest] fn dyn_gd_exclusive_guard() { let mut a = foreign::NodeHealth::new_alloc().into_dyn(); @@ -172,6 +247,113 @@ fn dyn_gd_pass_to_godot_api() { parent.free(); } +#[itest] +fn dyn_gd_variant_conversions() { + let original = Gd::from_object(RefcHealth { hp: 11 }).into_dyn::(); + let original_id = original.instance_id(); + let refc = original.into_gd().upcast::(); + + let variant = refc.to_variant(); + + // Convert to different levels of DynGd: + + let back: DynGd = variant.to(); + assert_eq!(back.bind().get_hitpoints(), 11); + assert_eq!(back.instance_id(), original_id); + + let back: DynGd = variant.to(); + assert_eq!(back.dyn_bind().get_hitpoints(), 11); + assert_eq!(back.instance_id(), original_id); + + let back: DynGd = variant.to(); + assert_eq!(back.dyn_bind().get_hitpoints(), 11); + assert_eq!(back.instance_id(), original_id); + + // Convert to different levels of Gd: + + let back: Gd = variant.to(); + assert_eq!(back.bind().get_hitpoints(), 11); + assert_eq!(back.instance_id(), original_id); + + let back: Gd = variant.to(); + assert_eq!(back.instance_id(), original_id); + + let back: Gd = variant.to(); + assert_eq!(back.instance_id(), original_id); +} + +#[itest] +fn dyn_gd_store_in_godot_array() { + let a = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::(); + let b = foreign::NodeHealth::new_alloc().into_dyn(); + + let array: Array> = array![&a.upcast(), &b.upcast()]; + + assert_eq!(array.at(0).dyn_bind().get_hitpoints(), 33); + assert_eq!(array.at(1).dyn_bind().get_hitpoints(), 100); + + array.at(1).free(); +} + +#[itest] +fn dyn_gd_error_unregistered_trait() { + trait UnrelatedTrait {} + + let obj = Gd::from_object(RefcHealth { hp: 33 }).into_dyn::(); + + let variant = obj.to_variant(); + let back = variant.try_to::>(); + + let err = back.expect_err("DynGd::try_to() should have failed"); + let expected_err = { + // The conversion fails before a DynGd is created, so Display still operates on the Gd. + let obj = obj.into_gd(); + + format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: {obj:?}") + }; + + assert_eq!(err.to_string(), expected_err); +} + +#[itest] +fn dyn_gd_error_unimplemented_trait() { + let obj = RefCounted::new_gd(); + + let variant = obj.to_variant(); + let back = variant.try_to::>(); + + let err = back.expect_err("DynGd::try_to() should have failed"); + assert_eq!( + err.to_string(), + format!("none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: {obj:?}") + ); +} + +#[itest] +fn dyn_gd_free_while_dyn_bound() { + let mut obj = foreign::NodeHealth::new_alloc().into_dyn(); + + { + let copy = obj.clone(); + let _guard = obj.dyn_bind(); + + expect_panic("Cannot free while dyn_bind() guard is held", || { + copy.free(); + }); + } + { + let copy = obj.clone(); + let _guard = obj.dyn_bind_mut(); + + expect_panic("Cannot free while dyn_bind_mut() guard is held", || { + copy.free(); + }); + } + + // Now allowed. + obj.free(); +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Example symbols @@ -191,6 +373,13 @@ struct RefcHealth { hp: u8, } +#[godot_api] +impl IRefCounted for RefcHealth { + fn to_string(&self) -> GString { + format!("RefcHealth(hp={})", self.hp).into() + } +} + // Pretend NodeHealth is defined somewhere else, with a default constructor but // no knowledge of health. We retrofit the property via Godot "meta" key-values. mod foreign { diff --git a/itest/rust/src/object_tests/dynamic_call_test.rs b/itest/rust/src/object_tests/dynamic_call_test.rs index 6b4f960af..6f3c80b4f 100644 --- a/itest/rust/src/object_tests/dynamic_call_test.rs +++ b/itest/rust/src/object_tests/dynamic_call_test.rs @@ -133,7 +133,7 @@ fn dynamic_call_parameter_mismatch() { "godot-rust function call failed: Object::call(&\"take_1_int\", [va] \"string\")\ \n Source: ObjPayload::take_1_int()\ \n Reason: parameter #0 (i64) conversion\ - \n Source: expected type INT, got STRING: \"string\"" + \n Source: cannot convert from STRING to INT: \"string\"" ); obj.free(); @@ -271,7 +271,7 @@ fn dynamic_call_parameter_mismatch_engine() { assert_eq!( call_error.to_string(), "godot-rust function call failed: Object::call(&\"set_name\", [va] 123)\ - \n Reason: parameter #1 conversion -- expected type STRING, got INT" + \n Reason: parameter #1 -- cannot convert from INT to STRING" ); node.free(); diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index ab94bdbb2..63c21e326 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -518,6 +518,20 @@ fn object_engine_convert_variant_nil() { }); } +#[itest] +fn object_engine_convert_variant_error() { + let refc = RefCounted::new_gd(); + let variant = refc.to_variant(); + + let err = Gd::::try_from_variant(&variant) + .expect_err("`Gd` should not convert to `Gd`"); + + assert_eq!( + err.to_string(), + format!("cannot convert to class Area2D: {refc:?}") + ); +} + #[itest] fn object_engine_returned_refcount() { let Some(file) = FileAccess::open("res://itest.gdextension", file_access::ModeFlags::READ)