diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 57a627a57..fd262db92 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -25,6 +25,7 @@ pub struct Context<'a> { inheritance_tree: InheritanceTree, cached_rust_types: HashMap, notifications_by_class: HashMap>, + classes_with_signals: HashSet, notification_enum_names_by_class: HashMap, method_table_indices: HashMap, method_table_next_index: HashMap, @@ -66,6 +67,10 @@ impl<'a> Context<'a> { // Populate class lookup by name engine_classes.insert(class_name.clone(), class); + if !option_as_slice(&class.signals).is_empty() { + ctx.classes_with_signals.insert(class_name.clone()); + } + // Populate derived-to-base relations if let Some(base) = class.inherits.as_ref() { let base_name = TyName::from_godot(base); @@ -278,6 +283,24 @@ impl<'a> Context<'a> { self.cached_rust_types.get(ty) } + /// Walks up in the hierarchy, and returns the first (nearest) base class which declares at least 1 signal. + /// + /// Always returns a result, as `Object` (the root) itself declares signals. + pub fn find_nearest_base_with_signals(&self, class_name: &TyName) -> TyName { + let tree = self.inheritance_tree(); + + let mut class = class_name.clone(); + while let Some(base) = tree.direct_base(&class) { + if self.classes_with_signals.contains(&base) { + return base; + } else { + class = base; + } + } + + panic!("Object (root) should always have signals") + } + pub fn notification_constants(&'a self, class_name: &TyName) -> Option<&'a Vec<(Ident, i32)>> { self.notifications_by_class.get(class_name) } diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index 7b038971f..ad3acfa08 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -123,7 +123,10 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas builders, } = make_class_methods(class, &class.methods, &cfg_attributes, ctx); - let signal_types = signals::make_class_signals(class, &class.signals, ctx); + let signals::SignalCodegen { + signal_code, + has_own_signals, + } = signals::make_class_signals(class, &class.signals, ctx); let enums = enums::make_enums(&class.enums, &cfg_attributes); let constants = constants::make_constants(&class.constants); @@ -137,14 +140,14 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas // Associated "sidecar" module is made public if there are other symbols related to the class, which are not // in top-level godot::classes module (notification enums are not in the sidecar, but in godot::classes::notify). // This checks if token streams (i.e. code) is empty. - let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || signal_types.is_some(); + let has_sidecar_module = !enums.is_empty() || !builders.is_empty() || has_own_signals; let class_doc = docs::make_class_doc( class_name, base_ident_opt, notification_enum.is_some(), has_sidecar_module, - signal_types.is_some(), + has_own_signals, ); let module_doc = docs::make_module_doc(class_name); @@ -262,7 +265,7 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas #builders #enums - #signal_types + #signal_code }; // note: TypePtr -> ObjectPtr conversion OK? diff --git a/godot-codegen/src/generator/signals.rs b/godot-codegen/src/generator/signals.rs index e94f9b441..f08a48ac8 100644 --- a/godot-codegen/src/generator/signals.rs +++ b/godot-codegen/src/generator/signals.rs @@ -12,48 +12,121 @@ use crate::context::Context; use crate::conv; -use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, RustTy, TyName}; +use crate::models::domain::{Class, ClassLike, ClassSignal, FnParam, ModName, RustTy, TyName}; use crate::util::{ident, safe_ident}; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; +pub struct SignalCodegen { + pub signal_code: TokenStream, + pub has_own_signals: bool, +} + pub fn make_class_signals( class: &Class, signals: &[ClassSignal], - _ctx: &mut Context, -) -> Option { - if signals.is_empty() { - return None; - } - + ctx: &mut Context, +) -> SignalCodegen { let all_params: Vec = signals .iter() .map(|s| SignalParams::new(&s.parameters)) .collect(); - let signal_collection_struct = make_signal_collection(class, signals, &all_params); + let class_name = class.name(); + + // If no signals are defined in current class, walk up until we find some. + let (own_collection_struct, nearest_collection_name, nearest_class, has_own_signals); + if signals.is_empty() { + // Use the nearest base class that *has* signals, and store its collection name. + let nearest = ctx.find_nearest_base_with_signals(class_name); + + // Doesn't define own collection struct if no signals are present (note that WithSignals is still implemented). + own_collection_struct = TokenStream::new(); + nearest_collection_name = make_collection_name(&nearest); + nearest_class = Some(nearest); + has_own_signals = false; + } else { + let (code, name) = make_signal_collection(class, signals, &all_params); + + own_collection_struct = code; + nearest_collection_name = name; + nearest_class = None; + has_own_signals = true; + }; let signal_types = signals .iter() .zip(all_params.iter()) .map(|(signal, params)| make_signal_individual_struct(signal, params)); - let class_name = class.name(); + let with_signals_impl = + make_with_signals_impl(class_name, &nearest_collection_name, nearest_class.as_ref()); - Some(quote! { + let deref_impl = + has_own_signals.then(|| make_upcast_deref_impl(class_name, &nearest_collection_name)); + + let code = quote! { #[cfg(since_api = "4.2")] pub use signals::*; #[cfg(since_api = "4.2")] mod signals { - use crate::obj::Gd; + use crate::obj::{Gd, GodotClass}; use super::re_export::#class_name; + use crate::registry::signal::TypedSignal; use super::*; - #signal_collection_struct + // These may be empty if the class doesn't define any signals itself. + #own_collection_struct #( #signal_types )* + + // These are always present. + #with_signals_impl + #deref_impl } - }) + }; + + SignalCodegen { + signal_code: code, + has_own_signals, + } +} + +/// Creates `impl WithSignals`. +/// +/// Present for every single class, as every class has at least inherited signals (since `Object` has some). +fn make_with_signals_impl( + class_name: &TyName, + collection_struct_name: &Ident, + nearest_class: Option<&TyName>, // None if own class has signals. +) -> TokenStream { + let base_use_statement = quote! { use crate::obj::WithSignals; }; + let use_statement = if let Some(nearest_class) = nearest_class { + let module_name = ModName::from_godot(&nearest_class.godot_ty); + quote! { + #base_use_statement + use crate::classes::#module_name::#collection_struct_name; + } + } else { + base_use_statement + }; + + quote! { + #use_statement + impl WithSignals for #class_name { + type SignalCollection<'c, C: WithSignals> = #collection_struct_name<'c, C>; + type __SignalObj<'c> = Gd; + // type __SignalObj<'c, C: WithSignals> = Gd; + + // During construction, C = Self. + #[doc(hidden)] + fn __signals_from_external(gd_mut: &mut Gd) -> Self::SignalCollection<'_, Self> { + Self::SignalCollection { + __internal_obj: Some(gd_mut.clone()), + } + } + } + } } // Used outside, to document class with links to this type. @@ -70,7 +143,9 @@ fn make_signal_collection( class: &Class, signals: &[ClassSignal], params: &[SignalParams], -) -> TokenStream { +) -> (TokenStream, Ident) { + debug_assert!(!signals.is_empty()); // checked outside + let class_name = class.name(); let collection_struct_name = make_collection_name(class_name); @@ -83,9 +158,9 @@ fn make_signal_collection( quote! { // Important to return lifetime 'c here, not '_. #[doc = #provider_docs] - pub fn #signal_name(&mut self) -> #individual_struct_name<'c> { + pub fn #signal_name(&mut self) -> #individual_struct_name<'c, C> { #individual_struct_name { - typed: crate::registry::signal::TypedSignal::new(self.__gd.clone(), #signal_name_str) + typed: TypedSignal::extract(&mut self.__internal_obj, #signal_name_str) } } } @@ -96,26 +171,48 @@ fn make_signal_collection( c = class_name.rust_ty ); - quote! { + let code = quote! { #[doc = #collection_docs] - pub struct #collection_struct_name<'c> { - __gd: &'c mut Gd<#class_name>, + // C is needed for signals of derived classes that are upcast via Deref; C in that class is the derived class. + pub struct #collection_struct_name<'c, C: WithSignals = #class_name> + { + #[doc(hidden)] + pub(crate) __internal_obj: Option>, } - impl<'c> #collection_struct_name<'c> { + impl<'c, C: WithSignals> #collection_struct_name<'c, C> { #( #provider_methods )* } + }; - impl crate::obj::WithSignals for #class_name { - type SignalCollection<'c> = #collection_struct_name<'c>; - #[doc(hidden)] - type __SignalObject<'c> = Gd<#class_name>; + (code, collection_struct_name) +} - #[doc(hidden)] - fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_> { - Self::SignalCollection { - __gd: external, - } +fn make_upcast_deref_impl(class_name: &TyName, collection_struct_name: &Ident) -> TokenStream { + // Root of hierarchy, no "upcast" derefs. + if class_name.rust_ty == "Object" { + return TokenStream::new(); + } + + quote! { + impl<'c, C: WithSignals> std::ops::Deref for #collection_struct_name<'c, C> { + // The whole upcast mechanism is based on C remaining the same even through upcast. + type Target = < + < + #class_name as crate::obj::GodotClass + >::Base as WithSignals + >::SignalCollection<'c, C>; + + fn deref(&self) -> &Self::Target { + type Derived = #class_name; + crate::private::signal_collection_to_base::(self) + } + } + + impl<'c, C: WithSignals> std::ops::DerefMut for #collection_struct_name<'c, C> { + fn deref_mut(&mut self) -> &mut Self::Target { + type Derived = #class_name; + crate::private::signal_collection_to_base_mut::(self) } } } @@ -139,10 +236,10 @@ fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) -> // Embedded in `mod signals`. quote! { // Reduce tokens to parse by reusing this type definitions. - type #typed_name<'c> = crate::registry::signal::TypedSignal<'c, #class_ty, #param_tuple>; + type #typed_name<'c, C> = TypedSignal<'c, C, #param_tuple>; - pub struct #individual_struct_name<'c> { - typed: #typed_name<'c>, + pub struct #individual_struct_name<'c, C: WithSignals = #class_ty> { + typed: #typed_name<'c, C>, } impl<'c> #individual_struct_name<'c> { @@ -151,15 +248,15 @@ fn make_signal_individual_struct(signal: &ClassSignal, params: &SignalParams) -> } } - impl<'c> std::ops::Deref for #individual_struct_name<'c> { - type Target = #typed_name<'c>; + impl<'c, C: WithSignals> std::ops::Deref for #individual_struct_name<'c, C> { + type Target = #typed_name<'c, C>; fn deref(&self) -> &Self::Target { &self.typed } } - impl std::ops::DerefMut for #individual_struct_name<'_> { + impl std::ops::DerefMut for #individual_struct_name<'_, C> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.typed } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 418873f34..7facb5c2d 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -721,7 +721,7 @@ where /// /// [`WithUserSignals::signals()`]: crate::obj::WithUserSignals::signals() #[cfg(since_api = "4.2")] - pub fn signals(&mut self) -> T::SignalCollection<'_> { + pub fn signals(&mut self) -> T::SignalCollection<'_, T> { T::__signals_from_external(self) } } diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index cb5ffc21e..39dfeea3c 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -10,6 +10,8 @@ use crate::builtin::GString; use crate::init::InitLevel; use crate::meta::ClassName; use crate::obj::{bounds, Base, BaseMut, BaseRef, Bounds, Gd}; +#[cfg(since_api = "4.2")] +use crate::registry::signal::SignalObject; use crate::storage::Storage; use godot_ffi as sys; @@ -441,22 +443,34 @@ pub trait WithUserSignals: WithSignals + WithBaseField {} /// /// User-defined classes with `#[signal]` additionally implement [`WithUserSignals`]. #[cfg(since_api = "4.2")] -pub trait WithSignals: GodotClass { +// Inherits bound makes some up/downcasting in signals impl easier. +pub trait WithSignals: GodotClass + Inherits { /// The associated struct listing all signals of this class. /// - /// `'c` denotes the lifetime during which the class instance is borrowed and its signals can be modified. - type SignalCollection<'c>; - - /// Trait that allows [`TypedSignal`] to store a reference to the user object. + /// Parameters: + /// - `'c` denotes the lifetime during which the class instance is borrowed and its signals can be modified. + /// - `C` is the concrete class on which the signals are provided. This can be different than `Self` in case of derived classes + /// (e.g. a user-defined node) connecting/emitting signals of a base class (e.g. `Node`). + type SignalCollection<'c, C> + where + C: WithSignals; + + /// Whether the representation needs to be able to hold just `Gd` (for engine classes) or `UserSignalObject` (for user classes). + // Note: this cannot be in Declarer (Engine/UserDecl) as associated type `type SignalObjectType<'c, T: WithSignals>`, + // because the user impl has the additional requirement T: WithUserSignals. #[doc(hidden)] - #[expect(private_bounds)] - type __SignalObject<'c>: crate::registry::signal::SignalObj; + type __SignalObj<'c>: SignalObject<'c>; + // type __SignalObj<'c, C>: SignalObject<'c> + // where + // C: WithSignals + 'c; /// Create from existing `Gd`, to enable `Gd::signals()`. /// + /// Only used for constructing from a concrete class, so `C = Self` in the return type. + /// /// Takes by reference and not value, to retain lifetime chain. #[doc(hidden)] - fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_>; + fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_, Self>; } /// Implemented for user-defined classes with at least one `#[signal]` declaration. @@ -489,7 +503,7 @@ pub trait WithUserSignals: WithSignals + WithBaseField { /// | `connect_self(f: impl FnMut(&mut Self, i32))` | Connects a `&mut self` method or closure. | /// | `emit(amount: i32)` | Emits the signal with the given arguments. | /// - fn signals(&mut self) -> Self::SignalCollection<'_>; + fn signals(&mut self) -> Self::SignalCollection<'_, Self>; } /// Extension trait for all reference-counted classes. diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index c82deb23e..3aba75592 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -12,6 +12,8 @@ pub use crate::registry::plugin::{ ClassPlugin, DynTraitImpl, ErasedDynGd, ErasedRegisterFn, ITraitImpl, InherentImpl, PluginItem, Struct, }; +#[cfg(since_api = "4.2")] +pub use crate::registry::signal::priv_re_export::*; pub use crate::storage::{as_storage, Storage}; pub use sys::out; @@ -23,7 +25,8 @@ use std::cell::RefCell; use crate::global::godot_error; use crate::meta::error::CallError; use crate::meta::CallContext; -use crate::sys; +use crate::obj::Gd; +use crate::{classes, sys}; use std::io::Write; use std::sync::atomic; use sys::Global; @@ -463,6 +466,14 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 { } } +// Currently unused; implemented due to temporary need and may come in handy. +pub fn rebuild_gd(object_ref: &classes::Object) -> Gd { + let ptr = object_ref.__object_ptr(); + + // SAFETY: ptr comes from valid internal API (and is non-null, so unwrap in from_obj_sys won't fail). + unsafe { Gd::from_obj_sys(ptr) } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- #[cfg(test)] diff --git a/godot-core/src/registry/mod.rs b/godot-core/src/registry/mod.rs index e1eb16326..4205784f8 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -20,7 +20,9 @@ pub mod signal; // Contents re-exported in `godot` crate; just keep empty. #[cfg(before_api = "4.2")] -pub mod signal {} +pub mod signal { + pub mod re_export {} +} // RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature. #[cfg(feature = "codegen-full")] diff --git a/godot-core/src/registry/signal/mod.rs b/godot-core/src/registry/signal/mod.rs index 4a72a49cf..889f1a415 100644 --- a/godot-core/src/registry/signal/mod.rs +++ b/godot-core/src/registry/signal/mod.rs @@ -5,11 +5,30 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +// Whole module only available in Godot 4.2+. + mod connect_builder; +mod signal_object; mod typed_signal; + pub(crate) mod variadic; +pub(crate) use connect_builder::*; +pub(crate) use signal_object::*; +pub(crate) use typed_signal::*; +pub(crate) use variadic::SignalReceiver; + +// Used in `godot` crate. +pub mod re_export { + pub use super::connect_builder::ConnectBuilder; + pub use super::typed_signal::TypedSignal; + pub use super::variadic::SignalReceiver; +} + +// Used in `godot::private` module. +pub mod priv_re_export { + pub use super::signal_object::{ + signal_collection_to_base, signal_collection_to_base_mut, UserSignalObject, + }; +} -pub use connect_builder::*; -pub use typed_signal::*; -pub use variadic::SignalReceiver; // ParamTuple re-exported in crate::meta. diff --git a/godot-core/src/registry/signal/signal_object.rs b/godot-core/src/registry/signal/signal_object.rs new file mode 100644 index 000000000..87b88fbf3 --- /dev/null +++ b/godot-core/src/registry/signal/signal_object.rs @@ -0,0 +1,169 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// Implementation note: +// If this codes too much code bloat / compilation time due to excessive monomorphization of C, it's possible to type-erase this, as +// the internal representation just needs Object. This would allow that all signal collections look the same. It would however make +// + +use crate::classes::Object; +use crate::obj::{Gd, GodotClass, WithBaseField, WithSignals, WithUserSignals}; + +/// Indirection from [`TypedSignal`] to the actual Godot object. +#[doc(hidden)] +pub trait SignalObject<'c> { + fn with_object_mut(&mut self, f: impl FnOnce(&mut Object)); + fn to_owned_object(&self) -> Gd; +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Impl for signals() on user classes. + +/// Links to a Godot object, either via reference (for `&mut self` uses) or via `Gd`. +/// +/// Needs to differentiate the two cases: +/// - `C` is a user object implementing `WithBaseField`, possibly having access from within the class. +/// - `C` is an engine object, so only accessible through `Gd`. +#[doc(hidden)] +pub enum UserSignalObject<'c, C> { + /// Helpful for emit: reuse `&mut self` from within the `impl` block, goes through `base_mut()` re-borrowing and thus allows re-entrant calls + /// through Godot. + Internal { self_mut: &'c mut C }, + //Internal { obj_mut: &'c mut classes::Object }, + /// From outside, based on `Gd` pointer. + External { gd: Gd }, +} + +impl<'c, C> UserSignalObject<'c, C> +where + // 2nd bound necessary, so generics match for TypedSignal construction. + C: WithUserSignals + WithSignals<__SignalObj<'c> = UserSignalObject<'c, C>>, +{ + #[inline] + pub fn from_external(object: Gd) -> Self { + Self::External { + gd: object.upcast(), + } + } + + #[inline] + pub fn from_internal(self_mut: &'c mut C) -> Self { + Self::Internal { self_mut } + } +} + +impl<'c, C: WithUserSignals> SignalObject<'c> for UserSignalObject<'c, C> { + #[inline] + fn with_object_mut(&mut self, f: impl FnOnce(&mut Object)) { + match self { + Self::Internal { self_mut } => { + let mut guard = ::base_mut(*self_mut); + f(guard.upcast_object_mut()) + } + Self::External { gd } => f(gd.upcast_object_mut()), + } + } + + #[inline] + fn to_owned_object(&self) -> Gd { + match self { + // SignalObject::Internal { obj_mut } => crate::private::rebuild_gd(*obj_mut), + Self::Internal { self_mut } => ::to_gd(self_mut).upcast_object(), + Self::External { gd } => gd.clone(), + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Impl for signals() on engine classes. + +impl SignalObject<'_> for Gd { + #[inline] + fn with_object_mut(&mut self, f: impl FnOnce(&mut Object)) { + f(self.upcast_object_mut()) + } + + #[inline] + fn to_owned_object(&self) -> Gd { + self.clone().upcast_object() + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helpers for SignalCollection upcasts. + +pub fn signal_collection_to_base<'r, 'c, C, Derived>( + derived: &'r Derived::SignalCollection<'c, C>, +) -> &'r <::Base as WithSignals>::SignalCollection<'c, C> +where + C: WithSignals, + Derived: WithSignals, +{ + type BaseCollection<'c, C, Derived> = + <::Base as WithSignals>::SignalCollection<'c, C>; + + let derived_collection_ptr = std::ptr::from_ref(derived); + let base_collection_ptr = derived_collection_ptr.cast::>(); + + // SAFETY: + // - Signal collections have the same memory layout, independent of their enclosing class. (While they may differ depending on + // internal/external usage, upcasts + // - The `Inherits` bound additionally ensures that all signals present in Base are also present in Derived, i.e. + // reducing the collection to a smaller subset of signals is safe. + // - The lifetimes remain unchanged. + unsafe { &*base_collection_ptr } +} + +pub fn signal_collection_to_base_mut<'r, 'c, C, Derived>( + derived: &'r mut Derived::SignalCollection<'c, C>, +) -> &'r mut <::Base as WithSignals>::SignalCollection<'c, C> +where + C: WithSignals, + Derived: WithSignals, +{ + type BaseCollection<'c, C, Derived> = + <::Base as WithSignals>::SignalCollection<'c, C>; + + let derived_collection_ptr = std::ptr::from_mut(derived); + let base_collection_ptr = derived_collection_ptr.cast::>(); + + // SAFETY: see signal_collection_to_base(). + unsafe { &mut *base_collection_ptr } +} + +/* Currently unused, but kept around as it's not unlikely we need some form of this. + +fn upcast_signal_collection<'r, 'c, C, Derived, Base>( + derived: &'r Derived::SignalCollection<'c, C>, +) -> &'r Base::SignalCollection<'c, C> +where + C: WithSignals, + Derived: WithSignals, // + Inherits, + Base: WithSignals, +{ + let derived_collection_ptr = std::ptr::from_ref(derived); + let base_collection_ptr = derived_collection_ptr.cast::>(); + + // SAFETY: see signal_collection_to_base(). + unsafe { &*base_collection_ptr } +} + +fn upcast_signal_collection_mut<'r, 'c, C, Derived, Base>( + derived: &'r mut Derived::SignalCollection<'c, C>, +) -> &'r mut Base::SignalCollection<'c, C> +where + C: WithSignals, + Derived: WithSignals + Inherits, + Base: WithSignals, +{ + let derived_collection_ptr = std::ptr::from_mut(derived); + let base_collection_ptr = derived_collection_ptr.cast::>(); + + // SAFETY: see signal_collection_to_base(). + unsafe { &mut *base_collection_ptr } +} +*/ diff --git a/godot-core/src/registry/signal/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs index aea4a657d..270638564 100644 --- a/godot-core/src/registry/signal/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -7,56 +7,31 @@ use crate::builtin::{Callable, Variant}; use crate::classes::object::ConnectFlags; +use crate::meta; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField, WithSignals, WithUserSignals}; -use crate::registry::signal::{make_callable_name, make_godot_fn, ConnectBuilder, SignalReceiver}; -use crate::{classes, meta}; +use crate::registry::signal::{ + make_callable_name, make_godot_fn, ConnectBuilder, SignalObject, SignalReceiver, +}; use std::borrow::Cow; use std::marker::PhantomData; -/// Indirection from [`TypedSignal`] to the actual Godot object. +/// Object part of the signal receiver (handler). /// -/// Needs to differentiate the two cases: -/// - `C` is a user object implementing `WithBaseField`, possibly having access from within the class. -/// - `C` is an engine object, so only accessible through `Gd`. -pub(crate) trait SignalObj { - fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)); - fn to_owned_object(&self) -> Gd; +/// Functionality overlaps partly with [`meta::AsObjectArg`] and [`meta::AsArg`]. Can however not directly be replaced +/// with `AsObjectArg`, since that allows nullability and doesn't require `&mut T`. Maybe there's a way to reuse them though. +pub trait ToSignalObj { + fn to_signal_obj(&self) -> Gd; } -/// Links to a Godot object, either via reference (for `&mut self` uses) or via `Gd`. -#[doc(hidden)] -pub enum UserSignalObj<'a, C: GodotClass> { - /// Helpful for emit: reuse `&mut self` from within the `impl` block, goes through `base_mut()` re-borrowing and thus allows re-entrant calls - /// through Godot. - Internal { obj_mut: &'a mut C }, - - /// From outside, based on `Gd` pointer. - External { gd: Gd }, -} - -impl SignalObj for UserSignalObj<'_, C> { - fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { - match self { - UserSignalObj::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()), - UserSignalObj::External { gd } => f(gd.upcast_object_mut()), - } - } - - fn to_owned_object(&self) -> Gd { - match self { - UserSignalObj::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut), - UserSignalObj::External { gd } => gd.clone(), - } +impl ToSignalObj for Gd { + fn to_signal_obj(&self) -> Gd { + self.clone() } } -impl SignalObj for Gd { - fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { - f(self.upcast_object_mut()); - } - - fn to_owned_object(&self) -> Gd { - self.clone() +impl ToSignalObj for C { + fn to_signal_obj(&self) -> Gd { + WithBaseField::to_gd(self) } } @@ -88,23 +63,50 @@ impl SignalObj for Gd { /// See the [Signals](https://godot-rust.github.io/book/register/signals.html) chapter in the book for a detailed introduction and examples. pub struct TypedSignal<'c, C: WithSignals, Ps> { /// In Godot, valid signals (unlike funcs) are _always_ declared in a class and become part of each instance. So there's always an object. - owner: C::__SignalObject<'c>, + object: C::__SignalObj<'c>, name: Cow<'static, str>, _signature: PhantomData, } impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { #[doc(hidden)] - pub fn new(owner: C::__SignalObject<'c>, name: &'static str) -> Self { + pub fn extract( + obj: &mut Option>, + signal_name: &'static str, + ) -> TypedSignal<'c, C, Ps> { + let obj = obj.take().unwrap_or_else(|| { + panic!( + "signals().{signal_name}() call failed; signals() allows only one signal configuration at a time \n\ + see https://godot-rust.github.io/book/register/signals.html#one-signal-at-a-time" + ) + }); + + Self::new(obj, signal_name) + } + + // pub fn extract_user( + // this: &mut Option>, + // signal_name: &'static str, + // ) -> TypedSignal<'c, C, Ps> { + // TypedSignal::extract(this, signal_name) + // } + + // Currently only invoked from godot-core classes, or from UserSignalObject::into_typed_signal. + // When making public, make also #[doc(hidden)]. + fn new(object: C::__SignalObj<'c>, name: &'static str) -> Self { Self { - owner, + object, name: Cow::Borrowed(name), _signature: PhantomData, } } pub(crate) fn receiver_object(&self) -> Gd { - self.owner.to_owned_object() + let object = self.object.to_owned_object(); + + // Potential optimization: downcast could use a new private Gd::unchecked_cast(). + // try_cast().unwrap_unchecked() won't be that efficient due to internal code path. + object.cast() } /// Emit the signal with the given parameters. @@ -114,7 +116,7 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { pub fn emit_tuple(&mut self, args: Ps) { let name = self.name.as_ref(); - self.owner.with_object_mut(|obj| { + self.object.with_object_mut(|obj| { obj.emit_signal(name, &args.to_variant_array()); }); } @@ -145,16 +147,17 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { /// /// To connect to methods on the same object that declares the `#[signal]`, use [`connect_self()`][Self::connect_self]. \ /// If you need cross-thread signals or connect flags, use [`connect_builder()`][Self::connect_builder]. - pub fn connect_obj(&mut self, object: &Gd, mut function: F) + pub fn connect_obj(&mut self, object: &impl ToSignalObj, mut method: F) where OtherC: GodotClass + Bounds, for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps>, { - let mut gd = object.clone(); + let mut gd = object.to_signal_obj(); + // let mut gd = gd.to_owned_object(); let godot_fn = make_godot_fn(move |args| { let mut instance = gd.bind_mut(); let instance = &mut *instance; - function.call(instance, args); + method.call(instance, args); }); self.inner_connect_godot_fn::(godot_fn); @@ -180,7 +183,7 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { let callable = Callable::from_local_fn(&callable_name, godot_fn); let signal_name = self.name.as_ref(); - self.owner.with_object_mut(|obj| { + self.object.with_object_mut(|obj| { obj.connect(signal_name, &callable); }); } @@ -198,7 +201,7 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { let signal_name = self.name.as_ref(); - self.owner.with_object_mut(|obj| { + self.object.with_object_mut(|obj| { let mut c = obj.connect_ex(signal_name, callable); if let Some(flags) = flags { c = c.flags(flags.ord() as u32); @@ -221,7 +224,7 @@ impl TypedSignal<'_, C, Ps> { where for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, { - let mut gd = self.owner.to_owned_object(); + let mut gd = self.receiver_object(); let godot_fn = make_godot_fn(move |args| { let mut instance = gd.bind_mut(); let instance = &mut *instance; diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 35c6a3999..cc6d96af5 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -107,7 +107,7 @@ pub fn transform_inherent_impl( // For each #[func] in this impl block, create one constant. let func_name_constants = make_funcs_collection_constants(&funcs, &class_name); - let (signal_registrations, signals_collection_struct) = + let (signal_registrations, signal_symbol_types) = make_signal_registrations(&signals, &class_name, &class_name_obj)?; #[cfg(feature = "codegen-full")] @@ -186,7 +186,7 @@ pub fn transform_inherent_impl( impl #funcs_collection { #( #func_name_constants )* } - #signals_collection_struct + #signal_symbol_types }; Ok(result) diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index 499f5efa7..1f1360a45 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -9,8 +9,8 @@ use crate::util::bail; use crate::{util, ParseResult}; -use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; +use proc_macro2::{Delimiter, Ident, TokenStream, TokenTree}; +use quote::{format_ident, quote, ToTokens}; /// Holds information known from a signal's definition pub struct SignalDefinition { @@ -24,11 +24,75 @@ pub struct SignalDefinition { pub has_builder: bool, } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Limits the visibility of signals to a few "blessed" syntaxes, excluding `pub(in PATH)`. +/// +/// This is necessary because the signal collection (containing all signals) must have the widest visibility of any signal, and for +/// that a total order must exist. `in` paths cannot be semantically analyzed by proc-macros. +/// +/// Documented in . +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum SignalVisibility { + Priv, + PubSuper, + PubCrate, + Pub, +} + +impl SignalVisibility { + pub fn try_parse(tokens: Option<&venial::VisMarker>) -> Option { + // No tokens: private. + let Some(tokens) = tokens else { + return Some(Self::Priv); + }; + + debug_assert_eq!(tokens.tk_token1.to_string(), "pub"); + + // Early exit if `pub` without following `(...)` group. + let group = match &tokens.tk_token2 { + None => return Some(Self::Pub), + Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => group, + _ => return None, + }; + + // `pub(...)` -> extract `...` part. + let mut tokens_in_paren = group.stream().into_iter(); + let vis = match tokens_in_paren.next() { + Some(TokenTree::Ident(ident)) if ident == "super" => Self::PubSuper, + Some(TokenTree::Ident(ident)) if ident == "crate" => Self::PubCrate, + _ => return None, + }; + + // No follow-up tokens allowed. + if tokens_in_paren.next().is_some() { + return None; + } + + Some(vis) + } +} + +impl ToTokens for SignalVisibility { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Priv => { /* do nothing */ } + Self::Pub => tokens.extend(quote! { pub }), + Self::PubSuper => tokens.extend(quote! { pub(super) }), + Self::PubCrate => tokens.extend(quote! { pub(crate) }), + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// Extracted syntax info for a declared signal. struct SignalDetails<'a> { /// `fn my_signal(i: i32, s: GString)` -- simplified from original declaration. fn_signature: &'a venial::Function, /// `MyClass` + #[allow(unused)] + // Current impl doesn't need it, but we already have it, too annoying to add/remove during refactors. class_name: &'a Ident, /// `i32`, `GString` param_types: Vec, @@ -48,6 +112,8 @@ struct SignalDetails<'a> { individual_struct_name: Ident, /// Visibility, e.g. `pub(crate)` vis_marker: Option, + // /// Detected visibility as strongly typed enum. + // vis_classified: SignalVisibility, } impl<'a> SignalDetails<'a> { @@ -83,6 +149,15 @@ impl<'a> SignalDetails<'a> { let signal_name = &fn_signature.name; let individual_struct_name = format_ident!("__godot_Signal_{}_{}", class_name, signal_name); + let vis_marker = &fn_signature.vis_marker; + let Some(_vis_classified) = SignalVisibility::try_parse(vis_marker.as_ref()) else { + return bail!( + vis_marker, + "invalid visibility `{}` for #[signal]; supported are `pub`, `pub(crate)`, `pub(super)` and private (no visibility marker)", + vis_marker.to_token_stream().to_string() + ); + }; + Ok(Self { fn_signature, class_name, @@ -94,18 +169,26 @@ impl<'a> SignalDetails<'a> { signal_name_str: fn_signature.name.to_string(), signal_cfg_attrs, individual_struct_name, - vis_marker: fn_signature.vis_marker.clone(), + vis_marker: vis_marker.clone(), + // vis_classified, }) } } +/// Returns tuple of: +/// * Code registering signals with Godot engine. +/// * Symbolic APIs for signals (collection struct + individual signal types + `WithSignal`/`Deref` impls). pub fn make_signal_registrations( signals: &[SignalDefinition], class_name: &Ident, class_name_obj: &TokenStream, ) -> ParseResult<(Vec, Option)> { let mut signal_registrations = Vec::new(); + + #[cfg(since_api = "4.2")] let mut collection_api = SignalCollection::default(); + // #[cfg(since_api = "4.2")] + // let mut max_visibility = SignalVisibility::Priv; for signal in signals { let SignalDefinition { @@ -120,15 +203,19 @@ pub fn make_signal_registrations( #[cfg(since_api = "4.2")] if *has_builder { collection_api.extend_with(&details); + // max_visibility = max_visibility.max(details.vis_classified); } let registration = make_signal_registration(&details, class_name_obj); signal_registrations.push(registration); } - let struct_code = make_signal_collection(class_name, collection_api); + #[cfg(since_api = "4.2")] + let signal_symbols = make_signal_symbols(class_name, collection_api); + #[cfg(before_api = "4.2")] + let signal_symbols = None; - Ok((signal_registrations, struct_code)) + Ok((signal_registrations, signal_symbols)) } // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -150,7 +237,7 @@ fn make_signal_registration(details: &SignalDetails, class_name_obj: &TokenStrea [ // Don't use raw sys pointers directly; it's very easy to have objects going out of scope. #( - <#signature_tuple as godot::meta::VarcallSignatureTuple> + <#signature_tuple as ::godot::meta::VarcallSignatureTuple> ::param_property_info(#indexes, #param_names_str), )* ] @@ -213,9 +300,15 @@ impl SignalCollection { // // However, it would still lead to a compile error when declaring the individual signal struct `pub` (or any other // visibility that exceeds the class visibility). So, we can as well declare the visibility here. - #vis_marker fn #signal_name(self) -> #individual_struct_name<'c> { + #vis_marker fn #signal_name(&mut self) -> #individual_struct_name<'c, C> { #individual_struct_name { - typed: ::godot::register::TypedSignal::new(self.__internal_obj, #signal_name_str) + // __typed: ::godot::register::TypedSignal::new(self.__internal_obj, #signal_name_str) + // __typed: ::godot::register::TypedSignal::<'c, C, _>::new(self.__internal_obj, #signal_name_str) + // __typed: todo!() + + // __typed: self.__internal_obj.into_typed_signal(#signal_name_str) + __typed: ::godot::register::TypedSignal::<'c, C, _>::extract(&mut self.__internal_obj, #signal_name_str) + } } }); @@ -233,7 +326,7 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { let emit_params = &details.fn_signature.params; let SignalDetails { - class_name, + // class_name, param_names, param_tuple, signal_cfg_attrs, @@ -260,82 +353,212 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { #(#signal_cfg_attrs)* #[allow(non_camel_case_types)] #[doc(hidden)] // Signal struct is hidden, but the method returning it is not (IDE completion). - #vis_marker struct #individual_struct_name<'a> { + #vis_marker struct #individual_struct_name<'c, C: ::godot::obj::WithSignals> { #[doc(hidden)] - typed: ::godot::register::TypedSignal<'a, #class_name, #param_tuple>, + __typed: ::godot::register::TypedSignal<'c, C, #param_tuple>, } // Concrete convenience API is macro-based; many parts are delegated to TypedSignal via Deref/DerefMut. #(#signal_cfg_attrs)* - impl #individual_struct_name<'_> { + impl #individual_struct_name<'_, C> { pub fn emit(&mut self, #emit_params) { - self.typed.emit_tuple((#( #param_names, )*)); + self.__typed.emit_tuple((#( #param_names, )*)); } } #(#signal_cfg_attrs)* - impl<'c> std::ops::Deref for #individual_struct_name<'c> { - type Target = ::godot::register::TypedSignal<'c, #class_name, #param_tuple>; + impl<'c, C: ::godot::obj::WithSignals> std::ops::Deref for #individual_struct_name<'c, C> { + type Target = ::godot::register::TypedSignal<'c, C, #param_tuple>; fn deref(&self) -> &Self::Target { - &self.typed + &self.__typed } } #(#signal_cfg_attrs)* - impl std::ops::DerefMut for #individual_struct_name<'_> { + impl std::ops::DerefMut for #individual_struct_name<'_, C> { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.typed + &mut self.__typed } } } } -/// Generates a unspecified-name struct holding methods to access each signal. -fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> Option { - if collection.is_empty() { +/// Generates symbolic API for signals: +/// * collection (unspecified-name struct holding methods to access each signal) +/// * individual signal types +/// * trait impls +fn make_signal_symbols( + class_name: &Ident, + collection_api: SignalCollection, + // max_visibility: SignalVisibility, +) -> Option { + // Future note: if we add Rust->Rust inheritance, then the WithSignals trait must be unconditionally implemented. + if collection_api.is_empty() { return None; } let collection_struct_name = format_ident!("__godot_Signals_{}", class_name); - let collection_struct_methods = &collection.provider_methods; - let individual_structs = collection.individual_structs; + let collection_struct_methods = &collection_api.provider_methods; + let with_signals_impl = make_with_signals_impl(class_name, &collection_struct_name); + let upcast_deref_impl = make_upcast_deref_impl(class_name, &collection_struct_name); + let individual_structs = collection_api.individual_structs; + + // The collection cannot be `pub` because `Deref::Target` contains the class type, which leads to "leak private type" errors. + // We thus adopt the visibility of the #[derive(GodotClass)] struct, imported via macro trick. + // + // A previous approach (that cannot access the struct visibility) used "max visibility": the user decides which visibility is acceptable f + // or individual #[signal]s. They can all be at most the class visibility. Since we assume that decision is correct, the signal collection + // itself can also share the widest visibility of any #[signal]. This approach however still led to problems because there's a 2-way + // dependency: `impl WithSignals for MyClass` has an associated type `SignalCollection` that mentions the generated collection type. If + // that collection type has *lower* visibility than the class, we *also* run into "leak private type" errors. + + // Unrelated, we could use the following for encapsulation: + // #[cfg(since_api = "4.2")] + // mod #signal_mod_name { + // pub use super::*; + // ... // all the code below + // } + // #[cfg(since_api = "4.2")] + // pub use #signal_mod_name::*; + // + // This now makes signal types/methods invisible to the surrounding scope, so we'd need to adjust visibility in some cases: + // * private -> `pub(super)` + // * `pub(super)` -> pub(in super::super) + // + // Benefit of encapsulating would be: + // * No need for `#[doc(hidden)]` on internal symbols like fields. + // * #[cfg(since_api = "4.2")] would not need to be repeated. This is less of a problem if the #[cfg] is used inside the macro + // instead of generated code. + // * Less scope pollution (even though names are mangled). + // + // Downside is slightly higher complexity and introducing signals in secondary blocks becomes harder (although we could use another + // module name, we'd need a way to create unique names). + + let visibility_macro = util::format_class_visibility_macro(class_name); let code = quote! { - #[allow(non_camel_case_types)] - #[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs. - pub struct #collection_struct_name<'c> { - // To allow external call in the future (given Gd, not self), this could be an enum with either BaseMut or &mut Gd/&mut T. - #[doc(hidden)] // Necessary because it's in the same scope as the user-defined class, so appearing in IDE completion. - __internal_obj: ::godot::register::UserSignalObj<'c, #class_name> + #visibility_macro! { + #[allow(non_camel_case_types)] + #[doc(hidden)] // Only on struct, not methods, to allow completion in IDEs. + struct #collection_struct_name<'c, C> { + // Hiding necessary because it's in the same scope as the user-defined class, so appearing in IDE completion. + #[doc(hidden)] + __internal_obj: Option<::godot::private::UserSignalObject<'c, C>> + } } - impl<'c> #collection_struct_name<'c> { + impl<'c, C> #collection_struct_name<'c, C> + where // bounds: see UserSignalObject::into_typed_signal(). + C: ::godot::obj::WithUserSignals + + ::godot::obj::WithSignals<__SignalObj<'c> = ::godot::private::UserSignalObject<'c, C>>, + { #( #collection_struct_methods )* } + #with_signals_impl + #upcast_deref_impl + #( #individual_structs )* + }; + + Some(code) +} + +fn make_with_signals_impl(class_name: &Ident, collection_struct_name: &Ident) -> TokenStream { + quote! { impl ::godot::obj::WithSignals for #class_name { - type SignalCollection<'c> = #collection_struct_name<'c>; + type SignalCollection<'c, C: ::godot::obj::WithSignals> = #collection_struct_name<'c, C>; + #[doc(hidden)] - type __SignalObject<'c> = ::godot::register::UserSignalObj<'c, Self>; + type __SignalObj<'c> = ::godot::private::UserSignalObject<'c, Self>; #[doc(hidden)] - fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_> { + fn __signals_from_external(external: &mut Gd) -> Self::SignalCollection<'_, Self> { Self::SignalCollection { - __internal_obj: ::godot::register::UserSignalObj::External { gd: external.clone() } + __internal_obj: Some(::godot::private::UserSignalObject::External { + gd: external.clone().upcast::() + }) } } } impl ::godot::obj::WithUserSignals for #class_name { - fn signals(&mut self) -> Self::SignalCollection<'_> { + fn signals(&mut self) -> Self::SignalCollection<'_, Self> { Self::SignalCollection { - __internal_obj: ::godot::register::UserSignalObj::Internal { obj_mut: self } + __internal_obj: Some(::godot::private::UserSignalObject::Internal { self_mut: self }) } } } + } +} - #( #individual_structs )* - }; - Some(code) +fn make_upcast_deref_impl(class_name: &Ident, collection_struct_name: &Ident) -> TokenStream { + quote! { + impl<'c, C: ::godot::obj::WithSignals> std::ops::Deref for #collection_struct_name<'c, C> { + type Target = < + < + #class_name as ::godot::obj::GodotClass + >::Base as ::godot::obj::WithSignals + >::SignalCollection<'c, C>; + + fn deref(&self) -> &Self::Target { + type Derived = #class_name; + ::godot::private::signal_collection_to_base::(self) + } + } + + impl<'c, C: ::godot::obj::WithSignals> std::ops::DerefMut for #collection_struct_name<'c, C> { + fn deref_mut(&mut self) -> &mut Self::Target { + type Derived = #class_name; + ::godot::private::signal_collection_to_base_mut::(self) + } + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_signal_visibility() { + #[rustfmt::skip] + let list = [ + (quote! { pub }, Some(SignalVisibility::Pub)), + (quote! { pub(crate) }, Some(SignalVisibility::PubCrate)), + (quote! {}, Some(SignalVisibility::Priv)), + (quote! { pub(super) }, Some(SignalVisibility::PubSuper)), + (quote! { pub(self) }, None), // not supported (equivalent to private) + (quote! { pub(in crate::foo) }, None), + ]; + + let parsed = list + .iter() + .map(|(vis, _)| { + // Dummy function, because venial has no per-item parser in public API. + let item = venial::parse_item(quote! { + #vis fn f() {} + }); + + let Ok(venial::Item::Function(f)) = item else { + panic!("expected function") + }; + + SignalVisibility::try_parse(f.vis_marker.as_ref()) + }) + .collect::>(); + + for ((_, expected), actual) in list.iter().zip(parsed.iter()) { + assert_eq!(expected, actual); + } + } + + #[test] + fn signal_visibility_order() { + assert!(SignalVisibility::Pub > SignalVisibility::PubCrate); + assert!(SignalVisibility::PubCrate > SignalVisibility::PubSuper); + assert!(SignalVisibility::PubSuper > SignalVisibility::Priv); + } } diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index c38235ce0..5cd42138f 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -5,9 +5,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Ident, Punct, TokenStream}; -use quote::{format_ident, quote, quote_spanned}; - use crate::class::{ make_property_impl, make_virtual_callback, BeforeKind, Field, FieldCond, FieldDefault, FieldExport, FieldVar, Fields, SignatureInfo, @@ -17,6 +14,8 @@ use crate::util::{ require_api_version, KvParser, }; use crate::{handle_mutually_exclusive_keys, util, ParseResult}; +use proc_macro2::{Ident, Punct, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class = item.as_struct().ok_or_else(|| { @@ -143,6 +142,8 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { pub struct #funcs_collection_struct_name {} }; + let visibility_macro = make_visibility_macro(class_name, class.vis_marker.as_ref()); + Ok(quote! { impl ::godot::obj::GodotClass for #class_name { type Base = #base_class; @@ -172,6 +173,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #godot_exports_impl #user_class_impl #init_expecter + #visibility_macro #( #deprecations )* #( #errors )* @@ -185,6 +187,30 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { }) } +/// Generates code for a decl-macro, which takes any item and prepends it with the visibility marker of the class. +/// +/// Used to access the visibility of the class in other proc-macros like `#[godot_api]`. +fn make_visibility_macro( + class_name: &Ident, + vis_marker: Option<&venial::VisMarker>, +) -> TokenStream { + let macro_name = util::format_class_visibility_macro(class_name); + + quote! { + macro_rules! #macro_name { + ( + $( #[$meta:meta] )* + struct $( $tt:tt )+ + ) => { + $( #[$meta] )* + #vis_marker struct $( $tt )+ + }; + + // Can be expanded to `fn` etc. if needed. + } + } +} + /// Checks at compile time that a function with the given name exists on `Self`. #[must_use] pub fn make_existence_check(ident: &Ident) -> TokenStream { @@ -265,7 +291,7 @@ fn make_onready_init(all_fields: &[Field]) -> TokenStream { if !onready_fields.is_empty() { quote! { { - let base = ::to_gd(self).upcast(); + let base = ::to_gd(self).upcast(); #( #onready_fields )* } } diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index ec0cc1d9f..be3081e4d 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -407,3 +407,8 @@ pub fn format_funcs_collection_constant(_class_name: &Ident, func_name: &Ident) pub fn format_funcs_collection_struct(class_name: &Ident) -> Ident { format_ident!("__godot_{class_name}_Funcs") } + +/// Returns the name of the macro used to communicate the `struct` (class) visibility to other symbols. +pub fn format_class_visibility_macro(class_name: &Ident) -> Ident { + format_ident!("__godot_{class_name}_vis_macro") +} diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 75c9bf850..6d8bda7aa 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -184,7 +184,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_core::registry::signal::*; + pub use godot_core::registry::signal::re_export::*; pub use godot_macros::{godot_api, godot_dyn, Export, GodotClass, GodotConvert, Var}; #[cfg(feature = "__codegen-full")] diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index edb2def6d..418778a56 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -7,7 +7,7 @@ use crate::framework::itest; use godot::builtin::{GString, Signal, StringName}; -use godot::classes::{Node, Object, RefCounted}; +use godot::classes::{Node, Node3D, Object, RefCounted}; use godot::meta::ToGodot; use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd}; use godot::register::{godot_api, GodotClass}; @@ -226,15 +226,17 @@ fn signal_symbols_engine(ctx: &crate::framework::TestContext) { let mut node = Node::new_alloc(); ctx.scene_tree.clone().add_child(&node); - // Deliberately declare here, because there was a bug with wrong lifetime, which would not compile due to early-dropped temporary. - let mut signals_in_node = node.signals(); - let mut renamed = signals_in_node.renamed(); - let mut entered = signals_in_node.child_entered_tree(); - + // API allows to only modify one signal at a time (borrowing &mut self). + let mut renamed = node.signals().renamed(); let renamed_count = Rc::new(Cell::new(0)); - let entered_tracker = Rc::new(RefCell::new(None)); { let renamed_count = renamed_count.clone(); + renamed.connect(move || renamed_count.set(renamed_count.get() + 1)); + } + + let mut entered = node.signals().child_entered_tree(); + let entered_tracker = Rc::new(RefCell::new(None)); + { let entered_tracker = entered_tracker.clone(); entered @@ -243,8 +245,6 @@ fn signal_symbols_engine(ctx: &crate::framework::TestContext) { *entered_tracker.borrow_mut() = Some(node); }) .done(); - - renamed.connect(move || renamed_count.set(renamed_count.get() + 1)); } // Apply changes, triggering signals. @@ -269,6 +269,51 @@ fn signal_symbols_engine(ctx: &crate::framework::TestContext) { node.free(); } +// Test that Node signals are accessible from a derived class. +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_engine_inherited(ctx: &crate::framework::TestContext) { + let mut node = Emitter::new_alloc(); + + // Add to tree, so signals are propagated. + ctx.scene_tree.clone().add_child(&node); + + let mut sig = node.signals().renamed(); + sig.connect_self(|this: &mut Emitter| { + this.last_received_int = 887; + }); + + node.set_name("new name"); + + assert_eq!(node.bind().last_received_int, 887); + + // Remove from tree for other tests. + node.free(); +} + +// Test that Node signals are accessible from a derived class, with Node3D middleman. +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_engine_inherited_indirect(ctx: &crate::framework::TestContext) { + let original = Emitter::new_alloc(); + let mut node = original.clone().upcast::(); + + // Add to tree, so signals are propagated. + ctx.scene_tree.clone().add_child(&node); + + let mut sig = node.signals().renamed(); + sig.connect_obj(&original, |this: &mut Emitter| { + this.last_received_int = 887; + }); + + node.set_name("new name"); + + assert_eq!(original.bind().last_received_int, 887); + + // Remove from tree for other tests. + node.free(); +} + #[itest] fn signal_construction_and_id() { let mut object = RefCounted::new_gd(); @@ -303,9 +348,9 @@ mod emitter { use godot::obj::WithUserSignals; #[derive(GodotClass)] - #[class(init, base=Object)] + #[class(init, base=Node3D)] // Node instead of Object to test some signals defined in superclasses. pub struct Emitter { - _base: Base, + _base: Base, #[cfg(since_api = "4.2")] pub last_received_int: i64, } @@ -401,6 +446,22 @@ impl Receiver { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +// Class which is deliberately `pub` but has only private `#[signal]` declaration. +// Regression test, as this caused "leaked private types" in the past. +#[derive(GodotClass)] +#[class(init, base=Object)] +pub struct PubClassPrivSignal { + _base: Base, +} + +#[godot_api] +impl PubClassPrivSignal { + #[signal] + fn private_signal(); +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // 4.2+ custom callables