diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 4d8790ca9..d18396c46 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -85,6 +85,7 @@ jobs: RUSTDOCFLAGS: > -D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links -D rustdoc::invalid-codeblock-attributes -D rustdoc::invalid-rust-codeblocks -D rustdoc::invalid-html-tags -D rustdoc::bare-urls -D rustdoc::unescaped-backticks + -D warnings run: cargo doc -p godot --ignore-rust-version diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index a995166b8..a72c2672e 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -146,9 +146,9 @@ pub use crate::{array, dict, real, reals, varray}; // Re-export generated enums. pub use crate::gen::central::global_reexported_enums::{Corner, EulerOrder, Side, VariantOperator}; -pub use crate::sys::VariantType; // Not yet public. pub(crate) use crate::gen::central::VariantDispatch; +pub use crate::sys::VariantType; #[doc(hidden)] pub mod __prelude_reexport { diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index a6f72a1ea..ba487b03d 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -47,7 +47,7 @@ pub(crate) fn display_string( obj: &Gd, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - let string: GString = obj.raw.as_object().to_string(); + let string: GString = obj.raw.as_object_ref().to_string(); ::fmt(&string, f) } diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index e516df64f..8782dab2e 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -60,6 +60,9 @@ pub use class_name::ClassName; pub use godot_convert::{FromGodot, GodotConvert, ToGodot}; pub use traits::{ArrayElement, GodotType, PackedArrayElement}; +#[cfg(since_api = "4.2")] +pub use crate::registry::signal::variadic::ParamTuple; + pub(crate) use array_type_info::ArrayTypeInfo; pub(crate) use traits::{ element_godot_type_name, element_variant_type, GodotFfiVariant, GodotNullableFfi, diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 53a609c71..c7bff68e3 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -20,7 +20,7 @@ use crate::meta::{ }; use crate::obj::{ bounds, cap, Bounds, DynGd, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId, - RawGd, + RawGd, WithSignals, }; use crate::private::callbacks; use crate::registry::property::{object_export_element_type_string, Export, Var}; @@ -327,6 +327,11 @@ impl Gd { .expect("Upcast to Object failed. This is a bug; please report it.") } + /// Equivalent to [`upcast_mut::()`][Self::upcast_mut], but without bounds. + pub(crate) fn upcast_object_mut(&mut self) -> &mut classes::Object { + self.raw.as_object_mut() + } + /// **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 @@ -700,6 +705,23 @@ where } } +impl Gd +where + T: WithSignals, +{ + /// Access user-defined signals of this object. + /// + /// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized + /// API for connecting and emitting signals in a type-safe way. This method is the equivalent of [`WithSignals::signals()`], but when + /// called externally (not from `self`). If you are within the `impl` of a class, use `self.signals()` directly instead. + /// + /// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a + /// walkthrough. + pub fn signals(&self) -> T::SignalCollection<'_> { + T::__signals_from_external(self) + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Trait impls diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index f4c324b6d..19a66c3c7 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -210,11 +210,16 @@ impl RawGd { // self.as_target_mut() // } - pub(crate) fn as_object(&self) -> &classes::Object { + pub(crate) fn as_object_ref(&self) -> &classes::Object { // SAFETY: Object is always a valid upcast target. unsafe { self.as_upcast_ref() } } + pub(crate) fn as_object_mut(&mut self) -> &mut classes::Object { + // SAFETY: Object is always a valid upcast target. + unsafe { self.as_upcast_mut() } + } + /// # Panics /// If this `RawGd` is null. In Debug mode, sanity checks (valid upcast, ID comparisons) can also lead to panics. /// diff --git a/godot-core/src/obj/script.rs b/godot-core/src/obj/script.rs index 3e22b126b..0bcd1d179 100644 --- a/godot-core/src/obj/script.rs +++ b/godot-core/src/obj/script.rs @@ -120,9 +120,10 @@ pub trait ScriptInstance: Sized { args: &[&Variant], ) -> Result; - /// Identifies the script instance as a placeholder. If this function and - /// [IScriptExtension::is_placeholder_fallback_enabled](crate::classes::IScriptExtension::is_placeholder_fallback_enabled) return true, - /// Godot will call [`Self::property_set_fallback`] instead of [`Self::set_property`]. + /// Identifies the script instance as a placeholder, routing property writes to a fallback if applicable. + /// + /// If this function and [IScriptExtension::is_placeholder_fallback_enabled] return true, Godot will call [`Self::property_set_fallback`] + /// instead of [`Self::set_property`]. fn is_placeholder(&self) -> bool; /// Validation function for the engine to verify if the script exposes a certain method. @@ -157,8 +158,7 @@ pub trait ScriptInstance: Sized { /// The engine may call this function if it failed to get a property value via [`ScriptInstance::get_property`] or the native type's getter. fn property_get_fallback(&self, name: StringName) -> Option; - /// The engine may call this function if - /// [`IScriptExtension::is_placeholder_fallback_enabled`](crate::classes::IScriptExtension::is_placeholder_fallback_enabled) is enabled. + /// The engine may call this function if [`IScriptExtension::is_placeholder_fallback_enabled`] is enabled. fn property_set_fallback(this: SiMut, name: StringName, value: &Variant) -> bool; /// This function will be called to handle calls to [`Object::get_method_argument_count`](crate::classes::Object::get_method_argument_count) @@ -347,7 +347,7 @@ pub unsafe fn create_script_instance( /// This function both checks if the passed script matches the one currently assigned to the passed object, as well as verifies that /// there is an instance for the script. /// -/// Use this function to implement [`IScriptExtension::instance_has`](crate::classes::IScriptExtension::instance_has). +/// Use this function to implement [`IScriptExtension::instance_has`]. #[cfg(since_api = "4.2")] pub fn script_instance_exists(object: &Gd, script: &Gd) -> bool where diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 1713fa248..7aabdc6c8 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -261,8 +261,8 @@ pub trait IndexEnum: EngineEnum { // Possible alternative for builder APIs, although even less ergonomic: Base could be Base and return Gd. #[diagnostic::on_unimplemented( message = "Class `{Self}` requires a `Base` field", - label = "missing field `_base: Base<...>`", - note = "A base field is required to access the base from within `self`, for script-virtual functions or #[rpc] methods", + label = "missing field `_base: Base<...>` in struct declaration", + note = "A base field is required to access the base from within `self`, as well as for #[signal], #[rpc] and #[func(virtual)]", note = "see also: https://godot-rust.github.io/book/register/classes.html#the-base-field" )] pub trait WithBaseField: GodotClass + Bounds { @@ -428,6 +428,40 @@ pub trait WithBaseField: GodotClass + Bounds { } } +pub trait WithSignals: WithBaseField { + type SignalCollection<'a>; + + /// Access user-defined signals of the current object `self`. + /// + /// For classes that have at least one `#[signal]` defined, returns a collection of signal names. Each returned signal has a specialized + /// API for connecting and emitting signals in a type-safe way. If you need to access signals from outside (given a `Gd` pointer), use + /// [`Gd::signals()`] instead. + /// + /// If you haven't already, read the [book chapter about signals](https://godot-rust.github.io/book/register/signals.html) for a + /// walkthrough. + /// + /// # Provided API + /// + /// The returned collection provides a method for each signal, with the same name as the corresponding `#[signal]`. \ + /// For example, if you have... + /// ```ignore + /// #[signal] + /// fn damage_taken(&mut self, amount: i32); + /// ``` + /// ...then you can access the signal as `self.signals().damage_taken()`, which returns an object with the following API: + /// + /// | Method signature | Description | + /// |------------------|-------------| + /// | `connect(f: impl FnMut(i32))` | Connects global or associated function, or a closure. | + /// | `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<'_>; + + #[doc(hidden)] + fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_>; +} + /// Extension trait for all reference-counted classes. pub trait NewGd: GodotClass { /// Return a new, ref-counted `Gd` containing a default-constructed instance. diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 766db1f7f..033b8155e 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -26,7 +26,6 @@ use std::sync::atomic; #[cfg(debug_assertions)] use std::sync::{Arc, Mutex}; use sys::Global; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables diff --git a/godot-core/src/registry/mod.rs b/godot-core/src/registry/mod.rs index 9ce2a155d..e1eb16326 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -15,6 +15,13 @@ pub mod method; pub mod plugin; pub mod property; +#[cfg(since_api = "4.2")] +pub mod signal; + +// Contents re-exported in `godot` crate; just keep empty. +#[cfg(before_api = "4.2")] +pub mod signal {} + // RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature. #[cfg(feature = "codegen-full")] mod rpc_config; diff --git a/godot-core/src/registry/signal/connect_builder.rs b/godot-core/src/registry/signal/connect_builder.rs new file mode 100644 index 000000000..f27477c60 --- /dev/null +++ b/godot-core/src/registry/signal/connect_builder.rs @@ -0,0 +1,351 @@ +/* + * 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/. + */ + +use crate::builtin::{Callable, GString, Variant}; +use crate::classes::object::ConnectFlags; +use crate::meta; +use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::registry::signal::{SignalReceiver, TypedSignal}; + +/// Type-state builder for customizing signal connections. +/// +/// Allows a high degree of customization for connecting signals, while maintaining complete type safety. +/// +///
+/// Warning: +/// Exact type parameters are subject to change and not part of the public API. We could annotate #[doc(hidden)], but it would make +/// things harder to understand. Thus, try not to name the ConnectBuilder type in your code; most connection setup doesn't need it. +///
+// If naming the type becomes a requirement, there may be some options: +// - Use a type alias in the module or TypedSignal, exposing only public parameters. This would work for constructor, but not all transformations. +// - Pack multiple types together into "type lists", i.e. custom structs carrying the type state. For a user, this would appear as one type, +// - which could also be #[doc(hidden)]. However, this may make the trait resolution more complex and worsen error messages, so not done now. +/// +/// # Builder stages +/// +/// The builder API has a well-defined flow and is separated in stages. In each stage, you have certain builder methods available that you can +/// or must call, before advancing to the next stage. Check the instructions. +/// +/// ## Stage 1 (required) +/// Choose one: +/// - [`function`][Self::function]: Connect a global/associated function or a closure. +/// - [`object_self`][Self::object_self]: If you want to connect a method (in stage 2), running on the same object as the signal. +/// - [`object`][Self::object]: If you want to connect a method, running on a separate object. +/// +/// ## Stage 2 (conditional) +/// Required iff _(if and only if)_ `object_self` or `object` was called in stage 1. +/// - [`method_mut`][Self::method_mut]: Connect a `&mut self` method. +/// - [`method_immut`][Self::method_immut]: Connect a `&self` method. +/// +/// ## Stage 3 +/// All these methods are optional, and they can be combined. +// Use HTML link due to conditional compilation; renders badly if target symbol is unavailable. +/// - [`sync`](#method.sync): If the signal connection should be callable across threads. \ +/// Requires `Send` + `Sync` bounds on the provided function/method, and is only available for the `experimental-threads` Cargo feature. +/// - [`name`][Self::name]: Name of the `Callable` (for debug purposes). \ +/// If not specified, the Rust function name is used. This is typically a good default, but not very readable for closures. +/// - [`flags`][Self::flags]: Provide one or multiple [`ConnectFlags`][crate::classes::object::ConnectFlags], possibly combined with bitwise OR. +/// +/// ## Final stage +/// - [`done`][Self::done]: Finalize the connection. Consumes the builder and registers the signal with Godot. +/// +#[must_use] +pub struct ConnectBuilder<'ts, 'c, CSig: GodotClass, CRcv, Ps, GodotFn> { + parent_sig: &'ts mut TypedSignal<'c, CSig, Ps>, + data: BuilderData, + + // Type-state data. + receiver_obj: CRcv, + godot_fn: GodotFn, +} + +impl<'ts, 'c, CSig: WithBaseField, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, CSig, (), Ps, ()> { + pub(super) fn new(parent_sig: &'ts mut TypedSignal<'c, CSig, Ps>) -> Self { + ConnectBuilder { + parent_sig, + data: BuilderData::default(), + godot_fn: (), + receiver_obj: (), + } + } + + /// **Stage 1:** global/associated function or closure. + pub fn function( + self, + mut function: F, + ) -> ConnectBuilder< + 'ts, + 'c, + /* CSig = */ CSig, + /* CRcv = */ (), + /* Ps = */ Ps, + /* GodotFn= */ impl FnMut(&[&Variant]) -> Result + 'static, + > + where + F: SignalReceiver<(), Ps>, + { + let godot_fn = make_godot_fn(move |args| { + function.call((), args); + }); + + ConnectBuilder { + parent_sig: self.parent_sig, + data: self.data.with_callable_name::(), + godot_fn, + receiver_obj: (), + } + } + + /// **Stage 1:** prepare for a method taking `self` (the class declaring the `#[signal]`). + pub fn object_self(self) -> ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> { + let receiver_obj: Gd = self.parent_sig.receiver_object(); + + ConnectBuilder { + parent_sig: self.parent_sig, + data: self.data, + godot_fn: (), + receiver_obj, + } + } + + /// **Stage 1:** prepare for a method taking any `Gd` object. + pub fn object( + self, + object: &Gd, + ) -> ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> { + ConnectBuilder { + parent_sig: self.parent_sig, + data: self.data, + godot_fn: (), + receiver_obj: object.clone(), + } + } +} + +impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> + ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> +{ + /// **Stage 2:** method taking `&mut self`. + pub fn method_mut( + self, + mut method_with_mut_self: F, + ) -> ConnectBuilder< + 'ts, + 'c, + /* CSig = */ CSig, + /* CRcv: again reset to unit type, after object has been captured in closure. */ + (), + /* Ps = */ Ps, + /* GodotFn = */ impl FnMut(&[&Variant]) -> Result, + > + where + CRcv: GodotClass + Bounds, + for<'c_rcv> F: SignalReceiver<&'c_rcv mut CRcv, Ps>, + { + let mut gd: Gd = self.receiver_obj; + let godot_fn = make_godot_fn(move |args| { + let mut guard = gd.bind_mut(); + let instance = &mut *guard; + method_with_mut_self.call(instance, args); + }); + + ConnectBuilder { + parent_sig: self.parent_sig, + data: self.data.with_callable_name::(), + godot_fn, + receiver_obj: (), + } + } + + /// **Stage 2:** method taking `&self`. + pub fn method_immut( + self, + mut method_with_shared_self: F, + ) -> ConnectBuilder< + 'ts, + 'c, + /* CSig = */ CSig, + /* CRcv: again reset to unit type, after object has been captured in closure. */ + (), + /* Ps = */ Ps, + /* GodotFn = */ impl FnMut(&[&Variant]) -> Result, + > + where + CRcv: GodotClass + Bounds, + for<'c_rcv> F: SignalReceiver<&'c_rcv CRcv, Ps>, + { + let gd: Gd = self.receiver_obj; + let godot_fn = make_godot_fn(move |args| { + let guard = gd.bind(); + let instance = &*guard; + method_with_shared_self.call(instance, args); + }); + + ConnectBuilder { + parent_sig: self.parent_sig, + data: self.data.with_callable_name::(), + godot_fn, + receiver_obj: (), + } + } +} + +#[allow(clippy::needless_lifetimes)] // 'ts + 'c are used conditionally. +impl<'ts, 'c, CSig, CRcv, Ps, GodotFn> ConnectBuilder<'ts, 'c, CSig, CRcv, Ps, GodotFn> +where + CSig: WithBaseField, + Ps: meta::ParamTuple, + GodotFn: FnMut(&[&Variant]) -> Result + 'static, +{ + /// **Stage 3:** allow signal to be called across threads. + /// + /// Requires `Send` + `Sync` bounds on the previously provided function/method, and is only available for the `experimental-threads` + /// Cargo feature. + #[cfg(feature = "experimental-threads")] + pub fn sync( + self, + ) -> ConnectBuilder< + 'ts, + 'c, + /* CSig = */ CSig, + /* CRcv = */ CRcv, + /* Ps = */ Ps, + /* GodotFn = */ impl FnMut(&[&Variant]) -> Result, + > + where + // Why both Send+Sync: closure can not only impact another thread (Sync), but it's also possible to share such Callables across threads + // (Send) or even call them from multiple threads (Sync). We don't differentiate the fine-grained needs, it's either thread-safe or not. + GodotFn: Send + Sync, + { + let Self { + parent_sig, + mut data, + receiver_obj, + godot_fn, + } = self; + + assert!( + data.sync_callable.is_none(), + "sync() called twice on the same builder." + ); + + let dummy_fn = + |_variants: &[&Variant]| panic!("sync() closure should have been replaced by now."); + + data.sync_callable = Some(Callable::from_sync_fn(data.callable_name_ref(), godot_fn)); + + ConnectBuilder { + parent_sig, + data, + godot_fn: dummy_fn, + receiver_obj, + } + } + + /// **Stage 3:** Name of the `Callable`, mostly used for debugging. + /// + /// If not provided, the Rust type name of the function/method is used. + pub fn name(mut self, name: impl meta::AsArg) -> Self { + assert!( + self.data.callable_name.is_none(), + "name() called twice on the same builder." + ); + + meta::arg_into_owned!(name); + self.data.callable_name = Some(name); + self + } + + /// **Stage 3:** add one or multiple flags to the connection, possibly combined with `|` operator. + pub fn flags(mut self, flags: ConnectFlags) -> Self { + assert!( + self.data.connect_flags.is_none(), + "flags() called twice on the same builder." + ); + + self.data.connect_flags = Some(flags); + self + } + + /// Finalize the builder. + /// + /// Actually connects the signal with the provided function/method. Consumes this builder instance and returns the mutable borrow of + /// the parent [`TypedSignal`] for further use. + pub fn done(self) { + let Self { + parent_sig, + data, + godot_fn, + receiver_obj: _, + } = self; + + let callable_name = data.callable_name_ref(); + + // If sync() was previously called, use the already-existing callable, otherwise construct a local one now. + #[cfg(feature = "experimental-threads")] + let callable = match data.sync_callable { + Some(sync_callable) => sync_callable, + None => Callable::from_local_fn(callable_name, godot_fn), + }; + + #[cfg(not(feature = "experimental-threads"))] + let callable = Callable::from_local_fn(callable_name, godot_fn); + + parent_sig.inner_connect_untyped(&callable, data.connect_flags); + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Gathers all the non-typestate data, so that the builder can easily transfer it without manually moving each field. +#[derive(Default)] +struct BuilderData { + /// User-specified name; if not provided, the Rust RTTI type name of the function is used. + callable_name: Option, + + /// Godot connection flags. + connect_flags: Option, + + /// If [`sync()`][ConnectBuilder::sync] was called, then this already contains the populated closure. + #[cfg(feature = "experimental-threads")] + sync_callable: Option, +} + +impl BuilderData { + fn with_callable_name(mut self) -> Self { + self.callable_name = Some(make_callable_name::()); + self + } + + fn callable_name_ref(&self) -> &GString { + self.callable_name + .as_ref() + .expect("Signal connect name not set; this is a bug.") + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub(super) fn make_godot_fn(mut input: F) -> impl FnMut(&[&Variant]) -> Result +where + F: FnMut(Ps), + Ps: meta::ParamTuple, +{ + move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + input(args); + + Ok(Variant::nil()) + } +} + +pub(super) fn make_callable_name() -> GString { + // When using sys::short_type_name() in the future, make sure global "func" and member "MyClass::func" are rendered as such. + // PascalCase heuristic should then be good enough. + + std::any::type_name::().into() +} diff --git a/godot-core/src/registry/signal/mod.rs b/godot-core/src/registry/signal/mod.rs new file mode 100644 index 000000000..4a72a49cf --- /dev/null +++ b/godot-core/src/registry/signal/mod.rs @@ -0,0 +1,15 @@ +/* + * 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/. + */ + +mod connect_builder; +mod typed_signal; +pub(crate) mod variadic; + +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/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs new file mode 100644 index 000000000..7f7901b13 --- /dev/null +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -0,0 +1,210 @@ +/* + * 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/. + */ + +use crate::builtin::{Callable, Variant}; +use crate::classes::object::ConnectFlags; +use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::registry::signal::{make_callable_name, make_godot_fn, ConnectBuilder, SignalReceiver}; +use crate::{classes, meta}; +use std::borrow::Cow; +use std::marker::PhantomData; + +/// Links to a Godot object, either via reference (for `&mut self` uses) or via `Gd`. +#[doc(hidden)] +pub enum ObjectRef<'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 ObjectRef<'_, C> +where + C: WithBaseField, +{ + fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { + match self { + ObjectRef::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()), + ObjectRef::External { gd } => f(gd.upcast_object_mut()), + } + } + + fn to_owned(&self) -> Gd { + match self { + ObjectRef::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut), + ObjectRef::External { gd } => gd.clone(), + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Type-safe version of a Godot signal. +/// +/// Short-lived type, only valid in the scope of its surrounding object type `C`, for lifetime `'c`. The generic argument `Ps` represents +/// the parameters of the signal, thus ensuring the type safety. +/// +/// The [`WithSignals::signals()`][crate::obj::WithSignals::signals] collection returns multiple signals with distinct, code-generated types, +/// but they all implement `Deref` and `DerefMut` to `TypedSignal`. This allows you to either use the concrete APIs of the generated types, +/// or the more generic ones of `TypedSignal`. +/// +/// # Connecting a signal to a receiver +/// Receiver functions are functions that are called when a signal is emitted. You can connect a signal in many different ways: +/// - [`connect()`][Self::connect] for global functions, associated functions or closures. +/// - [`connect_self()`][Self::connect_self] for methods with `&mut self` as the first parameter. +/// - [`connect_obj()`][Self::connect_obj] for methods with any `Gd` (not `self`) as the first parameter. +/// - [`connect_builder()`][Self::connect_builder] for more complex setups. +/// +/// # Emitting a signal +/// Code-generated signal types provide a method `emit(...)`, which adopts the names and types of the `#[signal]` parameter list. +/// In most cases, that's the method you are looking for. +/// +/// For generic use, you can also use [`emit_tuple()`][Self::emit_tuple], which does not provide parameter names. +/// +/// # More information +/// 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: GodotClass, 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: ObjectRef<'c, C>, + name: Cow<'static, str>, + _signature: PhantomData, +} + +impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { + #[doc(hidden)] + pub fn new(owner: ObjectRef<'c, C>, name: &'static str) -> Self { + Self { + owner, + name: Cow::Borrowed(name), + _signature: PhantomData, + } + } + + pub(crate) fn receiver_object(&self) -> Gd { + self.owner.to_owned() + } + + /// Emit the signal with the given parameters. + /// + /// This is intended for generic use. Typically, you'll want to use the more specific `emit()` method of the code-generated signal + /// type, which also has named parameters. + pub fn emit_tuple(&mut self, args: Ps) { + let name = self.name.as_ref(); + + self.owner.with_object_mut(|obj| { + obj.emit_signal(name, &args.to_variant_array()); + }); + } + + /// Connect a non-member function (global function, associated function or closure). + /// + /// Example usages: + /// ```ignore + /// sig.connect(Self::static_func); + /// sig.connect(global_func); + /// sig.connect(|arg| { /* closure */ }); + /// ``` + /// + /// To connect to a method of the own object `self`, use [`connect_self()`][Self::connect_self]. \ + /// If you need cross-thread signals or connect flags, use [`connect_builder()`][Self::connect_builder]. + pub fn connect(&mut self, mut function: F) + where + F: SignalReceiver<(), Ps>, + { + let godot_fn = make_godot_fn(move |args| { + function.call((), args); + }); + + self.inner_connect_godot_fn::(godot_fn); + } + + /// Connect a method (member function) with `&mut self` as the first parameter. + /// + /// To connect to methods on other objects, use [`connect_obj()`][Self::connect_obj]. \ + /// If you need a `&self` receiver, cross-thread signals or connect flags, use [`connect_builder()`][Self::connect_builder]. + pub fn connect_self(&mut self, mut function: F) + where + for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, + { + let mut gd = self.owner.to_owned(); + let godot_fn = make_godot_fn(move |args| { + let mut instance = gd.bind_mut(); + let instance = &mut *instance; + function.call(instance, args); + }); + + self.inner_connect_godot_fn::(godot_fn); + } + + /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. + /// + /// 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) + where + OtherC: GodotClass + Bounds, + for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps>, + { + let mut gd = object.clone(); + let godot_fn = make_godot_fn(move |args| { + let mut instance = gd.bind_mut(); + let instance = &mut *instance; + function.call(instance, args); + }); + + self.inner_connect_godot_fn::(godot_fn); + } + + /// Fully customizable connection setup. + /// + /// The returned builder provides several methods to configure how to connect the signal. It needs to be finalized with a call to + /// [`ConnectBuilder::done()`]. + pub fn connect_builder(&mut self) -> ConnectBuilder<'_, 'c, C, (), Ps, ()> { + ConnectBuilder::new(self) + } + + /// Directly connect a Rust callable `godot_fn`, with a name based on `F`. + /// + /// This exists as a short-hand for the connect methods on [`TypedSignal`] and avoids the generic instantiation of the full-blown + /// type state builder for simple + common connections, thus hopefully being a tiny bit lighter on compile times. + fn inner_connect_godot_fn( + &mut self, + godot_fn: impl FnMut(&[&Variant]) -> Result + 'static, + ) { + let callable_name = make_callable_name::(); + let callable = Callable::from_local_fn(&callable_name, godot_fn); + + let signal_name = self.name.as_ref(); + self.owner.with_object_mut(|obj| { + obj.connect(signal_name, &callable); + }); + } + + /// Connect an untyped callable, with optional flags. + /// + /// Used by [`ConnectBuilder::done()`]. Any other type-state (such as thread-local/sync, callable debug name, etc.) are baked into + /// `callable` and thus type-erased into runtime logic. + pub(super) fn inner_connect_untyped( + &mut self, + callable: &Callable, + flags: Option, + ) { + use crate::obj::EngineBitfield; + + let signal_name = self.name.as_ref(); + + self.owner.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); + } + c.done(); + }); + } +} diff --git a/godot-core/src/registry/signal/variadic.rs b/godot-core/src/registry/signal/variadic.rs new file mode 100644 index 000000000..94912011c --- /dev/null +++ b/godot-core/src/registry/signal/variadic.rs @@ -0,0 +1,113 @@ +/* + * 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/. + */ + +//! Emulates variadic argument lists (via tuples), related to functions and signals. + +// https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions +// +// Could be generalized with R return type, and not special-casing `self`. But keep simple until actually needed. + +use crate::builtin::Variant; +use crate::meta; + +/// Trait that is implemented for functions that can be connected to signals. +/// +// Direct RustDoc link doesn't work, for whatever reason again... +/// This is used in [`ConnectBuilder`](struct.ConnectBuilder.html). There are three variations of the `I` (instance) parameter: +/// - `()` for global and associated ("static") functions. +/// - `&C` for `&self` methods. +/// - `&mut C` for `&mut self` methods. +/// +/// See also [Signals](https://godot-rust.github.io/book/register/signals.html) in the book. +pub trait SignalReceiver: 'static { + /// Invoke the receiver on the given instance (possibly `()`) with `params`. + fn call(&mut self, maybe_instance: I, params: Ps); +} + +/// Represents a parameter list as Rust tuple. +/// +/// Each tuple element is one parameter. This trait provides conversions to and from `Variant` arrays. +// Re-exported under crate::meta. Might be worth splitting, but depends a bit on SignatureVarcall/Ptrcall refactoring. +pub trait ParamTuple: 'static { + fn to_variant_array(&self) -> Vec; + fn from_variant_array(array: &[&Variant]) -> Self; +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Generated impls + +macro_rules! impl_signal_recipient { + ($( $args:ident : $Ps:ident ),*) => { + // -------------------------------------------------------------------------------------------------------------------------------------- + // ParamTuple + + impl<$($Ps),*> ParamTuple for ($($Ps,)*) + where + $($Ps: meta::ToGodot + meta::FromGodot + 'static),* + { + fn to_variant_array(&self) -> Vec { + let ($($args,)*) = self; + + vec![ + $( $args.to_variant(), )* + ] + } + + #[allow(unused_variables, unused_mut, clippy::unused_unit)] + fn from_variant_array(array: &[&Variant]) -> Self { + let mut iter = array.iter(); + ( $( + <$Ps>::from_variant( + iter.next().unwrap_or_else(|| panic!("ParamTuple: {} access out-of-bounds (len {})", stringify!($args), array.len())) + ), + )* ) + } + } + + // -------------------------------------------------------------------------------------------------------------------------------------- + // SignalReceiver + + // Global and associated functions. + impl SignalReceiver<(), ( $($Ps,)* )> for F + where F: FnMut( $($Ps,)* ) -> R + 'static + { + fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { + self($($args,)*); + } + } + + // Methods with mutable receiver - &mut self. + impl SignalReceiver<&mut C, ( $($Ps,)* )> for F + where F: FnMut( &mut C, $($Ps,)* ) -> R + 'static + { + fn call(&mut self, instance: &mut C, ($($args,)*): ( $($Ps,)* )) { + self(instance, $($args,)*); + } + } + + // Methods with immutable receiver - &self. + impl SignalReceiver<&C, ( $($Ps,)* )> for F + where F: FnMut( &C, $($Ps,)* ) -> R + 'static + { + fn call(&mut self, instance: &C, ($($args,)*): ( $($Ps,)* )) { + self(instance, $($args,)*); + } + } + }; +} + +impl_signal_recipient!(); +impl_signal_recipient!(arg0: P0); +impl_signal_recipient!(arg0: P0, arg1: P1); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8); +impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2, arg3: P3, arg4: P4, arg5: P5, arg6: P6, arg7: P7, arg8: P8, arg9: P9); diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 1d1117c0d..70e971f7e 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -31,6 +31,20 @@ pub struct FuncDefinition { pub rpc_info: Option, } +impl FuncDefinition { + pub fn rust_ident(&self) -> &Ident { + &self.signature_info.method_name + } + + pub fn godot_name(&self) -> String { + if let Some(name_override) = self.registered_name.as_ref() { + name_override.clone() + } else { + self.rust_ident().to_string() + } + } +} + /// Returns a C function which acts as the callback when a virtual method of this instance is invoked. // // Virtual methods are non-static by their nature; so there's no support for static ones. @@ -96,13 +110,8 @@ pub fn make_method_registration( ); // String literals - let method_name = &signature_info.method_name; let class_name_str = class_name.to_string(); - let method_name_str = if let Some(updated_name) = func_definition.registered_name { - updated_name - } else { - method_name.to_string() - }; + let method_name_str = func_definition.godot_name(); let call_ctx = make_call_context(&class_name_str, &method_name_str); let varcall_fn_decl = make_varcall_fn(&call_ctx, &forwarding_closure); @@ -193,6 +202,9 @@ impl SignatureInfo { } } + // The below functions share quite a bit of tokenization. If ever we run into codegen slowness, we could cache/reuse identical + // sub-expressions. + pub fn tuple_type(&self) -> TokenStream { // Note: for GdSelf receivers, first parameter is not even part of SignatureInfo anymore. util::make_signature_tuple_type(&self.ret_type, &self.param_types) diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 89759089b..2316308a9 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -23,7 +23,7 @@ use quote::{format_ident, quote}; /// Attribute for user-declared function. enum ItemAttrType { Func(FuncAttr, Option), - Signal(venial::AttributeValue), + Signal(SignalAttr, venial::AttributeValue), Const(#[allow(dead_code)] venial::AttributeValue), } @@ -42,7 +42,7 @@ enum AttrParseResult { Func(FuncAttr), Rpc(RpcAttr), FuncRpc(FuncAttr, RpcAttr), - Signal(venial::AttributeValue), + Signal(SignalAttr, venial::AttributeValue), Const(#[allow(dead_code)] venial::AttributeValue), } @@ -53,7 +53,7 @@ impl AttrParseResult { // If only `#[rpc]` is present, we assume #[func] with default values. AttrParseResult::Rpc(rpc) => ItemAttrType::Func(FuncAttr::default(), Some(rpc)), AttrParseResult::FuncRpc(func, rpc) => ItemAttrType::Func(func, Some(rpc)), - AttrParseResult::Signal(signal) => ItemAttrType::Signal(signal), + AttrParseResult::Signal(signal, attr_val) => ItemAttrType::Signal(signal, attr_val), AttrParseResult::Const(constant) => ItemAttrType::Const(constant), } } @@ -66,6 +66,11 @@ struct FuncAttr { pub has_gd_self: bool, } +#[derive(Default)] +struct SignalAttr { + pub no_builder: bool, +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct InherentImplAttr { @@ -102,7 +107,8 @@ 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 = make_signal_registrations(signals, &class_name_obj); + let (signal_registrations, signals_collection_struct) = + make_signal_registrations(&signals, &class_name, &class_name_obj)?; #[cfg(feature = "codegen-full")] let rpc_registrations = crate::class::make_rpc_registrations_fn(&class_name, &funcs); @@ -180,6 +186,7 @@ pub fn transform_inherent_impl( impl #funcs_collection { #( #func_name_constants )* } + #signals_collection_struct }; Ok(result) @@ -293,17 +300,17 @@ fn process_godot_fns( }); } - ItemAttrType::Signal(ref _attr_val) => { + ItemAttrType::Signal(ref signal, ref _attr_val) => { if is_secondary_impl { return attr.bail( - "#[signal] is not currently supported in secondary impl blocks", + "#[signal] is currently not supported in secondary impl blocks", function, ); } if function.return_ty.is_some() { return bail!( &function.return_ty, - "return types in #[signal] are not supported" + "#[signal] does not support return types" ); } if function.body.is_some() { @@ -312,6 +319,12 @@ fn process_godot_fns( "#[signal] must not have a body; declare the function with a semicolon" ); } + if function.vis_marker.is_some() { + return bail!( + &function.vis_marker, + "#[signal] must not have a visibility specifier; signals are always public" + ); + } let external_attributes = function.attributes.clone(); let sig = util::reduce_to_signature(function); @@ -319,6 +332,7 @@ fn process_godot_fns( signal_definitions.push(SignalDefinition { signature: sig, external_attributes, + has_builder: !signal.no_builder, }); removed_indexes.push(index); @@ -361,7 +375,7 @@ fn process_godot_constants(decl: &mut venial::Impl) -> ParseResult { return bail!(constant, "#[func] and #[rpc] can only be used on functions") } - ItemAttrType::Signal(_) => { + ItemAttrType::Signal(_, _) => { return bail!(constant, "#[signal] can only be used on functions") } ItemAttrType::Const(_) => { @@ -483,7 +497,7 @@ where let parsed_attr = match attr_name { // #[func] name if name == "func" => { - // Safe unwrap since #[func] must be present if we got to this point + // Safe unwrap, since #[func] must be present if we got to this point. let mut parser = KvParser::parse(attributes, "func")?.unwrap(); // #[func(rename = MyClass)] @@ -564,10 +578,28 @@ where } // #[signal] - name if name == "signal" => AttrParseResult::Signal(attr.value.clone()), + name if name == "signal" => { + // Safe unwrap, since #[signal] must be present if we got to this point. + let mut parser = KvParser::parse(attributes, "signal")?.unwrap(); + + // Private #[signal(__no_builder)] + let no_builder = parser.handle_alone("__no_builder")?; + + parser.finish()?; + + let signal_attr = SignalAttr { no_builder }; + + AttrParseResult::Signal(signal_attr, attr.value.clone()) + } // #[constant] - name if name == "constant" => AttrParseResult::Const(attr.value.clone()), + name if name == "constant" => { + // Ensure no keys are present. + let parser = KvParser::parse(attributes, "constant")?.unwrap(); + parser.finish()?; + + AttrParseResult::Const(attr.value.clone()) + } // Ignore unknown attributes. _ => continue, diff --git a/godot-macros/src/class/data_models/rpc.rs b/godot-macros/src/class/data_models/rpc.rs index e4cf585f9..fe319208e 100644 --- a/godot-macros/src/class/data_models/rpc.rs +++ b/godot-macros/src/class/data_models/rpc.rs @@ -149,11 +149,7 @@ fn make_rpc_registration(func_def: &FuncDefinition) -> Option { } }; - let method_name_str = if let Some(rename) = &func_def.registered_name { - rename.to_string() - } else { - func_def.signature_info.method_name.to_string() - }; + let method_name_str = func_def.godot_name(); let registration = quote! { { diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index 302c27afd..b2b20e629 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -5,9 +5,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util; -use proc_macro2::TokenStream; -use quote::quote; +use crate::util::bail; +use crate::{util, ParseResult}; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; /// Holds information known from a signal's definition pub struct SignalDefinition { @@ -16,76 +17,309 @@ pub struct SignalDefinition { /// The signal's non-gdext attributes (all except #[signal]). pub external_attributes: Vec, + + /// Whether there is going to be a type-safe builder for this signal (true by default). + pub has_builder: bool, +} + +/// Extracted syntax info for a declared signal. +struct SignalDetails<'a> { + /// `fn my_signal(i: i32, s: GString)` + original_decl: &'a venial::Function, + /// `MyClass` + class_name: &'a Ident, + /// `i32`, `GString` + param_types: Vec, + /// `i`, `s` + param_names: Vec, + /// `"i"`, `"s"` + param_names_str: Vec, + /// `(i32, GString)` + param_tuple: TokenStream, + /// `MySignal` + signal_name: &'a Ident, + /// `"MySignal"` + signal_name_str: String, + /// `#[cfg(..)] #[cfg(..)]` + signal_cfg_attrs: Vec<&'a venial::Attribute>, + /// `MyClass_MySignal` + individual_struct_name: Ident, +} + +impl<'a> SignalDetails<'a> { + pub fn extract( + original_decl: &'a venial::Function, + class_name: &'a Ident, + external_attributes: &'a [venial::Attribute], + ) -> ParseResult> { + let mut param_types = vec![]; + let mut param_names = vec![]; + let mut param_names_str = vec![]; + + for (param, _punct) in original_decl.params.inner.iter() { + match param { + venial::FnParam::Typed(param) => { + param_types.push(param.ty.clone()); + param_names.push(param.name.clone()); + param_names_str.push(param.name.to_string()); + } + venial::FnParam::Receiver(receiver) => { + return bail!(receiver, "#[signal] cannot have receiver (self) parameter"); + } + }; + } + + // Transport #[cfg] attributes to the FFI glue, to ensure signals which were conditionally + // removed from compilation don't cause errors. + let signal_cfg_attrs = util::extract_cfg_attrs(external_attributes) + .into_iter() + .collect(); + + let param_tuple = quote! { ( #( #param_types, )* ) }; + let signal_name = &original_decl.name; + let individual_struct_name = format_ident!("__godot_Signal_{}_{}", class_name, signal_name); + + Ok(Self { + original_decl, + class_name, + param_types, + param_names, + param_names_str, + param_tuple, + signal_name, + signal_name_str: original_decl.name.to_string(), + signal_cfg_attrs, + individual_struct_name, + }) + } } pub fn make_signal_registrations( - signals: Vec, + signals: &[SignalDefinition], + class_name: &Ident, class_name_obj: &TokenStream, -) -> Vec { +) -> ParseResult<(Vec, Option)> { let mut signal_registrations = Vec::new(); + let mut collection_api = SignalCollection::default(); - for signal in signals.iter() { + for signal in signals { let SignalDefinition { signature, external_attributes, + has_builder, } = signal; - let mut param_types: Vec = Vec::new(); - let mut param_names: Vec = Vec::new(); - for param in signature.params.inner.iter() { - match ¶m.0 { - venial::FnParam::Typed(param) => { - param_types.push(param.ty.clone()); - param_names.push(param.name.to_string()); - } - venial::FnParam::Receiver(_) => {} - }; + let details = SignalDetails::extract(signature, class_name, external_attributes)?; + + // Callable custom functions are only supported in 4.2+, upon which custom signals rely. + #[cfg(since_api = "4.2")] + if *has_builder { + collection_api.extend_with(&details); } - let signature_tuple = util::make_signature_tuple_type("e! { () }, ¶m_types); - let indexes = 0..param_types.len(); - let param_array_decl = quote! { - [ - // Don't use raw sys pointers directly; it's very easy to have objects going out of scope. - #( - <#signature_tuple as godot::meta::VarcallSignatureTuple> - ::param_property_info(#indexes, #param_names), - )* - ] - }; + let registration = make_signal_registration(&details, class_name_obj); + signal_registrations.push(registration); + } - // Transport #[cfg] attributes to the FFI glue, to ensure signals which were conditionally - // removed from compilation don't cause errors. - let signal_cfg_attrs: Vec<&venial::Attribute> = - util::extract_cfg_attrs(external_attributes) - .into_iter() - .collect(); - let signal_name_str = signature.name.to_string(); - let signal_parameters_count = param_names.len(); - let signal_parameters = param_array_decl; - - let signal_registration = quote! { + let struct_code = make_signal_collection(class_name, collection_api); + + Ok((signal_registrations, struct_code)) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +fn make_signal_registration(details: &SignalDetails, class_name_obj: &TokenStream) -> TokenStream { + let SignalDetails { + param_types, + param_names, + param_names_str, + signal_name_str, + signal_cfg_attrs, + .. + } = details; + + let signature_tuple = util::make_signature_tuple_type("e! { () }, param_types); + + let indexes = 0..param_types.len(); + let param_property_infos = quote! { + [ + // Don't use raw sys pointers directly; it's very easy to have objects going out of scope. + #( + <#signature_tuple as godot::meta::VarcallSignatureTuple> + ::param_property_info(#indexes, #param_names_str), + )* + ] + }; + + let signal_parameters_count = param_names.len(); + + quote! { + #(#signal_cfg_attrs)* + unsafe { + use ::godot::sys; + let parameters_info: [::godot::meta::PropertyInfo; #signal_parameters_count] = #param_property_infos; + + let mut parameters_info_sys: [sys::GDExtensionPropertyInfo; #signal_parameters_count] = + std::array::from_fn(|i| parameters_info[i].property_sys()); + + let signal_name = ::godot::builtin::StringName::from(#signal_name_str); + + sys::interface_fn!(classdb_register_extension_class_signal)( + sys::get_library(), + #class_name_obj.string_sys(), + signal_name.string_sys(), + parameters_info_sys.as_ptr(), + sys::GDExtensionInt::from(#signal_parameters_count as i64), + ); + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// A collection struct accessible via `.signals()` in the generated impl. +/// +/// Also defines individual signal types. +#[derive(Default)] +struct SignalCollection { + /// The individual `my_signal()` accessors, returning concrete signal types. + collection_methods: Vec, + + /// The actual signal definitions, including both `struct` and `impl` blocks. + individual_structs: Vec, +} + +impl SignalCollection { + fn extend_with(&mut self, details: &SignalDetails) { + let SignalDetails { + signal_name, + signal_name_str, + signal_cfg_attrs, + individual_struct_name, + .. + } = details; + + self.collection_methods.push(quote! { + // Deliberately not #[doc(hidden)] for IDE completion. #(#signal_cfg_attrs)* - unsafe { - use ::godot::sys; - let parameters_info: [::godot::meta::PropertyInfo; #signal_parameters_count] = #signal_parameters; - - let mut parameters_info_sys: [sys::GDExtensionPropertyInfo; #signal_parameters_count] = - std::array::from_fn(|i| parameters_info[i].property_sys()); - - let signal_name = ::godot::builtin::StringName::from(#signal_name_str); - - sys::interface_fn!(classdb_register_extension_class_signal)( - sys::get_library(), - #class_name_obj.string_sys(), - signal_name.string_sys(), - parameters_info_sys.as_ptr(), - sys::GDExtensionInt::from(#signal_parameters_count as i64), - ); + fn #signal_name(self) -> #individual_struct_name<'a> { + #individual_struct_name { + typed: ::godot::register::TypedSignal::new(self.__internal_obj, #signal_name_str) + } } - }; + }); - signal_registrations.push(signal_registration); + self.individual_structs + .push(make_signal_individual_struct(details)) } - signal_registrations + + pub fn is_empty(&self) -> bool { + self.individual_structs.is_empty() + } +} + +fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { + let emit_params = &details.original_decl.params; + + let SignalDetails { + class_name, + param_names, + param_tuple, + signal_cfg_attrs, + individual_struct_name, + .. + } = details; + + // Define the individual types + trait impls. The idea was originally to use a module to reduce namespace pollution: + // let module_name = format_ident!("__godot_signal_{class_name}_{signal_name}"); + // #(#signal_cfg_attrs)* pub mod #module_name { use super::*; ... } + // #(#signal_cfg_attrs)* pub(crate) use #module_name::#individual_struct_name; + // However, there are some challenges: + // - Visibility becomes a pain to handle (rustc doesn't like re-exporting private symbols as pub, and we can't know the visibility of the + // surrounding class struct). Having signals always-public is much less of a headache, requires less choice on the user side + // (pub/pub(crate)/nothing on #[signal]), and likely good enough for the moment. + // - Not yet clear if we should have each signal + related types in separate module. If #[signal] is supported in #[godot_api(secondary)] + // impl blocks, then we would have to group them by the impl block. Rust doesn't allow partial modules, so they'd need to have individual + // names as well, possibly explicitly chosen by the user. + // + // For now, #[doc(hidden)] is used in some places to limit namespace pollution at least in IDEs + docs. This also means that the generated + // code is less observable by the user. If someone comes up with a good idea to handle all this, let us know :) + quote! { + #(#signal_cfg_attrs)* + #[allow(non_camel_case_types)] + #[doc(hidden)] // Signal struct is hidden, but the method returning it is not (IDE completion). + struct #individual_struct_name<'a> { + #[doc(hidden)] + typed: ::godot::register::TypedSignal<'a, #class_name, #param_tuple>, + } + + // Concrete convenience API is macro-based; many parts are delegated to TypedSignal via Deref/DerefMut. + #(#signal_cfg_attrs)* + impl #individual_struct_name<'_> { + pub fn emit(&mut self, #emit_params) { + self.typed.emit_tuple((#( #param_names, )*)); + } + } + + #(#signal_cfg_attrs)* + impl<'a> std::ops::Deref for #individual_struct_name<'a> { + type Target = ::godot::register::TypedSignal<'a, #class_name, #param_tuple>; + + fn deref(&self) -> &Self::Target { + &self.typed + } + } + + #(#signal_cfg_attrs)* + impl std::ops::DerefMut for #individual_struct_name<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &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() { + return None; + } + + let collection_struct_name = format_ident!("__godot_Signals_{}", class_name); + let collection_struct_methods = &collection.collection_methods; + let individual_structs = collection.individual_structs; + + 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<'a> { + // 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::ObjectRef<'a, #class_name> + } + + impl<'a> #collection_struct_name<'a> { + #( #collection_struct_methods )* + } + + impl ::godot::obj::WithSignals for #class_name { + type SignalCollection<'a> = #collection_struct_name<'a>; + + fn signals(&mut self) -> Self::SignalCollection<'_> { + Self::SignalCollection { + __internal_obj: ::godot::register::ObjectRef::Internal { obj_mut: self } + } + } + + #[doc(hidden)] + fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_> { + Self::SignalCollection { + __internal_obj: ::godot::register::ObjectRef::External { gd: external.clone() } + } + } + } + + #( #individual_structs )* + }; + Some(code) } diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index b5d8de845..17775d6f0 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -329,12 +329,14 @@ fn make_user_class_impl( let user_class_impl = quote! { impl ::godot::obj::UserClass for #class_name { + #[doc(hidden)] fn __config() -> ::godot::private::ClassConfig { ::godot::private::ClassConfig { is_tool: #is_tool, } } + #[doc(hidden)] fn __before_ready(&mut self) { #rpc_registrations #onready_inits diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs.rs index bb0064c77..895d58f07 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs.rs @@ -250,7 +250,7 @@ pub fn make_method_docs(method: &FuncDefinition) -> Option { let name = method .registered_name .clone() - .unwrap_or_else(|| method.signature_info.method_name.to_string()); + .unwrap_or_else(|| method.rust_ident().to_string()); let ret = method.signature_info.ret_type.to_token_stream().to_string(); let params = params( method diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 6c5c4b733..c16074225 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -40,7 +40,6 @@ use crate::util::{bail, ident, KvParser}; /// - [Properties and exports](#properties-and-exports) /// - [Property registration](#property-registration) /// - [Property exports](#property-exports) -/// - [Signals](#signals) /// - [Further class customization](#further-class-customization) /// - [Running code in the editor](#running-code-in-the-editor) /// - [Editor plugins](#editor-plugins) @@ -327,27 +326,6 @@ use crate::util::{bail, ident, KvParser}; /// } /// ``` /// -/// # Signals -/// -/// The `#[signal]` attribute is quite limited at the moment. The functions it decorates (the signals) can accept parameters. -/// It will be fundamentally reworked. -/// -/// ```no_run -/// # use godot::prelude::*; -/// #[derive(GodotClass)] -/// # #[class(init)] -/// struct MyClass {} -/// -/// #[godot_api] -/// impl MyClass { -/// #[signal] -/// fn some_signal(); -/// -/// #[signal] -/// fn some_signal_with_parameters(my_parameter: Gd); -/// } -/// ``` -/// /// # Further class customization /// /// ## Running code in the editor @@ -529,7 +507,8 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// - [Associated functions and methods](#associated-functions-and-methods) /// - [Virtual methods](#virtual-methods) /// - [RPC attributes](#rpc-attributes) -/// - [Constants and signals](#signals) +/// - [Signals](#signals) +/// - [Constants](#constants) /// - [Multiple inherent `impl` blocks](#multiple-inherent-impl-blocks) /// /// # Constructors @@ -770,7 +749,38 @@ pub fn derive_godot_class(input: TokenStream) -> TokenStream { /// [`TransferMode`]: ../classes/multiplayer_peer/struct.TransferMode.html /// [`RpcConfig`]: ../register/struct.RpcConfig.html /// -/// # Constants and signals +/// +/// # Signals +/// +/// The `#[signal]` attribute declares a Godot signal, which can accept parameters, but not return any value. +/// The procedural macro generates a type-safe API that allows you to connect and emit the signal from Rust. +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init)] +/// struct MyClass { +/// base: Base, // necessary for #[signal]. +/// } +/// +/// #[godot_api] +/// impl MyClass { +/// #[signal] +/// fn some_signal(my_parameter: Gd); +/// } +/// ``` +/// +/// The above implements the [`WithSignals`] trait for `MyClass`, which provides the `signals()` method. Through that +/// method, you can access all declared signals in `self.signals().some_signal()` or `gd.signals().some_signal()`. The returned object is +/// of type [`TypedSignal`], which provides further APIs for emitting and connecting, among others. +/// +/// A detailed explanation with examples is available in the [book chapter _Registering signals_](https://godot-rust.github.io/book/register/signals.html). +/// +/// [`WithSignals`]: ../obj/trait.WithSignals.html +/// [`TypedSignal`]: ../register/struct.TypedSignal.html +/// +/// +/// # Constants /// /// Please refer to [the book](https://godot-rust.github.io/book/register/constants.html). /// diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 46fbcaba0..a14b1d283 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -360,12 +360,8 @@ pub fn make_funcs_collection_constant( None => func_name.to_string(), }; - let doc_comment = - format!("The Rust function `{func_name}` is registered with Godot as `{const_value}`."); - quote! { #(#attributes)* - #[doc = #doc_comment] #[doc(hidden)] #[allow(non_upper_case_globals)] pub const #const_name: &str = #const_value; @@ -409,5 +405,5 @@ pub fn format_funcs_collection_constant(_class_name: &Ident, func_name: &Ident) /// Returns the name of the struct used as collection for all function name constants. pub fn format_funcs_collection_struct(class_name: &Ident) -> Ident { - format_ident!("__gdext_{class_name}_Funcs") + format_ident!("__godot_{class_name}_Funcs") } diff --git a/godot/src/lib.rs b/godot/src/lib.rs index bbe33ef6e..c8409382d 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -179,6 +179,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_macros::{godot_api, godot_dyn, Export, GodotClass, GodotConvert, Var}; #[cfg(feature = "__codegen-full")] diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index ddf7f6f62..0f3bb81e1 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -37,3 +37,4 @@ pub use super::obj::EngineEnum as _; pub use super::obj::NewAlloc as _; pub use super::obj::NewGd as _; pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd() +pub use super::obj::WithSignals as _; // signals() diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index bf13b277e..6aa404948 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -5,41 +5,220 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use crate::framework::itest; use godot::builtin::{GString, Signal, StringName}; use godot::classes::{Object, RefCounted}; use godot::meta::ToGodot; -use godot::obj::{Base, Gd, NewAlloc, NewGd, WithBaseField}; +use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd, WithSignals}; use godot::register::{godot_api, GodotClass}; use godot::sys; +use godot::sys::Global; use std::cell::Cell; - -use crate::framework::itest; +use std::rc::Rc; #[itest] fn signal_basic_connect_emit() { let mut emitter = Emitter::new_alloc(); let receiver = Receiver::new_alloc(); - let args = [ - vec![], - vec![987.to_variant()], - vec![receiver.to_variant(), SIGNAL_ARG_STRING.to_variant()], - ]; + emitter.connect("signal_unit", &receiver.callable("receive_unit")); + emitter.emit_signal("signal_unit", &[]); + assert_eq!(receiver.bind().last_received(), LastReceived::Unit); + + emitter.connect("signal_int", &receiver.callable("receive_int")); + emitter.emit_signal("signal_int", &[1278.to_variant()]); + assert_eq!(receiver.bind().last_received(), LastReceived::Int(1278)); + + let emitter_variant = emitter.to_variant(); + emitter.connect("signal_obj", &receiver.callable("receive_obj")); + emitter.emit_signal("signal_obj", &[emitter_variant]); + assert_eq!( + receiver.bind().last_received(), + LastReceived::Object(emitter.instance_id()) + ); + + receiver.free(); + emitter.free(); +} + +// "Internal" means connect/emit happens from within the class, via self.signals(). +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_internal() { + let mut emitter = Emitter::new_alloc(); + + // Connect signals from inside. + let tracker = Rc::new(Cell::new(0)); + let mut internal = emitter.bind_mut(); + internal.connect_signals_internal(tracker.clone()); + drop(internal); + + emitter.bind_mut().emit_signals_internal(); + + // Check that closure is invoked. + assert_eq!(tracker.get(), 1234, "Emit failed (closure)"); + + // Check that instance method is invoked. + assert_eq!( + emitter.bind().last_received_int, + 1234, + "Emit failed (method)" + ); - for (i, arg) in args.iter().enumerate() { - let signal_name = format!("emitter_{i}"); - let receiver_name = format!("receiver_{i}"); + // Check that static function is invoked. + assert_eq!( + *LAST_STATIC_FUNCTION_ARG.lock(), + 1234, + "Emit failed (static function)" + ); - emitter.connect(&signal_name, &receiver.callable(&receiver_name)); - emitter.emit_signal(&signal_name, arg); + emitter.free(); +} - assert!(receiver.bind().used[i].get()); +// "External" means connect/emit happens from outside the class, via Gd::signals(). +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_external() { + let emitter = Emitter::new_alloc(); + let mut sig = emitter.signals().signal_int(); + + // Local function; deliberately use a !Send type. + let tracker = Rc::new(Cell::new(0)); + { + let tracker = tracker.clone(); + sig.connect(move |i| { + tracker.set(i); + }); } + // Self-modifying method. + sig.connect_self(Emitter::self_receive); + + // Connect to other object. + let receiver = Receiver::new_alloc(); + sig.connect_obj(&receiver, Receiver::receive_int_mut); + + // Emit signal (now via tuple). + sig.emit_tuple((987,)); + + // Check that closure is invoked. + assert_eq!(tracker.get(), 987, "Emit failed (closure)"); + + // Check that instance method is invoked. + assert_eq!( + emitter.bind().last_received_int, + 987, + "Emit failed (method)" + ); + + // Check that *other* instance method is invoked. + assert_eq!( + receiver.bind().last_received(), + LastReceived::IntMut(987), + "Emit failed (other object method)" + ); + receiver.free(); emitter.free(); } +// "External" means connect/emit happens from outside the class, via Gd::signals(). +#[cfg(since_api = "4.2")] +#[itest] +fn signal_symbols_external_builder() { + let emitter = Emitter::new_alloc(); + let mut sig = emitter.signals().signal_int(); + + // Self-modifying method. + sig.connect_builder() + .object_self() + .method_mut(Emitter::self_receive) + .done(); + + // Connect to other object. + let receiver_mut = Receiver::new_alloc(); + sig.connect_builder() + .object(&receiver_mut) + .method_mut(Receiver::receive_int_mut) + .done(); + + // Connect to yet another object, immutable receiver. + let receiver_immut = Receiver::new_alloc(); + sig.connect_builder() + .object(&receiver_immut) + .method_immut(Receiver::receive_int) + .done(); + + let tracker = Rc::new(Cell::new(0)); + { + let tracker = tracker.clone(); + sig.connect_builder() + .function(move |i| tracker.set(i)) + .done(); + } + + // Emit signal. + sig.emit(552); + + // Check that closure is invoked. + assert_eq!(tracker.get(), 552, "Emit failed (closure)"); + + // Check that self instance method (mut) is invoked. + assert_eq!( + emitter.bind().last_received_int, + 552, + "Emit failed (mut method)" + ); + + // Check that *other* instance method is invoked. + assert_eq!( + receiver_immut.bind().last_received(), + LastReceived::Int(552), + "Emit failed (other object, immut method)" + ); + + // Check that *other* instance method is invoked. + assert_eq!( + receiver_mut.bind().last_received(), + LastReceived::IntMut(552), + "Emit failed (other object, mut method)" + ); + + // Check that closures set up with builder are invoked. + assert_eq!(tracker.get(), 552, "Emit failed (builder local)"); + + receiver_immut.free(); + receiver_mut.free(); + emitter.free(); +} + +#[cfg(all(since_api = "4.2", feature = "experimental-threads"))] +#[itest] +fn signal_symbols_sync() { + use std::sync::{Arc, Mutex}; + + let emitter = Emitter::new_alloc(); + let mut sig = emitter.signals().signal_int(); + + let sync_tracker = Arc::new(Mutex::new(0)); + { + let sync_tracker = sync_tracker.clone(); + sig.connect_builder() + .function(move |i| *sync_tracker.lock().unwrap() = i) + .sync() + .done(); + } + + sig.emit(1143); + assert_eq!( + *sync_tracker.lock().unwrap(), + 1143, + "Emit failed (builder sync)" + ); + + emitter.free(); +} + #[itest] fn signal_construction_and_id() { let mut object = RefCounted::new_gd(); @@ -63,52 +242,104 @@ fn signal_construction_and_id() { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Helper types +/// Global sets the value of the received argument and whether it was a static function. +static LAST_STATIC_FUNCTION_ARG: Global = Global::default(); + #[derive(GodotClass)] #[class(init, base=Object)] -struct Emitter {} +struct Emitter { + _base: Base, + #[cfg(since_api = "4.2")] + last_received_int: i64, +} #[godot_api] impl Emitter { #[signal] - fn emitter_0(); + fn signal_unit(); #[signal] - fn emitter_1(arg1: i64); + fn signal_int(arg1: i64); #[signal] - fn emitter_2(arg1: Gd, arg2: GString); + fn signal_obj(arg1: Gd, arg2: GString); + + #[func] + fn self_receive(&mut self, arg1: i64) { + #[cfg(since_api = "4.2")] + { + self.last_received_int = arg1; + } + } + + #[func] + fn self_receive_static(arg1: i64) { + *LAST_STATIC_FUNCTION_ARG.lock() = arg1; + } + + // "Internal" means connect/emit happens from within the class (via &mut self). + + #[cfg(since_api = "4.2")] + fn connect_signals_internal(&mut self, tracker: Rc>) { + let mut sig = self.signals().signal_int(); + sig.connect_self(Self::self_receive); + sig.connect(Self::self_receive_static); + sig.connect(move |i| tracker.set(i)); + } + + #[cfg(since_api = "4.2")] + fn emit_signals_internal(&mut self) { + self.signals().signal_int().emit(1234); + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] +enum LastReceived { + #[default] + Nothing, + Unit, + Int(i64), + IntMut(i64), + Object(InstanceId), } #[derive(GodotClass)] #[class(init, base=Object)] struct Receiver { - used: [Cell; 3], + last_received: Cell, base: Base, } - #[godot_api] impl Receiver { - #[func] - fn receiver_0(&self) { - self.used[0].set(true); + fn last_received(&self) -> LastReceived { + self.last_received.get() } + // Note: asserting inside #[func] will be caught by FFI layer and not cause a call-site panic, thus not fail the test. + // Therefore, store received values and check them manually in the test. + #[func] - fn receiver_1(&self, arg1: i64) { - self.used[1].set(true); - assert_eq!(arg1, 987); + fn receive_unit(&self) { + self.last_received.set(LastReceived::Unit); } #[func] - fn receiver_2(&self, arg1: Gd, arg2: GString) { - assert_eq!(self.base().clone(), arg1); - assert_eq!(SIGNAL_ARG_STRING, arg2.to_string()); + fn receive_int(&self, arg1: i64) { + self.last_received.set(LastReceived::Int(arg1)); + } - self.used[2].set(true); + fn receive_int_mut(&mut self, arg1: i64) { + self.last_received.set(LastReceived::IntMut(arg1)); } -} -const SIGNAL_ARG_STRING: &str = "Signal string arg"; + #[func] + fn receive_obj(&self, obj: Gd) { + self.last_received + .set(LastReceived::Object(obj.instance_id())); + } +} // ---------------------------------------------------------------------------------------------------------------------------------------------- // 4.2+ custom callables diff --git a/itest/rust/src/engine_tests/native_audio_structures_test.rs b/itest/rust/src/engine_tests/native_audio_structures_test.rs index ed22f1982..71ad9034f 100644 --- a/itest/rust/src/engine_tests/native_audio_structures_test.rs +++ b/itest/rust/src/engine_tests/native_audio_structures_test.rs @@ -15,7 +15,6 @@ use godot::classes::{ AudioStreamGeneratorPlayback, AudioStreamPlayer, Engine, IAudioEffect, IAudioEffectInstance, SceneTree, }; -use godot::global::godot_print; use godot::obj::{Base, Gd, NewAlloc, NewGd}; use godot::register::{godot_api, GodotClass}; diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index 7c08e9cde..f7f519ae7 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -122,6 +122,9 @@ pub fn passes_filter(filters: &[String], test_name: &str) -> bool { filters.is_empty() || filters.iter().any(|x| test_name.contains(x)) } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Toolbox for tests + pub fn expect_panic(context: &str, code: impl FnOnce()) { use std::panic; diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index dc380d407..3d648801d 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -913,7 +913,7 @@ pub(super) struct ObjPayload {} #[godot_api] impl ObjPayload { - #[signal] + #[signal(__no_builder)] fn do_use(); #[func] diff --git a/itest/rust/src/object_tests/reentrant_test.rs b/itest/rust/src/object_tests/reentrant_test.rs index 825e283fb..8f3b13a5c 100644 --- a/itest/rust/src/object_tests/reentrant_test.rs +++ b/itest/rust/src/object_tests/reentrant_test.rs @@ -20,7 +20,7 @@ pub struct ReentrantClass { #[godot_api] impl ReentrantClass { - #[signal] + #[signal(__no_builder)] fn some_signal(); #[func] diff --git a/itest/rust/src/register_tests/func_test.rs b/itest/rust/src/register_tests/func_test.rs index 43aab3efa..31458b6a7 100644 --- a/itest/rust/src/register_tests/func_test.rs +++ b/itest/rust/src/register_tests/func_test.rs @@ -79,7 +79,7 @@ struct GdSelfObj { #[godot_api] impl GdSelfObj { // A signal that will be looped back to update_internal through gdscript. - #[signal] + #[signal(__no_builder)] fn update_internal_signal(new_internal: i32); #[func]