From f62257fea4a3d11a91c0b1c062f1b4b07e05d8b9 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 8 Dec 2024 10:35:17 +0100 Subject: [PATCH 01/14] Symbolic accessors for #[func] and #[signal] Adds three APIs to classes that contain at least one #[signal] decl: - self.signals() - self.funcs() - Self::static_funcs() The idea is to use generated symbols for type-safe connecting (signal -> func) as well as emitting. This is still WIP. --- godot-core/src/builtin/mod.rs | 5 +- godot-core/src/builtin/typed_signal.rs | 204 +++++++++++ godot-core/src/obj/traits.rs | 19 +- godot-core/src/private.rs | 13 +- godot-macros/src/class/data_models/func.rs | 118 ++++++- .../src/class/data_models/inherent_impl.rs | 62 +++- godot-macros/src/class/data_models/rpc.rs | 6 +- godot-macros/src/class/data_models/signal.rs | 327 +++++++++++++++--- godot-macros/src/docs.rs | 2 +- godot-macros/src/util/mod.rs | 12 + godot/src/lib.rs | 3 + godot/src/prelude.rs | 5 +- .../builtin_tests/containers/signal_test.rs | 84 ++++- itest/rust/src/object_tests/object_test.rs | 2 +- itest/rust/src/object_tests/reentrant_test.rs | 2 +- itest/rust/src/register_tests/func_test.rs | 2 +- 16 files changed, 771 insertions(+), 95 deletions(-) create mode 100644 godot-core/src/builtin/typed_signal.rs diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 7abbfcdd4..a15f9d60a 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 { @@ -171,6 +171,8 @@ pub mod __prelude_reexport { pub use string::{GString, NodePath, StringName}; pub use transform2d::*; pub use transform3d::*; + // TODO move to register? + pub use typed_signal::{Func, TypedSignal}; pub use variant::*; pub use vectors::*; @@ -219,6 +221,7 @@ mod signal; mod string; mod transform2d; mod transform3d; +mod typed_signal; mod variant; mod vectors; diff --git a/godot-core/src/builtin/typed_signal.rs b/godot-core/src/builtin/typed_signal.rs new file mode 100644 index 000000000..11ee86fa8 --- /dev/null +++ b/godot-core/src/builtin/typed_signal.rs @@ -0,0 +1,204 @@ +/* + * 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/. + */ + +// Maybe move this to builtin::functional module? + +use crate::builtin::{Callable, Signal, Variant}; +use crate::obj::Gd; +use crate::{classes, meta, sys}; +use std::borrow::Cow; +use std::fmt; + +pub trait ParamTuple { + fn to_variant_array(&self) -> Vec; +} + +impl ParamTuple for () { + fn to_variant_array(&self) -> Vec { + Vec::new() + } +} + +impl ParamTuple for (T,) +where + T: meta::ToGodot, +{ + fn to_variant_array(&self) -> Vec { + vec![self.0.to_variant()] + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub struct TypedSignal { + signal: Signal, + _signature: std::marker::PhantomData, +} + +impl TypedSignal { + pub(crate) fn from_untyped(signal: Signal) -> Self { + Self { + signal, + _signature: std::marker::PhantomData, + } + } + + pub fn emit(&self, params: Ps) { + self.signal.emit(¶ms.to_variant_array()); + } + + pub fn connect_untyped(&mut self, callable: &Callable, flags: i64) { + self.signal.connect(callable, flags); + } + + pub fn to_untyped(&self) -> Signal { + self.signal.clone() + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +/* Previous impl based on assumption, Signal would be used. Could maybe be combined within an enum. + +pub struct TypedSignal { + signal: Signal, + _signature: std::marker::PhantomData, +} + +impl TypedSignal { + pub(crate) fn from_untyped(signal: Signal) -> Self { + Self { + signal, + _signature: std::marker::PhantomData, + } + } + + pub fn emit(&self, params: Ps) { + self.signal.emit(¶ms.to_variant_array()); + } + + pub fn connect_untyped(&mut self, callable: &Callable, flags: i64) { + self.signal.connect(callable, flags); + } + + pub fn to_untyped(&self) -> Signal { + self.signal.clone() + } +}*/ + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/* +pub struct TypedFunc { + godot_name: &'static str, + _return_type: std::marker::PhantomData, + _param_types: std::marker::PhantomData<(C, Ps)>, +} + +impl TypedFunc { + #[doc(hidden)] + pub fn from_godot_name(godot_name: &'static str) -> Self { + Self { + godot_name, + _return_type: std::marker::PhantomData, + _param_types: std::marker::PhantomData, + } + } + + pub fn with_object(obj: &Gd) {} + + pub fn godot_name(&self) -> &'static str { + self.godot_name + } +} +*/ + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Type-safe `#[func]` reference that is readily callable. +/// +/// Can be either a static function of a class, or a method which is bound to a concrete object. +/// +/// This can be seen as a more type-safe variant of Godot's `Callable`, which can carry intermediate information about function signatures (e.g. +/// when connecting signals). +pub struct Func { + godot_function_name: &'static str, + callable_kind: CallableKind, + _return_type: std::marker::PhantomData, + _param_types: std::marker::PhantomData, +} + +enum CallableKind { + StaticFunction { + // Maybe class name can be moved out (and also be useful for methods), e.g. Debug impl or so. + class_godot_name: Cow<'static, str>, + }, + Method { + bound_object: Gd, + }, +} + +impl Func { + #[doc(hidden)] + pub fn from_instance_method( + bound_object: Gd, + method_godot_name: &'static str, + ) -> Self { + Self { + godot_function_name: method_godot_name, + callable_kind: CallableKind::Method { bound_object }, + _return_type: std::marker::PhantomData, + _param_types: std::marker::PhantomData, + } + } + + #[doc(hidden)] + pub fn from_static_function( + class_godot_name: Cow<'static, str>, + method_godot_name: &'static str, + ) -> Self { + Self { + godot_function_name: method_godot_name, + callable_kind: CallableKind::StaticFunction { class_godot_name }, + _return_type: std::marker::PhantomData, + _param_types: std::marker::PhantomData, + } + } + + pub fn to_callable(&self) -> Callable { + match &self.callable_kind { + CallableKind::StaticFunction { class_godot_name } => { + let class_name = class_godot_name.as_ref(); + Callable::from_local_static(class_name, self.godot_function_name) + } + CallableKind::Method { bound_object } => { + Callable::from_object_method(bound_object, self.godot_function_name) + } + } + } +} + +impl fmt::Debug for Func { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let r = sys::short_type_name::(); + let ps = sys::short_type_name::(); + + let (obj_or_class, is_static); + match &self.callable_kind { + CallableKind::StaticFunction { class_godot_name } => { + obj_or_class = class_godot_name.to_string(); + is_static = "; static"; + } + CallableKind::Method { bound_object } => { + obj_or_class = format!("{bound_object:?}"); + is_static = ""; + } + }; + + let function = self.godot_function_name; + write!(f, "Func({obj_or_class}.{function}{is_static}; {ps} -> {r})") + } +} diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index d9be74d5a..059962c14 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 { @@ -571,6 +571,21 @@ pub mod cap { fn __godot_property_get_revert(&self, property: StringName) -> Option; } + // Move one level up, like WithBaseField? + pub trait WithFuncs { + type FuncCollection; + type StaticFuncCollection; + + fn static_funcs() -> Self::StaticFuncCollection; + fn funcs(&self) -> Self::FuncCollection; + } + + pub trait WithSignals: WithBaseField { + type SignalCollection<'a>; + + fn signals(&mut self) -> Self::SignalCollection<'_>; + } + /// Auto-implemented for `#[godot_api] impl MyClass` blocks pub trait ImplementsGodotApi: GodotClass { #[doc(hidden)] diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 653e89d64..e87eda72a 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -17,15 +17,16 @@ pub use sys::out; #[cfg(feature = "trace")] pub use crate::meta::trace; +use crate::builtin::Variant; use crate::global::godot_error; use crate::meta::error::CallError; use crate::meta::CallContext; +use crate::obj::{bounds, BaseMut, GodotClass, Inherits}; use crate::sys; use std::sync::atomic; #[cfg(debug_assertions)] use std::sync::{Arc, Mutex}; use sys::Global; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables @@ -120,6 +121,16 @@ pub(crate) fn call_error_remove(in_error: &sys::GDExtensionCallError) -> Option< call_error } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Functional and signal APIs + +// pub fn emit_signal(obj: &mut BaseMut, varargs: &[Variant]) +// where +// T: GodotClass + Inherits, +// { +// obj.upcast_mut().emit_signal(varargs); +// } + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Plugin and global state handling diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 4c1beb7ac..3d16542a4 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. @@ -89,13 +103,8 @@ pub fn make_method_registration( make_forwarding_closure(class_name, signature_info, BeforeKind::Without); // 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); @@ -155,6 +164,96 @@ pub fn make_method_registration( Ok(registration) } +// See also make_signal_collection(). +pub fn make_func_collection( + class_name: &Ident, + func_definitions: &[FuncDefinition], +) -> TokenStream { + let instance_collection = format_ident!("{}Funcs", class_name); + let static_collection = format_ident!("{}StaticFuncs", class_name); + + let mut instance_collection_methods = vec![]; + let mut static_collection_methods = vec![]; + + for func in func_definitions { + let rust_func_name = func.rust_ident(); + let godot_func_name = func.godot_name(); + + let signature_info = &func.signature_info; + let generic_args = signature_info.separate_return_params_args(); + + // Transport #[cfg] attrs to the FFI glue to ensure functions which were conditionally + // removed from compilation don't cause errors. + // TODO remove code duplication + double computation, see above. + let cfg_attrs = util::extract_cfg_attrs(&func.external_attributes) + .into_iter() + .collect::>(); + + if func.signature_info.receiver_type == ReceiverType::Static { + static_collection_methods.push(quote! { + #(#cfg_attrs)* + // Use `&self` here to enable `.` chaining, such as in MyClass::static_funcs().my_func(). + fn #rust_func_name(self) -> ::godot::builtin::Func<#generic_args> { + let class_name = <#class_name as ::godot::obj::GodotClass>::class_name(); + ::godot::builtin::Func::from_static_function(class_name.to_cow_str(), #godot_func_name) + } + }); + } else { + instance_collection_methods.push(quote! { + #(#cfg_attrs)* + fn #rust_func_name(self) -> ::godot::builtin::Func<#generic_args> { + ::godot::builtin::Func::from_instance_method(self.obj, #godot_func_name) + } + }); + } + } + + quote! { + #[non_exhaustive] // Prevent direct instantiation. + #[allow(non_camel_case_types)] + pub struct #instance_collection { + // Could use #class_name instead of Object, but right now the inner Func<..> type anyway uses Object. + obj: ::godot::obj::Gd<::godot::classes::Object>, + } + + impl #instance_collection { + #[doc(hidden)] + pub fn __internal(obj: ::godot::obj::Gd<::godot::classes::Object>) -> Self { + Self { obj } + } + + #( #instance_collection_methods )* + } + + #[non_exhaustive] // Prevent direct instantiation. + #[allow(non_camel_case_types)] + pub struct #static_collection {} + + impl #static_collection { + #[doc(hidden)] + pub fn __internal() -> Self { + Self {} + } + + #( #static_collection_methods )* + } + + impl ::godot::obj::cap::WithFuncs for #class_name { + type FuncCollection = #instance_collection; + type StaticFuncCollection = #static_collection; + + fn funcs(&self) -> Self::FuncCollection { + let obj = ::to_gd(self); + Self::FuncCollection::__internal(obj.upcast()) + } + + fn static_funcs() -> Self::StaticFuncCollection { + Self::StaticFuncCollection::__internal() + } + } + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation @@ -186,10 +285,17 @@ 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) } + + pub fn separate_return_params_args(&self) -> TokenStream { + util::make_signature_generic_args(&self.ret_type, &self.param_types) + } } #[derive(Copy, Clone)] diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 4eeff41f2..32714701a 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -6,9 +6,9 @@ */ use crate::class::{ - into_signature_info, make_constant_registration, make_method_registration, - make_signal_registrations, ConstDefinition, FuncDefinition, RpcAttr, RpcMode, SignalDefinition, - SignatureInfo, TransferMode, + into_signature_info, make_constant_registration, make_func_collection, + make_method_registration, make_signal_registrations, ConstDefinition, FuncDefinition, RpcAttr, + RpcMode, SignalDefinition, SignatureInfo, TransferMode, }; use crate::util::{bail, c_str, ident, require_api_version, KvParser}; use crate::{handle_mutually_exclusive_keys, util, ParseResult}; @@ -20,7 +20,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), } @@ -39,7 +39,7 @@ enum AttrParseResult { Func(FuncAttr), Rpc(RpcAttr), FuncRpc(FuncAttr, RpcAttr), - Signal(venial::AttributeValue), + Signal(SignalAttr, venial::AttributeValue), Const(#[allow(dead_code)] venial::AttributeValue), } @@ -50,7 +50,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), } } @@ -63,6 +63,11 @@ struct FuncAttr { pub has_gd_self: bool, } +#[derive(Default)] +struct SignalAttr { + pub no_builder: bool, +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct InherentImplAttr { @@ -89,13 +94,20 @@ pub fn transform_inherent_impl( #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] let docs = quote! {}; - 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); #[cfg(not(feature = "codegen-full"))] let rpc_registrations = TokenStream::new(); + // If at least one #[signal] is present, generate both signals() + funcs() and their types. + // Do not generate otherwise, to save on compile time + scope pollution. + let func_collection_struct = signals_collection_struct + .as_ref() + .map(|_| make_func_collection(&class_name, &funcs)); + let method_registrations: Vec = funcs .into_iter() .map(|func_def| make_method_registration(&class_name, func_def)) @@ -174,6 +186,8 @@ pub fn transform_inherent_impl( #trait_impl #fill_storage #class_registration + #func_collection_struct + #signals_collection_struct }; Ok(result) @@ -284,14 +298,13 @@ 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() { + } else if function.return_ty.is_some() { return attr.bail("return types in #[signal] are not supported", function); } @@ -301,6 +314,7 @@ fn process_godot_fns( signal_definitions.push(SignalDefinition { signature: sig, external_attributes, + has_builder: !signal.no_builder, }); removed_indexes.push(index); @@ -343,7 +357,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(_) => { @@ -465,7 +479,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)] @@ -546,10 +560,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..f7564b24c 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,288 @@ 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 Vec, + ) -> 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!("{}_{}", 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)?; + + 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! { () }, ¶m_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, + // signals_fields: Vec, +} + +impl SignalCollection { + fn extend_with(&mut self, details: &SignalDetails) { + let SignalDetails { + signal_name, + signal_cfg_attrs, + individual_struct_name, + .. + } = details; + + // self.signals_fields.push(quote! { + // #(#signal_cfg_attrs)* + // #signal_name: ::godot::builtin::TypedSignal<#param_tuple> + // }); + + self.collection_methods.push(quote! { #(#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(&mut self) -> #individual_struct_name<'_> { + let object_mut = &mut *self.object_base; + #individual_struct_name { object_mut } } - }; + }); + + self.individual_structs + .push(make_signal_individual_struct(details)) + } - signal_registrations.push(signal_registration); + pub fn is_empty(&self) -> bool { + self.individual_structs.is_empty() } - signal_registrations +} + +fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { + let emit_params = &details.original_decl.params; + + let SignalDetails { + class_name, + param_names, + param_tuple, + signal_name_str, + signal_cfg_attrs, + individual_struct_name, + .. + } = details; + + quote! { + #(#signal_cfg_attrs)* + #[allow(non_camel_case_types)] + pub struct #individual_struct_name<'a> { + object_mut: &'a mut ::godot::classes::Object + //object_base: ::godot::obj::BaseMut<'a, #class_name>, + //signal: ::godot::builtin::TypedSignal<#param_tuple> + } + + #(#signal_cfg_attrs)* + impl #individual_struct_name<'_> { + pub fn emit(&mut self, #emit_params) { + use ::godot::meta::ToGodot; + // Potential optimization: encode args as signature-tuple and use direct ptrcall. + let varargs = [ + #( #param_names.to_variant(), )* + ]; + self.object_mut.emit_signal(#signal_name_str, &varargs); + } + + fn connect_fn(&mut self, f: impl FnMut #param_tuple) { + + } + + fn connect(mut self, registered_func: ::godot::register::Func) -> Self { + // connect() return value is ignored -- do not write `let _ = ...`, so we can revisit this when adding #[must_use] to Error. + + let callable = registered_func.to_callable(); + self.object_mut.connect(#signal_name_str, &callable); + self + } + } + } +} + +// See also make_func_collection(). +fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> Option { + if collection.is_empty() { + return None; + } + + let struct_name = format_ident!("{}Signals", class_name); + let signals_struct_methods = &collection.collection_methods; + let individual_structs = collection.individual_structs; + + let code = quote! { + #[allow(non_camel_case_types)] + pub struct #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. + object_base: ::godot::obj::BaseMut<'a, #class_name>, + } + + impl #struct_name<'_> { + #( #signals_struct_methods )* + } + + impl ::godot::obj::cap::WithSignals for #class_name { + type SignalCollection<'a> = #struct_name<'a>; + + fn signals(&mut self) -> Self::SignalCollection<'_> { + Self::SignalCollection { + object_base: self.base_mut(), + } + } + } + + #( #individual_structs )* + }; + Some(code) } diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs.rs index abe967862..0ac8647a0 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs.rs @@ -252,7 +252,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/util/mod.rs b/godot-macros/src/util/mod.rs index f88525733..63e780479 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -116,6 +116,18 @@ pub fn make_signature_tuple_type( } } +/// Returns a type expression `R, Params` (without parentheses) that can be used as two separate generic args. +/// +/// `Params` is a `(P1, P2, P3, ...)` tuple. +pub fn make_signature_generic_args( + ret_type: &TokenStream, + param_types: &[venial::TypeExpr], +) -> TokenStream { + quote::quote! { + #ret_type, (#(#param_types,)*) + } +} + fn is_punct(tt: &TokenTree, c: char) -> bool { match tt { TokenTree::Punct(punct) => punct.as_char() == c, diff --git a/godot/src/lib.rs b/godot/src/lib.rs index c5f687f6a..a2f0a8af6 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -186,6 +186,9 @@ pub mod register { pub use godot_core::registry::property; pub use godot_macros::{godot_api, godot_dyn, Export, GodotClass, GodotConvert, Var}; + // FIXME + pub use godot_core::builtin::Func; + #[cfg(feature = "__codegen-full")] pub use godot_core::registry::RpcConfig; diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index ddf7f6f62..8de770a25 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -32,8 +32,11 @@ pub use super::obj::{ }; // Make trait methods available. +pub use super::obj::cap::WithFuncs as _; // funcs() +pub use super::obj::cap::WithSignals as _; // signals() +pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd() + pub use super::obj::EngineBitfield as _; 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() diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index bf13b277e..25252d8d6 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -5,16 +5,17 @@ * 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::cap::{WithFuncs, WithSignals}; use godot::obj::{Base, Gd, NewAlloc, NewGd, WithBaseField}; use godot::register::{godot_api, GodotClass}; use godot::sys; +use godot::sys::Global; use std::cell::Cell; -use crate::framework::itest; - #[itest] fn signal_basic_connect_emit() { let mut emitter = Emitter::new_alloc(); @@ -40,6 +41,37 @@ fn signal_basic_connect_emit() { emitter.free(); } +#[itest] +fn signal_symbols_api() { + let mut emitter = Emitter::new_alloc(); + let receiver = Receiver::new_alloc(); + + let mut internal = emitter.bind_mut(); + + internal.connect_signals_internal(); + //internal.signals().emitter_1() + drop(internal); + + // let check = Signal::from_object_signal(&emitter, "emitter_1"); + // dbg!(check.connections()); + + println!("PRE_EMIT"); + emitter.emit_signal("emitter_1", &[1234.to_variant()]); + println!("POST_EMIT"); + + emitter.bind_mut().emit_signals_internal(); + + let received_arg = LAST_METHOD_ARG.lock(); + assert_eq!(*received_arg, Some(1234), "Emit failed (method)"); + + // This is currently broken: + // let received_arg = LAST_STATIC_FUNCTION_ARG.lock(); + // assert_eq!(*received_arg, Some(1234), "Emit failed (static function)"); + + receiver.free(); + emitter.free(); +} + #[itest] fn signal_construction_and_id() { let mut object = RefCounted::new_gd(); @@ -63,9 +95,15 @@ fn signal_construction_and_id() { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Helper types +/// Global sets the value of the received argument and whether it was a static function. +static LAST_METHOD_ARG: Global> = Global::default(); +static LAST_STATIC_FUNCTION_ARG: Global> = Global::default(); + #[derive(GodotClass)] #[class(init, base=Object)] -struct Emitter {} +struct Emitter { + _base: Base, +} #[godot_api] impl Emitter { @@ -77,6 +115,38 @@ impl Emitter { #[signal] fn emitter_2(arg1: Gd, arg2: GString); + + #[func] + fn self_receive(&mut self, arg1: i64) { + println!("Received instance: {}", arg1); + *LAST_METHOD_ARG.lock() = Some(arg1); + } + + #[func] + fn self_receive_static(arg1: i64) { + let x = Self::self_receive; + let y = Self::self_receive_static; + + println!("Received static: {}", arg1); + *LAST_STATIC_FUNCTION_ARG.lock() = Some(arg1); + } + + // "Internal" means connect/emit happens from within the class (via &mut self). + + fn connect_signals_internal(&mut self) { + self.signals().emitter_2().connect_fn(|obj, s| { + println!("emitter_2({obj}, {s})"); + }); + + let m = self.funcs().self_receive(); + let s = Self::static_funcs().self_receive_static(); + + self.signals().emitter_1().connect(m).connect(s); + } + + fn emit_signals_internal(&mut self) { + self.signals().emitter_1().emit(1234); + } } #[derive(GodotClass)] @@ -106,6 +176,14 @@ impl Receiver { self.used[2].set(true); } + + // This should probably have a dedicated key such as #[godot_api(func_refs)] or so... + #[signal] + fn _just_here_to_generate_funcs(); + + fn func(&self) { + // let f = self.signals().receiver_2(); + } } const SIGNAL_ARG_STRING: &str = "Signal string arg"; diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 63c21e326..7a4ab6875 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -889,7 +889,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 94fd08581..194f2960a 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] From 04e2a8d782202e6e56d8da93a9d7b49f77e5a00c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 5 Jan 2025 22:44:20 +0100 Subject: [PATCH 02/14] Integrate connect/emit with Rust named function pointers --- godot-core/src/builtin/mod.rs | 3 - godot-core/src/classes/class_runtime.rs | 2 +- godot-core/src/obj/gd.rs | 5 + godot-core/src/obj/raw_gd.rs | 7 +- godot-core/src/private.rs | 2 - godot-core/src/registry/as_func.rs | 46 ++++++ godot-core/src/registry/mod.rs | 5 + .../src/{builtin => registry}/typed_signal.rs | 149 +++++++++++++++--- godot-macros/src/class/data_models/func.rs | 8 +- godot-macros/src/class/data_models/signal.rs | 83 +++++----- godot/src/lib.rs | 8 +- .../builtin_tests/containers/signal_test.rs | 34 +--- 12 files changed, 253 insertions(+), 99 deletions(-) create mode 100644 godot-core/src/registry/as_func.rs rename godot-core/src/{builtin => registry}/typed_signal.rs (52%) diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index a15f9d60a..e9feb0783 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -171,8 +171,6 @@ pub mod __prelude_reexport { pub use string::{GString, NodePath, StringName}; pub use transform2d::*; pub use transform3d::*; - // TODO move to register? - pub use typed_signal::{Func, TypedSignal}; pub use variant::*; pub use vectors::*; @@ -221,7 +219,6 @@ mod signal; mod string; mod transform2d; mod transform3d; -mod typed_signal; mod variant; mod vectors; 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/obj/gd.rs b/godot-core/src/obj/gd.rs index 436de6520..7a93898b4 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -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 diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index e03b817ee..ea019a538 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -206,11 +206,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/private.rs b/godot-core/src/private.rs index e87eda72a..157d274db 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -17,11 +17,9 @@ pub use sys::out; #[cfg(feature = "trace")] pub use crate::meta::trace; -use crate::builtin::Variant; use crate::global::godot_error; use crate::meta::error::CallError; use crate::meta::CallContext; -use crate::obj::{bounds, BaseMut, GodotClass, Inherits}; use crate::sys; use std::sync::atomic; #[cfg(debug_assertions)] diff --git a/godot-core/src/registry/as_func.rs b/godot-core/src/registry/as_func.rs new file mode 100644 index 000000000..f5769e36c --- /dev/null +++ b/godot-core/src/registry/as_func.rs @@ -0,0 +1,46 @@ +/* + * 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/. + */ + +// 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. + +pub trait AsFunc { + fn call(&mut self, maybe_instance: I, params: Ps); +} + +// pub trait AsMethod { +// fn call(&mut self, instance: &mut C, params: Ps); +// } + +// Now generalize via macro: +macro_rules! impl_signal_recipient { + ($( $args:ident : $Ps:ident ),*) => { + // Global and associated functions. + impl AsFunc<(), ( $($Ps,)* )> for F + where F: FnMut( $($Ps,)* ) -> R + { + fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { + self($($args,)*); + } + } + + // Methods. + impl AsFunc<&mut C, ( $($Ps,)* )> for F + where F: FnMut( &mut C, $($Ps,)* ) -> R + { + fn call(&mut self, instance: &mut 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); diff --git a/godot-core/src/registry/mod.rs b/godot-core/src/registry/mod.rs index 9ce2a155d..65678ee0d 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -15,6 +15,11 @@ pub mod method; pub mod plugin; pub mod property; +#[cfg(since_api = "4.2")] +pub mod as_func; +#[cfg(since_api = "4.2")] +pub mod typed_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/builtin/typed_signal.rs b/godot-core/src/registry/typed_signal.rs similarity index 52% rename from godot-core/src/builtin/typed_signal.rs rename to godot-core/src/registry/typed_signal.rs index 11ee86fa8..abf6b8f49 100644 --- a/godot-core/src/builtin/typed_signal.rs +++ b/godot-core/src/registry/typed_signal.rs @@ -7,56 +7,163 @@ // Maybe move this to builtin::functional module? -use crate::builtin::{Callable, Signal, Variant}; -use crate::obj::Gd; +use crate::builtin::{Callable, Variant}; +use crate::obj::{Gd, GodotClass, WithBaseField}; +use crate::registry::as_func::AsFunc; use crate::{classes, meta, sys}; use std::borrow::Cow; use std::fmt; pub trait ParamTuple { fn to_variant_array(&self) -> Vec; + fn from_variant_array(array: &[&Variant]) -> Self; } -impl ParamTuple for () { - fn to_variant_array(&self) -> Vec { - Vec::new() - } +macro_rules! impl_param_tuple { + // Recursive case for tuple with N elements + ($($args:ident : $Ps:ident),*) => { + impl<$($Ps),*> ParamTuple for ($($Ps,)*) + where + $($Ps: meta::ToGodot + meta::FromGodot),* + { + 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())) + ), + )* ) + } + } + }; } -impl ParamTuple for (T,) +impl_param_tuple!(); +impl_param_tuple!(arg0: P0); +impl_param_tuple!(arg0: P0, arg1: P1); +impl_param_tuple!(arg0: P0, arg1: P1, arg2: P2); + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +#[doc(hidden)] +pub enum ObjectRef<'a, C: GodotClass> { + /// Helpful for emit: reuse `&self` from within the `impl` block, goes through `base()` 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 - T: meta::ToGodot, + C: WithBaseField, { - fn to_variant_array(&self) -> Vec { - vec![self.0.to_variant()] + 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(), + } } } // ---------------------------------------------------------------------------------------------------------------------------------------------- -pub struct TypedSignal { - signal: Signal, +pub struct TypedSignal<'a, C: GodotClass, Ps> { + //signal: Signal, + /// 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<'a, C>, + name: Cow<'static, str>, _signature: std::marker::PhantomData, } -impl TypedSignal { - pub(crate) fn from_untyped(signal: Signal) -> Self { +impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { + #[doc(hidden)] + pub fn new(owner: ObjectRef<'a, C>, name: &'static str) -> Self { Self { - signal, + owner, + name: Cow::Borrowed(name), _signature: std::marker::PhantomData, } } - pub fn emit(&self, params: Ps) { - self.signal.emit(¶ms.to_variant_array()); + pub fn emit(&mut self, params: Ps) { + let name = self.name.as_ref(); + + self.owner.with_object_mut(|obj| { + obj.emit_signal(name, ¶ms.to_variant_array()); + }); } - pub fn connect_untyped(&mut self, callable: &Callable, flags: i64) { - self.signal.connect(callable, flags); + /// Connect a method (member function) with `&mut self` as the first parameter. + pub fn connect_self(&mut self, mut function: F) + where + for<'c> F: AsFunc<&'c mut C, Ps> + 'static, + { + // 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. + let callable_name = std::any::type_name_of_val(&function); + + let object = self.owner.to_owned(); + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + + // let mut function = function; + // function.call(instance, args); + let mut object = object.clone(); + + // TODO: how to avoid another bind, when emitting directly from Rust? + let mut instance = object.bind_mut(); + let instance = &mut *instance; + function.call(instance, args); + + Ok(Variant::nil()) + }; + + let name = self.name.as_ref(); + let callable = Callable::from_local_fn(callable_name, godot_fn); + + self.owner.with_object_mut(|obj| { + obj.connect(name, &callable); + }); } - pub fn to_untyped(&self) -> Signal { - self.signal.clone() + /// Connect a static function (global or associated function). + pub fn connect(&mut self, mut function: F) + where + F: AsFunc<(), Ps> + 'static, + { + let callable_name = std::any::type_name_of_val(&function); + + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + function.call((), args); + + Ok(Variant::nil()) + }; + + let name = self.name.as_ref(); + let callable = Callable::from_local_fn(callable_name, godot_fn); + + self.owner.with_object_mut(|obj| { + obj.connect(name, &callable); + }); } } diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 3d16542a4..0e9e368dd 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -193,16 +193,16 @@ pub fn make_func_collection( static_collection_methods.push(quote! { #(#cfg_attrs)* // Use `&self` here to enable `.` chaining, such as in MyClass::static_funcs().my_func(). - fn #rust_func_name(self) -> ::godot::builtin::Func<#generic_args> { + fn #rust_func_name(self) -> ::godot::register::Func<#generic_args> { let class_name = <#class_name as ::godot::obj::GodotClass>::class_name(); - ::godot::builtin::Func::from_static_function(class_name.to_cow_str(), #godot_func_name) + ::godot::register::Func::from_static_function(class_name.to_cow_str(), #godot_func_name) } }); } else { instance_collection_methods.push(quote! { #(#cfg_attrs)* - fn #rust_func_name(self) -> ::godot::builtin::Func<#generic_args> { - ::godot::builtin::Func::from_instance_method(self.obj, #godot_func_name) + fn #rust_func_name(self) -> ::godot::register::Func<#generic_args> { + ::godot::register::Func::from_instance_method(self.obj, #godot_func_name) } }); } diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index f7564b24c..9bee17bb4 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -50,7 +50,7 @@ impl<'a> SignalDetails<'a> { pub fn extract( original_decl: &'a venial::Function, class_name: &'a Ident, - external_attributes: &'a Vec, + external_attributes: &'a [venial::Attribute], ) -> ParseResult> { let mut param_types = vec![]; let mut param_names = vec![]; @@ -109,8 +109,10 @@ pub fn make_signal_registrations( has_builder, } = signal; - let details = SignalDetails::extract(&signature, class_name, external_attributes)?; + 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); } @@ -136,7 +138,7 @@ fn make_signal_registration(details: &SignalDetails, class_name_obj: &TokenStrea .. } = details; - let signature_tuple = util::make_signature_tuple_type("e! { () }, ¶m_types); + let signature_tuple = util::make_signature_tuple_type("e! { () }, param_types); let indexes = 0..param_types.len(); let param_property_infos = quote! { @@ -185,28 +187,24 @@ struct SignalCollection { /// The actual signal definitions, including both `struct` and `impl` blocks. individual_structs: Vec, - // signals_fields: 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.signals_fields.push(quote! { - // #(#signal_cfg_attrs)* - // #signal_name: ::godot::builtin::TypedSignal<#param_tuple> - // }); - self.collection_methods.push(quote! { #(#signal_cfg_attrs)* - fn #signal_name(&mut self) -> #individual_struct_name<'_> { - let object_mut = &mut *self.object_base; - #individual_struct_name { object_mut } + fn #signal_name(self) -> #individual_struct_name<'a> { + #individual_struct_name { + typed: ::godot::register::TypedSignal::new(self.object, #signal_name_str) + } } }); @@ -226,44 +224,53 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { class_name, param_names, param_tuple, - signal_name_str, + // signal_name, signal_cfg_attrs, individual_struct_name, .. } = details; + // let module_name = format_ident!("__godot_signal_{class_name}_{signal_name}"); + + // Module + re-export. + // Could also keep contained in module to reduce namespace pollution, but might make docs a bit more nested. quote! { + // #(#signal_cfg_attrs)* + // mod #module_name { + + // TODO make pub without running into "private type `MySignal` in public interface" errors. #(#signal_cfg_attrs)* #[allow(non_camel_case_types)] - pub struct #individual_struct_name<'a> { - object_mut: &'a mut ::godot::classes::Object - //object_base: ::godot::obj::BaseMut<'a, #class_name>, - //signal: ::godot::builtin::TypedSignal<#param_tuple> + struct #individual_struct_name<'a> { + 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) { - use ::godot::meta::ToGodot; - // Potential optimization: encode args as signature-tuple and use direct ptrcall. - let varargs = [ - #( #param_names.to_variant(), )* - ]; - self.object_mut.emit_signal(#signal_name_str, &varargs); + self.typed.emit((#( #param_names, )*)); } + } - fn connect_fn(&mut self, f: impl FnMut #param_tuple) { + #(#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 } + } - fn connect(mut self, registered_func: ::godot::register::Func) -> Self { - // connect() return value is ignored -- do not write `let _ = ...`, so we can revisit this when adding #[must_use] to Error. - - let callable = registered_func.to_callable(); - self.object_mut.connect(#signal_name_str, &callable); - self + #(#signal_cfg_attrs)* + impl std::ops::DerefMut for #individual_struct_name<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.typed } } + + // #(#signal_cfg_attrs)* + // pub(crate) use #module_name::#individual_struct_name; } } @@ -273,27 +280,27 @@ fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> O return None; } - let struct_name = format_ident!("{}Signals", class_name); - let signals_struct_methods = &collection.collection_methods; + let collection_struct_name = format_ident!("{}Signals", class_name); + let collection_struct_methods = &collection.collection_methods; let individual_structs = collection.individual_structs; let code = quote! { #[allow(non_camel_case_types)] - pub struct #struct_name<'a> { + 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. - object_base: ::godot::obj::BaseMut<'a, #class_name>, + object: ::godot::register::ObjectRef<'a, #class_name> } - impl #struct_name<'_> { - #( #signals_struct_methods )* + impl<'a> #collection_struct_name<'a> { + #( #collection_struct_methods )* } impl ::godot::obj::cap::WithSignals for #class_name { - type SignalCollection<'a> = #struct_name<'a>; + type SignalCollection<'a> = #collection_struct_name<'a>; fn signals(&mut self) -> Self::SignalCollection<'_> { Self::SignalCollection { - object_base: self.base_mut(), + object: ::godot::register::ObjectRef::Internal { obj_mut: self } } } } diff --git a/godot/src/lib.rs b/godot/src/lib.rs index a2f0a8af6..17854c3cd 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -186,12 +186,14 @@ pub mod register { pub use godot_core::registry::property; pub use godot_macros::{godot_api, godot_dyn, Export, GodotClass, GodotConvert, Var}; - // FIXME - pub use godot_core::builtin::Func; - #[cfg(feature = "__codegen-full")] pub use godot_core::registry::RpcConfig; + #[cfg(since_api = "4.2")] + pub use godot_core::registry::as_func::*; + #[cfg(since_api = "4.2")] + pub use godot_core::registry::typed_signal::*; + /// Re-exports used by proc-macro API. #[doc(hidden)] pub mod private { diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 25252d8d6..d9a207f74 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -9,7 +9,7 @@ use crate::framework::itest; use godot::builtin::{GString, Signal, StringName}; use godot::classes::{Object, RefCounted}; use godot::meta::ToGodot; -use godot::obj::cap::{WithFuncs, WithSignals}; +use godot::obj::cap::WithSignals; use godot::obj::{Base, Gd, NewAlloc, NewGd, WithBaseField}; use godot::register::{godot_api, GodotClass}; use godot::sys; @@ -49,24 +49,20 @@ fn signal_symbols_api() { let mut internal = emitter.bind_mut(); internal.connect_signals_internal(); - //internal.signals().emitter_1() drop(internal); // let check = Signal::from_object_signal(&emitter, "emitter_1"); // dbg!(check.connections()); - println!("PRE_EMIT"); - emitter.emit_signal("emitter_1", &[1234.to_variant()]); - println!("POST_EMIT"); - emitter.bind_mut().emit_signals_internal(); + // Check that instance method is invoked. let received_arg = LAST_METHOD_ARG.lock(); assert_eq!(*received_arg, Some(1234), "Emit failed (method)"); - // This is currently broken: - // let received_arg = LAST_STATIC_FUNCTION_ARG.lock(); - // assert_eq!(*received_arg, Some(1234), "Emit failed (static function)"); + // Check that static function is invoked. + let received_arg = LAST_STATIC_FUNCTION_ARG.lock(); + assert_eq!(*received_arg, Some(1234), "Emit failed (static function)"); receiver.free(); emitter.free(); @@ -118,30 +114,20 @@ impl Emitter { #[func] fn self_receive(&mut self, arg1: i64) { - println!("Received instance: {}", arg1); *LAST_METHOD_ARG.lock() = Some(arg1); } #[func] fn self_receive_static(arg1: i64) { - let x = Self::self_receive; - let y = Self::self_receive_static; - - println!("Received static: {}", arg1); *LAST_STATIC_FUNCTION_ARG.lock() = Some(arg1); } // "Internal" means connect/emit happens from within the class (via &mut self). fn connect_signals_internal(&mut self) { - self.signals().emitter_2().connect_fn(|obj, s| { - println!("emitter_2({obj}, {s})"); - }); - - let m = self.funcs().self_receive(); - let s = Self::static_funcs().self_receive_static(); - - self.signals().emitter_1().connect(m).connect(s); + let mut sig = self.signals().emitter_1(); + sig.connect_self(Self::self_receive); + sig.connect(Self::self_receive_static); } fn emit_signals_internal(&mut self) { @@ -180,10 +166,6 @@ impl Receiver { // This should probably have a dedicated key such as #[godot_api(func_refs)] or so... #[signal] fn _just_here_to_generate_funcs(); - - fn func(&self) { - // let f = self.signals().receiver_2(); - } } const SIGNAL_ARG_STRING: &str = "Signal string arg"; From b6fa9e4f080046cf57a2cafdb456bc348a3894a2 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 6 Jan 2025 20:38:12 +0100 Subject: [PATCH 03/14] Own module for typed signals + variadic args --- godot-core/src/registry/as_func.rs | 46 -------- godot-core/src/registry/functional/mod.rs | 12 +++ .../registry/{ => functional}/typed_signal.rs | 100 +----------------- .../src/registry/functional/variadic.rs | 85 +++++++++++++++ godot-core/src/registry/mod.rs | 8 +- godot/src/lib.rs | 6 +- .../builtin_tests/containers/signal_test.rs | 3 + 7 files changed, 108 insertions(+), 152 deletions(-) delete mode 100644 godot-core/src/registry/as_func.rs create mode 100644 godot-core/src/registry/functional/mod.rs rename godot-core/src/registry/{ => functional}/typed_signal.rs (70%) create mode 100644 godot-core/src/registry/functional/variadic.rs diff --git a/godot-core/src/registry/as_func.rs b/godot-core/src/registry/as_func.rs deleted file mode 100644 index f5769e36c..000000000 --- a/godot-core/src/registry/as_func.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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/. - */ - -// 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. - -pub trait AsFunc { - fn call(&mut self, maybe_instance: I, params: Ps); -} - -// pub trait AsMethod { -// fn call(&mut self, instance: &mut C, params: Ps); -// } - -// Now generalize via macro: -macro_rules! impl_signal_recipient { - ($( $args:ident : $Ps:ident ),*) => { - // Global and associated functions. - impl AsFunc<(), ( $($Ps,)* )> for F - where F: FnMut( $($Ps,)* ) -> R - { - fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { - self($($args,)*); - } - } - - // Methods. - impl AsFunc<&mut C, ( $($Ps,)* )> for F - where F: FnMut( &mut C, $($Ps,)* ) -> R - { - fn call(&mut self, instance: &mut 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); diff --git a/godot-core/src/registry/functional/mod.rs b/godot-core/src/registry/functional/mod.rs new file mode 100644 index 000000000..d084defe1 --- /dev/null +++ b/godot-core/src/registry/functional/mod.rs @@ -0,0 +1,12 @@ +/* + * 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 typed_signal; +mod variadic; + +pub use typed_signal::*; +pub use variadic::*; diff --git a/godot-core/src/registry/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs similarity index 70% rename from godot-core/src/registry/typed_signal.rs rename to godot-core/src/registry/functional/typed_signal.rs index abf6b8f49..af6d5c983 100644 --- a/godot-core/src/registry/typed_signal.rs +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -9,51 +9,11 @@ use crate::builtin::{Callable, Variant}; use crate::obj::{Gd, GodotClass, WithBaseField}; -use crate::registry::as_func::AsFunc; -use crate::{classes, meta, sys}; +use crate::registry::functional::{AsFunc, ParamTuple}; +use crate::{classes, sys}; use std::borrow::Cow; use std::fmt; -pub trait ParamTuple { - fn to_variant_array(&self) -> Vec; - fn from_variant_array(array: &[&Variant]) -> Self; -} - -macro_rules! impl_param_tuple { - // Recursive case for tuple with N elements - ($($args:ident : $Ps:ident),*) => { - impl<$($Ps),*> ParamTuple for ($($Ps,)*) - where - $($Ps: meta::ToGodot + meta::FromGodot),* - { - 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())) - ), - )* ) - } - } - }; -} - -impl_param_tuple!(); -impl_param_tuple!(arg0: P0); -impl_param_tuple!(arg0: P0, arg1: P1); -impl_param_tuple!(arg0: P0, arg1: P1, arg2: P2); - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - #[doc(hidden)] pub enum ObjectRef<'a, C: GodotClass> { /// Helpful for emit: reuse `&self` from within the `impl` block, goes through `base()` re-borrowing and thus allows re-entrant calls @@ -167,62 +127,6 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { } } -// ---------------------------------------------------------------------------------------------------------------------------------------------- -/* Previous impl based on assumption, Signal would be used. Could maybe be combined within an enum. - -pub struct TypedSignal { - signal: Signal, - _signature: std::marker::PhantomData, -} - -impl TypedSignal { - pub(crate) fn from_untyped(signal: Signal) -> Self { - Self { - signal, - _signature: std::marker::PhantomData, - } - } - - pub fn emit(&self, params: Ps) { - self.signal.emit(¶ms.to_variant_array()); - } - - pub fn connect_untyped(&mut self, callable: &Callable, flags: i64) { - self.signal.connect(callable, flags); - } - - pub fn to_untyped(&self) -> Signal { - self.signal.clone() - } -}*/ - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -/* -pub struct TypedFunc { - godot_name: &'static str, - _return_type: std::marker::PhantomData, - _param_types: std::marker::PhantomData<(C, Ps)>, -} - -impl TypedFunc { - #[doc(hidden)] - pub fn from_godot_name(godot_name: &'static str) -> Self { - Self { - godot_name, - _return_type: std::marker::PhantomData, - _param_types: std::marker::PhantomData, - } - } - - pub fn with_object(obj: &Gd) {} - - pub fn godot_name(&self) -> &'static str { - self.godot_name - } -} -*/ - // ---------------------------------------------------------------------------------------------------------------------------------------------- /// Type-safe `#[func]` reference that is readily callable. diff --git a/godot-core/src/registry/functional/variadic.rs b/godot-core/src/registry/functional/variadic.rs new file mode 100644 index 000000000..a0f4d6cad --- /dev/null +++ b/godot-core/src/registry/functional/variadic.rs @@ -0,0 +1,85 @@ +/* + * 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; + +pub trait AsFunc { + fn call(&mut self, maybe_instance: I, params: Ps); +} + +macro_rules! impl_signal_recipient { + ($( $args:ident : $Ps:ident ),*) => { + // Global and associated functions. + impl AsFunc<(), ( $($Ps,)* )> for F + where F: FnMut( $($Ps,)* ) -> R + { + fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { + self($($args,)*); + } + } + + // Methods. + impl AsFunc<&mut C, ( $($Ps,)* )> for F + where F: FnMut( &mut C, $($Ps,)* ) -> R + { + fn call(&mut self, instance: &mut 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); + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub trait ParamTuple { + fn to_variant_array(&self) -> Vec; + fn from_variant_array(array: &[&Variant]) -> Self; +} + +macro_rules! impl_param_tuple { + ($($args:ident : $Ps:ident),*) => { + impl<$($Ps),*> ParamTuple for ($($Ps,)*) + where + $($Ps: meta::ToGodot + meta::FromGodot),* + { + 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())) + ), + )* ) + } + } + }; +} + +impl_param_tuple!(); +impl_param_tuple!(arg0: P0); +impl_param_tuple!(arg0: P0, arg1: P1); +impl_param_tuple!(arg0: P0, arg1: P1, arg2: P2); diff --git a/godot-core/src/registry/mod.rs b/godot-core/src/registry/mod.rs index 65678ee0d..26d43d198 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -16,9 +16,11 @@ pub mod plugin; pub mod property; #[cfg(since_api = "4.2")] -pub mod as_func; -#[cfg(since_api = "4.2")] -pub mod typed_signal; +pub mod functional; + +// Contents re-exported in `godot` crate; just keep empty. +#[cfg(before_api = "4.2")] +pub mod functional {} // RpcConfig uses MultiplayerPeer::TransferMode and MultiplayerApi::RpcMode, which are only enabled in `codegen-full` feature. #[cfg(feature = "codegen-full")] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 17854c3cd..5f8902571 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -183,17 +183,13 @@ pub mod init { /// Register/export Rust symbols to Godot: classes, methods, enums... pub mod register { + pub use godot_core::registry::functional::*; pub use godot_core::registry::property; pub use godot_macros::{godot_api, godot_dyn, Export, GodotClass, GodotConvert, Var}; #[cfg(feature = "__codegen-full")] pub use godot_core::registry::RpcConfig; - #[cfg(since_api = "4.2")] - pub use godot_core::registry::as_func::*; - #[cfg(since_api = "4.2")] - pub use godot_core::registry::typed_signal::*; - /// Re-exports used by proc-macro API. #[doc(hidden)] pub mod private { diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index d9a207f74..5e33b973a 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -41,6 +41,7 @@ fn signal_basic_connect_emit() { emitter.free(); } +#[cfg(since_api = "4.2")] #[itest] fn signal_symbols_api() { let mut emitter = Emitter::new_alloc(); @@ -124,12 +125,14 @@ impl Emitter { // "Internal" means connect/emit happens from within the class (via &mut self). + #[cfg(since_api = "4.2")] fn connect_signals_internal(&mut self) { let mut sig = self.signals().emitter_1(); sig.connect_self(Self::self_receive); sig.connect(Self::self_receive_static); } + #[cfg(since_api = "4.2")] fn emit_signals_internal(&mut self) { self.signals().emitter_1().emit(1234); } From 189b541fa9f83c738ff6652bba6aacec7eb14998 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 19 Jan 2025 19:22:32 +0100 Subject: [PATCH 04/14] Provide Gd::signals() for external connections --- godot-core/src/obj/gd.rs | 18 +++++++++ godot-core/src/obj/traits.rs | 28 ++++++++++++++ godot-macros/src/class/data_models/signal.rs | 8 ++++ .../builtin_tests/containers/signal_test.rs | 37 +++++++++++++++++-- itest/rust/src/framework/mod.rs | 3 ++ 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 7a93898b4..199ed58a4 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -18,6 +18,7 @@ use crate::meta::{ ArrayElement, AsArg, CallContext, ClassName, CowArg, FromGodot, GodotConvert, GodotType, ParamType, PropertyHintInfo, RefArg, ToGodot, }; +use crate::obj::cap::WithSignals; use crate::obj::{ bounds, cap, Bounds, DynGd, EngineEnum, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId, RawGd, @@ -705,6 +706,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/traits.rs b/godot-core/src/obj/traits.rs index 059962c14..773085fc5 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -583,7 +583,35 @@ pub mod cap { 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<'_>; } /// Auto-implemented for `#[godot_api] impl MyClass` blocks diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index 9bee17bb4..9945394a4 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -242,6 +242,7 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { #(#signal_cfg_attrs)* #[allow(non_camel_case_types)] struct #individual_struct_name<'a> { + #[doc(hidden)] typed: ::godot::register::TypedSignal<'a, #class_name, #param_tuple>, } @@ -303,6 +304,13 @@ fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> O object: ::godot::register::ObjectRef::Internal { obj_mut: self } } } + + #[doc(hidden)] + fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_> { + Self::SignalCollection { + object: ::godot::register::ObjectRef::External { gd: external.clone() } + } + } } #( #individual_structs )* diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 5e33b973a..9968e8a17 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -15,6 +15,7 @@ use godot::register::{godot_api, GodotClass}; use godot::sys; use godot::sys::Global; use std::cell::Cell; +use std::rc::Rc; #[itest] fn signal_basic_connect_emit() { @@ -41,12 +42,11 @@ fn signal_basic_connect_emit() { emitter.free(); } +// "Internal" means connect/emit happens from within the class, via self.signals(). #[cfg(since_api = "4.2")] -#[itest] -fn signal_symbols_api() { +#[itest(focus)] +fn signal_symbols_internal() { let mut emitter = Emitter::new_alloc(); - let receiver = Receiver::new_alloc(); - let mut internal = emitter.bind_mut(); internal.connect_signals_internal(); @@ -65,6 +65,35 @@ fn signal_symbols_api() { let received_arg = LAST_STATIC_FUNCTION_ARG.lock(); assert_eq!(*received_arg, Some(1234), "Emit failed (static function)"); + emitter.free(); +} + +// "External" means connect/emit happens from outside the class, via Gd::signals(). +#[cfg(since_api = "4.2")] +#[itest(focus)] +fn signal_symbols_external() { + let emitter = Emitter::new_alloc(); + + // Local function; deliberately use a !Send type. + let received = Rc::new(Cell::new(0)); + let received_clone = received.clone(); + emitter.signals().emitter_1().connect(move |i| { + received_clone.set(i); + }); + + // Self-modifying method. + emitter + .signals() + .emitter_1() + .connect_self(Emitter::self_receive); + + // Connect to other object. + let receiver = Receiver::new_alloc(); + + // Emit and check received values. + emitter.signals().emitter_1().emit(2345); + assert_eq!(received.get(), 2345); + receiver.free(); emitter.free(); } 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; From 577797946d59de7e0d234aa6ec49e4741c8da311 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 19 Jan 2025 20:04:12 +0100 Subject: [PATCH 05/14] Add TypedSignal::connect_obj() to connect to other user objects --- .../src/registry/functional/typed_signal.rs | 48 ++++++++++-- .../builtin_tests/containers/signal_test.rs | 73 +++++++++++++------ 2 files changed, 92 insertions(+), 29 deletions(-) diff --git a/godot-core/src/registry/functional/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs index af6d5c983..f84e43074 100644 --- a/godot-core/src/registry/functional/typed_signal.rs +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -8,7 +8,7 @@ // Maybe move this to builtin::functional module? use crate::builtin::{Callable, Variant}; -use crate::obj::{Gd, GodotClass, WithBaseField}; +use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; use crate::registry::functional::{AsFunc, ParamTuple}; use crate::{classes, sys}; use std::borrow::Cow; @@ -71,6 +71,37 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { }); } + /// 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]. + pub fn connect(&mut self, mut function: F) + where + F: AsFunc<(), Ps> + 'static, + { + let callable_name = std::any::type_name_of_val(&function); + + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + function.call((), args); + + Ok(Variant::nil()) + }; + + let name = self.name.as_ref(); + let callable = Callable::from_local_fn(callable_name, godot_fn); + + self.owner.with_object_mut(|obj| { + obj.connect(name, &callable); + }); + } + /// Connect a method (member function) with `&mut self` as the first parameter. pub fn connect_self(&mut self, mut function: F) where @@ -104,16 +135,23 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { }); } - /// Connect a static function (global or associated function). - pub fn connect(&mut self, mut function: F) + /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. + /// + /// To connect to methods on the same object, use [`connect_self()`][Self::connect_self]. + pub fn connect_obj(&mut self, object: &Gd, mut function: F) where - F: AsFunc<(), Ps> + 'static, + OtherC: GodotClass + Bounds, + for<'c> F: AsFunc<&'c mut OtherC, Ps> + 'static, { let callable_name = std::any::type_name_of_val(&function); + let mut object = object.clone(); let godot_fn = move |variant_args: &[&Variant]| -> Result { let args = Ps::from_variant_array(variant_args); - function.call((), args); + + let mut instance = object.bind_mut(); + let instance = &mut *instance; + function.call(instance, args); Ok(Variant::nil()) }; diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 9968e8a17..7899070d6 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -44,12 +44,14 @@ fn signal_basic_connect_emit() { // "Internal" means connect/emit happens from within the class, via self.signals(). #[cfg(since_api = "4.2")] -#[itest(focus)] +#[itest] fn signal_symbols_internal() { let mut emitter = Emitter::new_alloc(); - let mut internal = emitter.bind_mut(); - internal.connect_signals_internal(); + // 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); // let check = Signal::from_object_signal(&emitter, "emitter_1"); @@ -57,42 +59,57 @@ fn signal_symbols_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. - let received_arg = LAST_METHOD_ARG.lock(); - assert_eq!(*received_arg, Some(1234), "Emit failed (method)"); + assert_eq!(emitter.bind().last_received, 1234, "Emit failed (method)"); // Check that static function is invoked. - let received_arg = LAST_STATIC_FUNCTION_ARG.lock(); - assert_eq!(*received_arg, Some(1234), "Emit failed (static function)"); + assert_eq!( + *LAST_STATIC_FUNCTION_ARG.lock(), + 1234, + "Emit failed (static function)" + ); emitter.free(); } // "External" means connect/emit happens from outside the class, via Gd::signals(). #[cfg(since_api = "4.2")] -#[itest(focus)] +#[itest] fn signal_symbols_external() { let emitter = Emitter::new_alloc(); // Local function; deliberately use a !Send type. - let received = Rc::new(Cell::new(0)); - let received_clone = received.clone(); - emitter.signals().emitter_1().connect(move |i| { - received_clone.set(i); + let tracker = Rc::new(Cell::new(0)); + let tracker_copy = tracker.clone(); + let mut sig = emitter.signals().emitter_1(); + sig.connect(move |i| { + tracker_copy.set(i); }); // Self-modifying method. - emitter - .signals() - .emitter_1() - .connect_self(Emitter::self_receive); + sig.connect_self(Emitter::self_receive); // Connect to other object. let receiver = Receiver::new_alloc(); + sig.connect_obj(&receiver, Receiver::receiver_1_mut); + + // Emit signal. + sig.emit(987); + + // Check that closure is invoked. + assert_eq!(tracker.get(), 987, "Emit failed (closure)"); - // Emit and check received values. - emitter.signals().emitter_1().emit(2345); - assert_eq!(received.get(), 2345); + // Check that instance method is invoked. + assert_eq!(emitter.bind().last_received, 987, "Emit failed (method)"); + + // Check that *other* instance method is invoked. + assert!( + receiver.bind().used[1].get(), + "Emit failed (other object method)" + ); receiver.free(); emitter.free(); @@ -122,13 +139,14 @@ fn signal_construction_and_id() { // Helper types /// Global sets the value of the received argument and whether it was a static function. -static LAST_METHOD_ARG: Global> = Global::default(); -static LAST_STATIC_FUNCTION_ARG: Global> = Global::default(); +static LAST_STATIC_FUNCTION_ARG: Global = Global::default(); #[derive(GodotClass)] #[class(init, base=Object)] struct Emitter { _base: Base, + #[cfg(since_api = "4.2")] + last_received: i64, } #[godot_api] @@ -144,21 +162,22 @@ impl Emitter { #[func] fn self_receive(&mut self, arg1: i64) { - *LAST_METHOD_ARG.lock() = Some(arg1); + self.last_received = arg1; } #[func] fn self_receive_static(arg1: i64) { - *LAST_STATIC_FUNCTION_ARG.lock() = Some(arg1); + *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) { + fn connect_signals_internal(&mut self, tracker: Rc>) { let mut sig = self.signals().emitter_1(); sig.connect_self(Self::self_receive); sig.connect(Self::self_receive_static); + sig.connect(move |i| tracker.set(i)); } #[cfg(since_api = "4.2")] @@ -187,6 +206,12 @@ impl Receiver { assert_eq!(arg1, 987); } + // TODO remove as soon as shared-ref emitter receivers are supported. + fn receiver_1_mut(&mut self, arg1: i64) { + self.used[1].set(true); + assert_eq!(arg1, 987); + } + #[func] fn receiver_2(&self, arg1: Gd, arg2: GString) { assert_eq!(self.base().clone(), arg1); From 880e18b635ca7d461ad65cf48d84604b321796fb Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 9 Feb 2025 21:40:53 +0100 Subject: [PATCH 06/14] Add ConnectBuilder as type-state builder --- .../registry/functional/connect_builder.rs | 282 ++++++++++++++++++ godot-core/src/registry/functional/mod.rs | 1 + .../src/registry/functional/typed_signal.rs | 84 ++++-- .../src/registry/functional/variadic.rs | 6 +- .../builtin_tests/containers/signal_test.rs | 56 +++- .../native_audio_structures_test.rs | 1 - 6 files changed, 389 insertions(+), 41 deletions(-) create mode 100644 godot-core/src/registry/functional/connect_builder.rs diff --git a/godot-core/src/registry/functional/connect_builder.rs b/godot-core/src/registry/functional/connect_builder.rs new file mode 100644 index 000000000..e7fb3dbac --- /dev/null +++ b/godot-core/src/registry/functional/connect_builder.rs @@ -0,0 +1,282 @@ +/* + * 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::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::registry::functional::{AsFunc, ParamTuple, TypedSignal}; +use crate::{meta, sys}; + +/// Type-state builder for customizing signal connections. +/// +/// Allows a high degree of customization for connecting signals, while maintaining complete type safety. +/// +/// # 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 `object_self` or `object` was called in stage 2. +/// - [`method_mut`][Self::method_mut]: Connect a `&mut self` method. +/// +/// ## Stage 3 +/// All these methods are optional, and they can be combined. +/// - [`sync`][Self::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: 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: AsFunc<(), Ps>, + { + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + function.call((), args); + + Ok(Variant::nil()) + }; + + let mut data = self.data; + data.callable_name = Some(sys::short_type_name::().into()); + + ConnectBuilder { + parent_sig: self.parent_sig, + data, + godot_fn, + receiver_obj: (), + } + } + + 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, + } + } + + 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, + } + } +} + +impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: 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: AsFunc<&'c_rcv mut CRcv, Ps>, + { + let mut gd: Gd = self.receiver_obj; + + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + let mut guard = gd.bind_mut(); + let instance = &mut *guard; + method_with_mut_self.call(instance, args); + + Ok(Variant::nil()) + }; + + let mut data = self.data; + data.callable_name = Some(sys::short_type_name::().into()); + + ConnectBuilder { + parent_sig: self.parent_sig, + data, + godot_fn, + receiver_obj: (), + } + } +} + +impl<'ts, 'c, CSig, CRcv, Ps, GodotFn> ConnectBuilder<'ts, 'c, CSig, CRcv, Ps, GodotFn> +where + CSig: WithBaseField, + Ps: ParamTuple, + GodotFn: FnMut(&[&Variant]) -> Result + 'static, +{ + #[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, + } + } + + /// 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 + } + + /// 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 + } + + 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.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 callable_name_ref(&self) -> &GString { + self.callable_name + .as_ref() + .expect("Signal connect name not set; this is a bug.") + } +} diff --git a/godot-core/src/registry/functional/mod.rs b/godot-core/src/registry/functional/mod.rs index d084defe1..9edb7e74c 100644 --- a/godot-core/src/registry/functional/mod.rs +++ b/godot-core/src/registry/functional/mod.rs @@ -5,6 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +mod connect_builder; mod typed_signal; mod variadic; diff --git a/godot-core/src/registry/functional/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs index f84e43074..825374b90 100644 --- a/godot-core/src/registry/functional/typed_signal.rs +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -7,12 +7,15 @@ // Maybe move this to builtin::functional module? -use crate::builtin::{Callable, Variant}; +use crate::builtin::{Callable, GString, Variant}; +use crate::classes::object::ConnectFlags; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; +use crate::registry::functional::connect_builder::ConnectBuilder; use crate::registry::functional::{AsFunc, ParamTuple}; -use crate::{classes, sys}; +use crate::{classes, meta, sys}; use std::borrow::Cow; use std::fmt; +use std::marker::PhantomData; #[doc(hidden)] pub enum ObjectRef<'a, C: GodotClass> { @@ -45,24 +48,28 @@ where // ---------------------------------------------------------------------------------------------------------------------------------------------- -pub struct TypedSignal<'a, C: GodotClass, Ps> { +pub struct TypedSignal<'c, C: GodotClass, Ps> { //signal: Signal, /// 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<'a, C>, + owner: ObjectRef<'c, C>, name: Cow<'static, str>, - _signature: std::marker::PhantomData, + _signature: PhantomData, } -impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { +impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { #[doc(hidden)] - pub fn new(owner: ObjectRef<'a, C>, name: &'static str) -> Self { + pub fn new(owner: ObjectRef<'c, C>, name: &'static str) -> Self { Self { owner, name: Cow::Borrowed(name), - _signature: std::marker::PhantomData, + _signature: PhantomData, } } + pub(crate) fn receiver_object(&self) -> Gd { + self.owner.to_owned() + } + pub fn emit(&mut self, params: Ps) { let name = self.name.as_ref(); @@ -83,7 +90,7 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { /// To connect to a method of the own object `self`, use [`connect_self()`][Self::connect_self]. pub fn connect(&mut self, mut function: F) where - F: AsFunc<(), Ps> + 'static, + F: AsFunc<(), Ps>, { let callable_name = std::any::type_name_of_val(&function); @@ -94,18 +101,13 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { Ok(Variant::nil()) }; - let name = self.name.as_ref(); - let callable = Callable::from_local_fn(callable_name, godot_fn); - - self.owner.with_object_mut(|obj| { - obj.connect(name, &callable); - }); + self.inner_connect_local(callable_name, godot_fn); } /// Connect a method (member function) with `&mut self` as the first parameter. pub fn connect_self(&mut self, mut function: F) where - for<'c> F: AsFunc<&'c mut C, Ps> + 'static, + for<'c_rcv> F: AsFunc<&'c_rcv mut C, Ps>, { // 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. @@ -127,12 +129,7 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { Ok(Variant::nil()) }; - let name = self.name.as_ref(); - let callable = Callable::from_local_fn(callable_name, godot_fn); - - self.owner.with_object_mut(|obj| { - obj.connect(name, &callable); - }); + self.inner_connect_local(callable_name, godot_fn); } /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. @@ -141,7 +138,7 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { pub fn connect_obj(&mut self, object: &Gd, mut function: F) where OtherC: GodotClass + Bounds, - for<'c> F: AsFunc<&'c mut OtherC, Ps> + 'static, + for<'c_rcv> F: AsFunc<&'c_rcv mut OtherC, Ps>, { let callable_name = std::any::type_name_of_val(&function); @@ -156,13 +153,38 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { Ok(Variant::nil()) }; - let name = self.name.as_ref(); + self.inner_connect_local(callable_name, godot_fn); + } + + fn inner_connect_local(&mut self, callable_name: impl meta::AsArg, godot_fn: F) + where + F: FnMut(&[&Variant]) -> Result + 'static, + { + let signal_name = self.name.as_ref(); let callable = Callable::from_local_fn(callable_name, godot_fn); self.owner.with_object_mut(|obj| { - obj.connect(name, &callable); + obj.connect(signal_name, &callable); }); } + + pub(super) fn connect_untyped(&mut self, callable: &Callable, flags: Option) { + use crate::obj::EngineEnum; + + 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(); + }); + } + + pub fn connect_builder(&mut self) -> ConnectBuilder<'_, 'c, C, (), Ps, ()> { + ConnectBuilder::new(self) + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -176,8 +198,8 @@ impl<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { pub struct Func { godot_function_name: &'static str, callable_kind: CallableKind, - _return_type: std::marker::PhantomData, - _param_types: std::marker::PhantomData, + _return_type: PhantomData, + _param_types: PhantomData, } enum CallableKind { @@ -199,8 +221,8 @@ impl Func { Self { godot_function_name: method_godot_name, callable_kind: CallableKind::Method { bound_object }, - _return_type: std::marker::PhantomData, - _param_types: std::marker::PhantomData, + _return_type: PhantomData, + _param_types: PhantomData, } } @@ -212,8 +234,8 @@ impl Func { Self { godot_function_name: method_godot_name, callable_kind: CallableKind::StaticFunction { class_godot_name }, - _return_type: std::marker::PhantomData, - _param_types: std::marker::PhantomData, + _return_type: PhantomData, + _param_types: PhantomData, } } diff --git a/godot-core/src/registry/functional/variadic.rs b/godot-core/src/registry/functional/variadic.rs index a0f4d6cad..8d672ed3a 100644 --- a/godot-core/src/registry/functional/variadic.rs +++ b/godot-core/src/registry/functional/variadic.rs @@ -14,7 +14,7 @@ use crate::builtin::Variant; use crate::meta; -pub trait AsFunc { +pub trait AsFunc: 'static { fn call(&mut self, maybe_instance: I, params: Ps); } @@ -22,7 +22,7 @@ macro_rules! impl_signal_recipient { ($( $args:ident : $Ps:ident ),*) => { // Global and associated functions. impl AsFunc<(), ( $($Ps,)* )> for F - where F: FnMut( $($Ps,)* ) -> R + where F: FnMut( $($Ps,)* ) -> R + 'static { fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { self($($args,)*); @@ -31,7 +31,7 @@ macro_rules! impl_signal_recipient { // Methods. impl AsFunc<&mut C, ( $($Ps,)* )> for F - where F: FnMut( &mut C, $($Ps,)* ) -> R + where F: FnMut( &mut C, $($Ps,)* ) -> R + 'static { fn call(&mut self, instance: &mut C, ($($args,)*): ( $($Ps,)* )) { self(instance, $($args,)*); diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 7899070d6..eb38fdf60 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -80,14 +80,16 @@ fn signal_symbols_internal() { #[itest] fn signal_symbols_external() { let emitter = Emitter::new_alloc(); + let mut sig = emitter.signals().emitter_1(); // Local function; deliberately use a !Send type. let tracker = Rc::new(Cell::new(0)); - let tracker_copy = tracker.clone(); - let mut sig = emitter.signals().emitter_1(); - sig.connect(move |i| { - tracker_copy.set(i); - }); + { + let tracker = tracker.clone(); + sig.connect(move |i| { + tracker.set(i); + }); + } // Self-modifying method. sig.connect_self(Emitter::self_receive); @@ -96,6 +98,15 @@ fn signal_symbols_external() { let receiver = Receiver::new_alloc(); sig.connect_obj(&receiver, Receiver::receiver_1_mut); + let local_tracker = Rc::new(Cell::new(0)); + { + let local_tracker = local_tracker.clone(); + let mut sig = emitter.signals().emitter_1(); + sig.connect_builder() + .function(move |i| local_tracker.set(i)) + .done(); + } + // Emit signal. sig.emit(987); @@ -111,10 +122,40 @@ fn signal_symbols_external() { "Emit failed (other object method)" ); + // Check that closures set up with builder are invoked. + assert_eq!(local_tracker.get(), 987, "Emit failed (builder local)"); + receiver.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().emitter_1(); + + 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(); @@ -162,7 +203,10 @@ impl Emitter { #[func] fn self_receive(&mut self, arg1: i64) { - self.last_received = arg1; + #[cfg(since_api = "4.2")] + { + self.last_received = arg1; + } } #[func] 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}; From 9f9ee1e0e84ebaa769bae2f8f1cf873545f095b6 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 16 Feb 2025 23:41:55 +0100 Subject: [PATCH 07/14] CI: make doc-lints job stricter, fix warnings --- .github/workflows/full-ci.yml | 1 + godot-core/src/obj/script.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 3071825af..acac818e3 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/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 From 2ce3c7d41b3e38ef17c6a4c3db8a35f4e4a4ca6c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 16 Feb 2025 21:10:28 +0100 Subject: [PATCH 08/14] Add ConnectBuilder::method_immut() + docs --- .../registry/functional/connect_builder.rs | 74 +++++++++++++++-- godot-core/src/registry/functional/mod.rs | 1 + .../src/registry/functional/typed_signal.rs | 2 +- .../src/registry/functional/variadic.rs | 11 ++- .../builtin_tests/containers/signal_test.rs | 79 ++++++++++++++++--- 5 files changed, 147 insertions(+), 20 deletions(-) diff --git a/godot-core/src/registry/functional/connect_builder.rs b/godot-core/src/registry/functional/connect_builder.rs index e7fb3dbac..986a40bcd 100644 --- a/godot-core/src/registry/functional/connect_builder.rs +++ b/godot-core/src/registry/functional/connect_builder.rs @@ -15,6 +15,16 @@ use crate::{meta, sys}; /// /// 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 @@ -27,12 +37,14 @@ use crate::{meta, sys}; /// - [`object`][Self::object]: If you want to connect a method, running on a separate object. /// /// ## Stage 2 (conditional) -/// Required iff `object_self` or `object` was called in stage 2. +/// 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. -/// - [`sync`][Self::sync]: If the signal connection should be callable across threads. \ +// 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. @@ -94,6 +106,7 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: ParamTuple> ConnectBuilder<'ts, 'c, CSig, } } + /// **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(); @@ -105,15 +118,16 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: ParamTuple> ConnectBuilder<'ts, 'c, CSig, } } + /// **Stage 1:** prepare for a method taking any `Gd` object. pub fn object( self, - object: Gd, + object: &Gd, ) -> ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> { ConnectBuilder { parent_sig: self.parent_sig, data: self.data, godot_fn: (), - receiver_obj: object, + receiver_obj: object.clone(), } } } @@ -159,14 +173,58 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: ParamTuple> 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: AsFunc<&'c_rcv CRcv, Ps>, + { + let gd: Gd = self.receiver_obj; + + let godot_fn = move |variant_args: &[&Variant]| -> Result { + let args = Ps::from_variant_array(variant_args); + let guard = gd.bind(); + let instance = &*guard; + method_with_shared_self.call(instance, args); + + Ok(Variant::nil()) + }; + + let mut data = self.data; + data.callable_name = Some(sys::short_type_name::().into()); + + ConnectBuilder { + parent_sig: self.parent_sig, + data, + 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: 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, @@ -208,7 +266,7 @@ where } } - /// Name of the `Callable`, mostly used for debugging. + /// **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 { @@ -222,7 +280,7 @@ where self } - /// Add one or multiple flags to the connection, possibly combined with `|` operator. + /// **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(), @@ -233,6 +291,10 @@ where 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, diff --git a/godot-core/src/registry/functional/mod.rs b/godot-core/src/registry/functional/mod.rs index 9edb7e74c..f05a5b0d4 100644 --- a/godot-core/src/registry/functional/mod.rs +++ b/godot-core/src/registry/functional/mod.rs @@ -9,5 +9,6 @@ mod connect_builder; mod typed_signal; mod variadic; +pub use connect_builder::*; pub use typed_signal::*; pub use variadic::*; diff --git a/godot-core/src/registry/functional/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs index 825374b90..6af0f5fbb 100644 --- a/godot-core/src/registry/functional/typed_signal.rs +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -174,7 +174,7 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { let signal_name = self.name.as_ref(); self.owner.with_object_mut(|obj| { - let mut c = obj.connect_ex(signal_name, &callable); + let mut c = obj.connect_ex(signal_name, callable); if let Some(flags) = flags { c = c.flags(flags.ord() as u32); } diff --git a/godot-core/src/registry/functional/variadic.rs b/godot-core/src/registry/functional/variadic.rs index 8d672ed3a..b7475206b 100644 --- a/godot-core/src/registry/functional/variadic.rs +++ b/godot-core/src/registry/functional/variadic.rs @@ -29,7 +29,7 @@ macro_rules! impl_signal_recipient { } } - // Methods. + // Methods with mutable receiver - &mut self. impl AsFunc<&mut C, ( $($Ps,)* )> for F where F: FnMut( &mut C, $($Ps,)* ) -> R + 'static { @@ -37,6 +37,15 @@ macro_rules! impl_signal_recipient { self(instance, $($args,)*); } } + + // Methods with immutable receiver - &self. + impl AsFunc<&C, ( $($Ps,)* )> for F + where F: FnMut( &C, $($Ps,)* ) -> R + 'static + { + fn call(&mut self, instance: &C, ($($args,)*): ( $($Ps,)* )) { + self(instance, $($args,)*); + } + } }; } diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index eb38fdf60..529c0afeb 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -98,15 +98,6 @@ fn signal_symbols_external() { let receiver = Receiver::new_alloc(); sig.connect_obj(&receiver, Receiver::receiver_1_mut); - let local_tracker = Rc::new(Cell::new(0)); - { - let local_tracker = local_tracker.clone(); - let mut sig = emitter.signals().emitter_1(); - sig.connect_builder() - .function(move |i| local_tracker.set(i)) - .done(); - } - // Emit signal. sig.emit(987); @@ -122,10 +113,75 @@ fn signal_symbols_external() { "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().emitter_1(); + + // 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::receiver_1_mut) + .done(); + + // Connect to yet another object, immutable receiver. + let receiver_immut = Receiver::new_alloc(); + sig.connect_builder() + .object(&receiver_immut) + .method_immut(Receiver::receiver_1) + .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, + 552, + "Emit failed (mut method)" + ); + + // Check that *other* instance method is invoked. + assert!( + receiver_immut.bind().used[1].get(), + "Emit failed (other object, immut method)" + ); + + // Check that *other* instance method is invoked. + assert!( + receiver_mut.bind().used[1].get(), + "Emit failed (other object, mut method)" + ); + // Check that closures set up with builder are invoked. - assert_eq!(local_tracker.get(), 987, "Emit failed (builder local)"); + assert_eq!(tracker.get(), 552, "Emit failed (builder local)"); - receiver.free(); + receiver_immut.free(); + receiver_mut.free(); emitter.free(); } @@ -250,7 +306,6 @@ impl Receiver { assert_eq!(arg1, 987); } - // TODO remove as soon as shared-ref emitter receivers are supported. fn receiver_1_mut(&mut self, arg1: i64) { self.used[1].set(true); assert_eq!(arg1, 987); From 3422c40e2717e088b441d08ae3379685da0e4148 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 16 Feb 2025 23:27:04 +0100 Subject: [PATCH 09/14] Move WithSignals up (obj::cap -> obj) --- godot-core/src/obj/gd.rs | 3 +- godot-core/src/obj/traits.rs | 68 +++++++++---------- godot-macros/src/class/data_models/signal.rs | 2 +- godot-macros/src/lib.rs | 58 +++++++++------- godot/src/prelude.rs | 4 +- .../builtin_tests/containers/signal_test.rs | 3 +- 6 files changed, 73 insertions(+), 65 deletions(-) diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 199ed58a4..0201e94d3 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -18,10 +18,9 @@ use crate::meta::{ ArrayElement, AsArg, CallContext, ClassName, CowArg, FromGodot, GodotConvert, GodotType, ParamType, PropertyHintInfo, RefArg, ToGodot, }; -use crate::obj::cap::WithSignals; use crate::obj::{ bounds, cap, Bounds, DynGd, EngineEnum, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, - InstanceId, RawGd, + InstanceId, RawGd, WithSignals, }; use crate::private::callbacks; use crate::registry::property::{Export, Var}; diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index 8082fe8a5..a276a06a2 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -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. @@ -588,40 +622,6 @@ pub mod cap { fn funcs(&self) -> Self::FuncCollection; } - 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<'_>; - } - /// Auto-implemented for `#[godot_api] impl MyClass` blocks pub trait ImplementsGodotApi: GodotClass { #[doc(hidden)] diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index 9945394a4..4ea66a742 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -296,7 +296,7 @@ fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> O #( #collection_struct_methods )* } - impl ::godot::obj::cap::WithSignals for #class_name { + impl ::godot::obj::WithSignals for #class_name { type SignalCollection<'a> = #collection_struct_name<'a>; fn signals(&mut self) -> Self::SignalCollection<'_> { 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/src/prelude.rs b/godot/src/prelude.rs index 8de770a25..da05db9d2 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -33,8 +33,8 @@ pub use super::obj::{ // Make trait methods available. pub use super::obj::cap::WithFuncs as _; // funcs() -pub use super::obj::cap::WithSignals as _; // signals() -pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd() +pub use super::obj::WithBaseField as _; +pub use super::obj::WithSignals as _; // signals() // base(), base_mut(), to_gd() pub use super::obj::EngineBitfield as _; pub use super::obj::EngineEnum as _; diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 529c0afeb..fe4bce1e8 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -9,8 +9,7 @@ use crate::framework::itest; use godot::builtin::{GString, Signal, StringName}; use godot::classes::{Object, RefCounted}; use godot::meta::ToGodot; -use godot::obj::cap::WithSignals; -use godot::obj::{Base, Gd, NewAlloc, NewGd, WithBaseField}; +use godot::obj::{Base, Gd, NewAlloc, NewGd, WithBaseField, WithSignals}; use godot::register::{godot_api, GodotClass}; use godot::sys; use godot::sys::Global; From 1137114fa1324e78dba1a96157a01d3aa75fe10c Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 23 Feb 2025 10:28:55 +0100 Subject: [PATCH 10/14] Remove Func and generated c.funcs(), C::static_funcs() Now no longer necessary for signals with Rust-side function references, but might still be useful one day. --- godot-core/src/obj/traits.rs | 9 -- .../src/registry/functional/typed_signal.rs | 93 +----------------- godot-macros/src/class/data_models/func.rs | 94 ------------------- .../src/class/data_models/inherent_impl.rs | 13 +-- godot-macros/src/util/mod.rs | 12 --- godot/src/prelude.rs | 1 - 6 files changed, 5 insertions(+), 217 deletions(-) diff --git a/godot-core/src/obj/traits.rs b/godot-core/src/obj/traits.rs index a276a06a2..7aabdc6c8 100644 --- a/godot-core/src/obj/traits.rs +++ b/godot-core/src/obj/traits.rs @@ -613,15 +613,6 @@ pub mod cap { fn __godot_validate_property(&self, property: &mut PropertyInfo); } - // Move one level up, like WithBaseField? - pub trait WithFuncs { - type FuncCollection; - type StaticFuncCollection; - - fn static_funcs() -> Self::StaticFuncCollection; - fn funcs(&self) -> Self::FuncCollection; - } - /// Auto-implemented for `#[godot_api] impl MyClass` blocks pub trait ImplementsGodotApi: GodotClass { #[doc(hidden)] diff --git a/godot-core/src/registry/functional/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs index 6af0f5fbb..a7022d1eb 100644 --- a/godot-core/src/registry/functional/typed_signal.rs +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -10,11 +10,9 @@ use crate::builtin::{Callable, GString, Variant}; use crate::classes::object::ConnectFlags; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; -use crate::registry::functional::connect_builder::ConnectBuilder; -use crate::registry::functional::{AsFunc, ParamTuple}; -use crate::{classes, meta, sys}; +use crate::registry::functional::{AsFunc, ConnectBuilder, ParamTuple}; +use crate::{classes, meta}; use std::borrow::Cow; -use std::fmt; use std::marker::PhantomData; #[doc(hidden)] @@ -186,90 +184,3 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { ConnectBuilder::new(self) } } - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -/// Type-safe `#[func]` reference that is readily callable. -/// -/// Can be either a static function of a class, or a method which is bound to a concrete object. -/// -/// This can be seen as a more type-safe variant of Godot's `Callable`, which can carry intermediate information about function signatures (e.g. -/// when connecting signals). -pub struct Func { - godot_function_name: &'static str, - callable_kind: CallableKind, - _return_type: PhantomData, - _param_types: PhantomData, -} - -enum CallableKind { - StaticFunction { - // Maybe class name can be moved out (and also be useful for methods), e.g. Debug impl or so. - class_godot_name: Cow<'static, str>, - }, - Method { - bound_object: Gd, - }, -} - -impl Func { - #[doc(hidden)] - pub fn from_instance_method( - bound_object: Gd, - method_godot_name: &'static str, - ) -> Self { - Self { - godot_function_name: method_godot_name, - callable_kind: CallableKind::Method { bound_object }, - _return_type: PhantomData, - _param_types: PhantomData, - } - } - - #[doc(hidden)] - pub fn from_static_function( - class_godot_name: Cow<'static, str>, - method_godot_name: &'static str, - ) -> Self { - Self { - godot_function_name: method_godot_name, - callable_kind: CallableKind::StaticFunction { class_godot_name }, - _return_type: PhantomData, - _param_types: PhantomData, - } - } - - pub fn to_callable(&self) -> Callable { - match &self.callable_kind { - CallableKind::StaticFunction { class_godot_name } => { - let class_name = class_godot_name.as_ref(); - Callable::from_local_static(class_name, self.godot_function_name) - } - CallableKind::Method { bound_object } => { - Callable::from_object_method(bound_object, self.godot_function_name) - } - } - } -} - -impl fmt::Debug for Func { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let r = sys::short_type_name::(); - let ps = sys::short_type_name::(); - - let (obj_or_class, is_static); - match &self.callable_kind { - CallableKind::StaticFunction { class_godot_name } => { - obj_or_class = class_godot_name.to_string(); - is_static = "; static"; - } - CallableKind::Method { bound_object } => { - obj_or_class = format!("{bound_object:?}"); - is_static = ""; - } - }; - - let function = self.godot_function_name; - write!(f, "Func({obj_or_class}.{function}{is_static}; {ps} -> {r})") - } -} diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index b9257e646..70e971f7e 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -171,96 +171,6 @@ pub fn make_method_registration( Ok(registration) } -// See also make_signal_collection(). -pub fn make_func_collection( - class_name: &Ident, - func_definitions: &[FuncDefinition], -) -> TokenStream { - let instance_collection = format_ident!("{}Funcs", class_name); - let static_collection = format_ident!("{}StaticFuncs", class_name); - - let mut instance_collection_methods = vec![]; - let mut static_collection_methods = vec![]; - - for func in func_definitions { - let rust_func_name = func.rust_ident(); - let godot_func_name = func.godot_name(); - - let signature_info = &func.signature_info; - let generic_args = signature_info.separate_return_params_args(); - - // Transport #[cfg] attrs to the FFI glue to ensure functions which were conditionally - // removed from compilation don't cause errors. - // TODO remove code duplication + double computation, see above. - let cfg_attrs = util::extract_cfg_attrs(&func.external_attributes) - .into_iter() - .collect::>(); - - if func.signature_info.receiver_type == ReceiverType::Static { - static_collection_methods.push(quote! { - #(#cfg_attrs)* - // Use `&self` here to enable `.` chaining, such as in MyClass::static_funcs().my_func(). - fn #rust_func_name(self) -> ::godot::register::Func<#generic_args> { - let class_name = <#class_name as ::godot::obj::GodotClass>::class_name(); - ::godot::register::Func::from_static_function(class_name.to_cow_str(), #godot_func_name) - } - }); - } else { - instance_collection_methods.push(quote! { - #(#cfg_attrs)* - fn #rust_func_name(self) -> ::godot::register::Func<#generic_args> { - ::godot::register::Func::from_instance_method(self.obj, #godot_func_name) - } - }); - } - } - - quote! { - #[non_exhaustive] // Prevent direct instantiation. - #[allow(non_camel_case_types)] - pub struct #instance_collection { - // Could use #class_name instead of Object, but right now the inner Func<..> type anyway uses Object. - obj: ::godot::obj::Gd<::godot::classes::Object>, - } - - impl #instance_collection { - #[doc(hidden)] - pub fn __internal(obj: ::godot::obj::Gd<::godot::classes::Object>) -> Self { - Self { obj } - } - - #( #instance_collection_methods )* - } - - #[non_exhaustive] // Prevent direct instantiation. - #[allow(non_camel_case_types)] - pub struct #static_collection {} - - impl #static_collection { - #[doc(hidden)] - pub fn __internal() -> Self { - Self {} - } - - #( #static_collection_methods )* - } - - impl ::godot::obj::cap::WithFuncs for #class_name { - type FuncCollection = #instance_collection; - type StaticFuncCollection = #static_collection; - - fn funcs(&self) -> Self::FuncCollection { - let obj = ::to_gd(self); - Self::FuncCollection::__internal(obj.upcast()) - } - - fn static_funcs() -> Self::StaticFuncCollection { - Self::StaticFuncCollection::__internal() - } - } - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation @@ -299,10 +209,6 @@ impl SignatureInfo { // Note: for GdSelf receivers, first parameter is not even part of SignatureInfo anymore. util::make_signature_tuple_type(&self.ret_type, &self.param_types) } - - pub fn separate_return_params_args(&self) -> TokenStream { - util::make_signature_generic_args(&self.ret_type, &self.param_types) - } } #[derive(Copy, Clone)] diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 4c1a8ce6e..0383c8368 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -6,9 +6,9 @@ */ use crate::class::{ - into_signature_info, make_constant_registration, make_func_collection, - make_method_registration, make_signal_registrations, ConstDefinition, FuncDefinition, RpcAttr, - RpcMode, SignalDefinition, SignatureInfo, TransferMode, + into_signature_info, make_constant_registration, make_method_registration, + make_signal_registrations, ConstDefinition, FuncDefinition, RpcAttr, RpcMode, SignalDefinition, + SignatureInfo, TransferMode, }; use crate::util::{ bail, c_str, format_funcs_collection_struct, ident, make_funcs_collection_constants, @@ -115,13 +115,6 @@ pub fn transform_inherent_impl( #[cfg(not(feature = "codegen-full"))] let rpc_registrations = TokenStream::new(); - // If at least one #[signal] is present, generate both signals() + funcs() and their types. - // Do not generate otherwise, to save on compile time + scope pollution. - // TODO remove this. - let _func_collection_struct = signals_collection_struct - .as_ref() - .map(|_| make_func_collection(&class_name, &funcs)); - let method_registrations: Vec = funcs .into_iter() .map(|func_def| make_method_registration(&class_name, func_def, None)) diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 129a2e435..46fbcaba0 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -117,18 +117,6 @@ pub fn make_signature_tuple_type( } } -/// Returns a type expression `R, Params` (without parentheses) that can be used as two separate generic args. -/// -/// `Params` is a `(P1, P2, P3, ...)` tuple. -pub fn make_signature_generic_args( - ret_type: &TokenStream, - param_types: &[venial::TypeExpr], -) -> TokenStream { - quote::quote! { - #ret_type, (#(#param_types,)*) - } -} - fn is_punct(tt: &TokenTree, c: char) -> bool { match tt { TokenTree::Punct(punct) => punct.as_char() == c, diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index da05db9d2..b01b74d58 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -32,7 +32,6 @@ pub use super::obj::{ }; // Make trait methods available. -pub use super::obj::cap::WithFuncs as _; // funcs() pub use super::obj::WithBaseField as _; pub use super::obj::WithSignals as _; // signals() // base(), base_mut(), to_gd() From 1193b6fafd195393d91b3d3608bfbdfd5fd95026 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 23 Feb 2025 17:05:30 +0100 Subject: [PATCH 11/14] Clean up signal handling code + user-facing symbols Also disallow visibility markers in #[signal]. --- godot-core/src/private.rs | 10 ----- .../src/class/data_models/inherent_impl.rs | 10 ++++- godot-macros/src/class/data_models/signal.rs | 44 +++++++++++-------- godot-macros/src/class/derive_godot_class.rs | 2 + godot-macros/src/util/mod.rs | 6 +-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 3e76c4edc..033b8155e 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -120,16 +120,6 @@ pub(crate) fn call_error_remove(in_error: &sys::GDExtensionCallError) -> Option< call_error } -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Functional and signal APIs - -// pub fn emit_signal(obj: &mut BaseMut, varargs: &[Variant]) -// where -// T: GodotClass + Inherits, -// { -// obj.upcast_mut().emit_signal(varargs); -// } - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Plugin and global state handling diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 0383c8368..2316308a9 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -310,7 +310,7 @@ fn process_godot_fns( 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() { @@ -319,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); @@ -576,7 +582,7 @@ where // 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)] + // Private #[signal(__no_builder)] let no_builder = parser.handle_alone("__no_builder")?; parser.finish()?; diff --git a/godot-macros/src/class/data_models/signal.rs b/godot-macros/src/class/data_models/signal.rs index 4ea66a742..5282c6b07 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -77,7 +77,7 @@ impl<'a> SignalDetails<'a> { let param_tuple = quote! { ( #( #param_types, )* ) }; let signal_name = &original_decl.name; - let individual_struct_name = format_ident!("{}_{}", class_name, signal_name); + let individual_struct_name = format_ident!("__godot_Signal_{}_{}", class_name, signal_name); Ok(Self { original_decl, @@ -200,10 +200,11 @@ impl SignalCollection { } = details; self.collection_methods.push(quote! { + // Deliberately not #[doc(hidden)] for IDE completion. #(#signal_cfg_attrs)* fn #signal_name(self) -> #individual_struct_name<'a> { #individual_struct_name { - typed: ::godot::register::TypedSignal::new(self.object, #signal_name_str) + typed: ::godot::register::TypedSignal::new(self.__internal_obj, #signal_name_str) } } }); @@ -224,23 +225,29 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { class_name, param_names, param_tuple, - // signal_name, signal_cfg_attrs, individual_struct_name, .. } = details; - // let module_name = format_ident!("__godot_signal_{class_name}_{signal_name}"); - - // Module + re-export. - // Could also keep contained in module to reduce namespace pollution, but might make docs a bit more nested. + // 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)* - // mod #module_name { - - // TODO make pub without running into "private type `MySignal` in public interface" errors. #(#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>, @@ -269,27 +276,26 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { &mut self.typed } } - - // #(#signal_cfg_attrs)* - // pub(crate) use #module_name::#individual_struct_name; } } -// See also make_func_collection(). +/// 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!("{}Signals", class_name); + 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. - object: ::godot::register::ObjectRef<'a, #class_name> + #[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> { @@ -301,14 +307,14 @@ fn make_signal_collection(class_name: &Ident, collection: SignalCollection) -> O fn signals(&mut self) -> Self::SignalCollection<'_> { Self::SignalCollection { - object: ::godot::register::ObjectRef::Internal { obj_mut: self } + __internal_obj: ::godot::register::ObjectRef::Internal { obj_mut: self } } } #[doc(hidden)] fn __signals_from_external(external: &Gd) -> Self::SignalCollection<'_> { Self::SignalCollection { - object: ::godot::register::ObjectRef::External { gd: external.clone() } + __internal_obj: ::godot::register::ObjectRef::External { gd: external.clone() } } } } 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/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") } From cb6577f66d6eda07e3778667467df809fb5089b4 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 23 Feb 2025 17:38:11 +0100 Subject: [PATCH 12/14] Module organization --- godot-core/src/meta/mod.rs | 3 + godot-core/src/registry/mod.rs | 4 +- .../{functional => signal}/connect_builder.rs | 14 +-- .../registry/{functional => signal}/mod.rs | 5 +- .../{functional => signal}/typed_signal.rs | 69 +++++++++--- .../{functional => signal}/variadic.rs | 105 +++++++++++------- godot-macros/src/class/data_models/signal.rs | 2 +- godot/src/lib.rs | 2 +- godot/src/prelude.rs | 5 +- .../builtin_tests/containers/signal_test.rs | 11 +- 10 files changed, 135 insertions(+), 85 deletions(-) rename godot-core/src/registry/{functional => signal}/connect_builder.rs (96%) rename godot-core/src/registry/{functional => signal}/mod.rs (78%) rename godot-core/src/registry/{functional => signal}/typed_signal.rs (62%) rename godot-core/src/registry/{functional => signal}/variadic.rs (56%) 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/registry/mod.rs b/godot-core/src/registry/mod.rs index 26d43d198..e1eb16326 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -16,11 +16,11 @@ pub mod plugin; pub mod property; #[cfg(since_api = "4.2")] -pub mod functional; +pub mod signal; // Contents re-exported in `godot` crate; just keep empty. #[cfg(before_api = "4.2")] -pub mod functional {} +pub mod signal {} // 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/functional/connect_builder.rs b/godot-core/src/registry/signal/connect_builder.rs similarity index 96% rename from godot-core/src/registry/functional/connect_builder.rs rename to godot-core/src/registry/signal/connect_builder.rs index 986a40bcd..715ee890a 100644 --- a/godot-core/src/registry/functional/connect_builder.rs +++ b/godot-core/src/registry/signal/connect_builder.rs @@ -8,7 +8,7 @@ use crate::builtin::{Callable, GString, Variant}; use crate::classes::object::ConnectFlags; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; -use crate::registry::functional::{AsFunc, ParamTuple, TypedSignal}; +use crate::registry::signal::{SignalReceiver, TypedSignal}; use crate::{meta, sys}; /// Type-state builder for customizing signal connections. @@ -63,7 +63,7 @@ pub struct ConnectBuilder<'ts, 'c, CSig: GodotClass, CRcv, Ps, GodotFn> { godot_fn: GodotFn, } -impl<'ts, 'c, CSig: WithBaseField, Ps: ParamTuple> ConnectBuilder<'ts, 'c, CSig, (), Ps, ()> { +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, @@ -86,7 +86,7 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: ParamTuple> ConnectBuilder<'ts, 'c, CSig, /* GodotFn= */ impl FnMut(&[&Variant]) -> Result + 'static, > where - F: AsFunc<(), Ps>, + F: SignalReceiver<(), Ps>, { let godot_fn = move |variant_args: &[&Variant]| -> Result { let args = Ps::from_variant_array(variant_args); @@ -132,7 +132,7 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: ParamTuple> ConnectBuilder<'ts, 'c, CSig, } } -impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: ParamTuple> +impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, CSig, Gd, Ps, ()> { /// **Stage 2:** method taking `&mut self`. @@ -150,7 +150,7 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: ParamTuple> > where CRcv: GodotClass + Bounds, - for<'c_rcv> F: AsFunc<&'c_rcv mut CRcv, Ps>, + for<'c_rcv> F: SignalReceiver<&'c_rcv mut CRcv, Ps>, { let mut gd: Gd = self.receiver_obj; @@ -189,7 +189,7 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: ParamTuple> > where CRcv: GodotClass + Bounds, - for<'c_rcv> F: AsFunc<&'c_rcv CRcv, Ps>, + for<'c_rcv> F: SignalReceiver<&'c_rcv CRcv, Ps>, { let gd: Gd = self.receiver_obj; @@ -218,7 +218,7 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: ParamTuple> impl<'ts, 'c, CSig, CRcv, Ps, GodotFn> ConnectBuilder<'ts, 'c, CSig, CRcv, Ps, GodotFn> where CSig: WithBaseField, - Ps: ParamTuple, + Ps: meta::ParamTuple, GodotFn: FnMut(&[&Variant]) -> Result + 'static, { /// **Stage 3:** allow signal to be called across threads. diff --git a/godot-core/src/registry/functional/mod.rs b/godot-core/src/registry/signal/mod.rs similarity index 78% rename from godot-core/src/registry/functional/mod.rs rename to godot-core/src/registry/signal/mod.rs index f05a5b0d4..4a72a49cf 100644 --- a/godot-core/src/registry/functional/mod.rs +++ b/godot-core/src/registry/signal/mod.rs @@ -7,8 +7,9 @@ mod connect_builder; mod typed_signal; -mod variadic; +pub(crate) mod variadic; pub use connect_builder::*; pub use typed_signal::*; -pub use variadic::*; +pub use variadic::SignalReceiver; +// ParamTuple re-exported in crate::meta. diff --git a/godot-core/src/registry/functional/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs similarity index 62% rename from godot-core/src/registry/functional/typed_signal.rs rename to godot-core/src/registry/signal/typed_signal.rs index bea292d82..f57242c57 100644 --- a/godot-core/src/registry/functional/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -5,19 +5,18 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -// Maybe move this to builtin::functional module? - use crate::builtin::{Callable, GString, Variant}; use crate::classes::object::ConnectFlags; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; -use crate::registry::functional::{AsFunc, ConnectBuilder, ParamTuple}; +use crate::registry::signal::{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 `&self` from within the `impl` block, goes through `base()` re-borrowing and thus allows re-entrant calls + /// 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 }, @@ -46,15 +45,38 @@ where // ---------------------------------------------------------------------------------------------------------------------------------------------- +/// 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> { - //signal: Signal, /// 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: ParamTuple> TypedSignal<'c, C, Ps> { +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 { @@ -68,11 +90,15 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { self.owner.to_owned() } - pub fn emit(&mut self, params: Ps) { + /// 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, ¶ms.to_variant_array()); + obj.emit_signal(name, &args.to_variant_array()); }); } @@ -85,10 +111,11 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { /// sig.connect(|arg| { /* closure */ }); /// ``` /// - /// To connect to a method of the own object `self`, use [`connect_self()`][Self::connect_self]. + /// 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: AsFunc<(), Ps>, + F: SignalReceiver<(), Ps>, { let callable_name = std::any::type_name_of_val(&function); @@ -103,9 +130,12 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { } /// 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: AsFunc<&'c_rcv mut C, Ps>, + for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, { // 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. @@ -132,11 +162,12 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. /// - /// To connect to methods on the same object, use [`connect_self()`][Self::connect_self]. + /// 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: AsFunc<&'c_rcv mut OtherC, Ps>, + for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps>, { let callable_name = std::any::type_name_of_val(&function); @@ -154,6 +185,14 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { self.inner_connect_local(callable_name, 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) + } + fn inner_connect_local(&mut self, callable_name: impl meta::AsArg, godot_fn: F) where F: FnMut(&[&Variant]) -> Result + 'static, @@ -179,8 +218,4 @@ impl<'c, C: WithBaseField, Ps: ParamTuple> TypedSignal<'c, C, Ps> { c.done(); }); } - - pub fn connect_builder(&mut self) -> ConnectBuilder<'_, 'c, C, (), Ps, ()> { - ConnectBuilder::new(self) - } } diff --git a/godot-core/src/registry/functional/variadic.rs b/godot-core/src/registry/signal/variadic.rs similarity index 56% rename from godot-core/src/registry/functional/variadic.rs rename to godot-core/src/registry/signal/variadic.rs index b7475206b..2aff2249a 100644 --- a/godot-core/src/registry/functional/variadic.rs +++ b/godot-core/src/registry/signal/variadic.rs @@ -14,14 +14,65 @@ use crate::builtin::Variant; use crate::meta; -pub trait AsFunc: 'static { +/// 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 { + 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),* + { + 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 AsFunc<(), ( $($Ps,)* )> for F + impl SignalReceiver<(), ( $($Ps,)* )> for F where F: FnMut( $($Ps,)* ) -> R + 'static { fn call(&mut self, _no_instance: (), ($($args,)*): ( $($Ps,)* )) { @@ -30,7 +81,7 @@ macro_rules! impl_signal_recipient { } // Methods with mutable receiver - &mut self. - impl AsFunc<&mut C, ( $($Ps,)* )> for F + impl SignalReceiver<&mut C, ( $($Ps,)* )> for F where F: FnMut( &mut C, $($Ps,)* ) -> R + 'static { fn call(&mut self, instance: &mut C, ($($args,)*): ( $($Ps,)* )) { @@ -39,7 +90,7 @@ macro_rules! impl_signal_recipient { } // Methods with immutable receiver - &self. - impl AsFunc<&C, ( $($Ps,)* )> for F + impl SignalReceiver<&C, ( $($Ps,)* )> for F where F: FnMut( &C, $($Ps,)* ) -> R + 'static { fn call(&mut self, instance: &C, ($($args,)*): ( $($Ps,)* )) { @@ -53,42 +104,10 @@ impl_signal_recipient!(); impl_signal_recipient!(arg0: P0); impl_signal_recipient!(arg0: P0, arg1: P1); impl_signal_recipient!(arg0: P0, arg1: P1, arg2: P2); - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -pub trait ParamTuple { - fn to_variant_array(&self) -> Vec; - fn from_variant_array(array: &[&Variant]) -> Self; -} - -macro_rules! impl_param_tuple { - ($($args:ident : $Ps:ident),*) => { - impl<$($Ps),*> ParamTuple for ($($Ps,)*) - where - $($Ps: meta::ToGodot + meta::FromGodot),* - { - 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())) - ), - )* ) - } - } - }; -} - -impl_param_tuple!(); -impl_param_tuple!(arg0: P0); -impl_param_tuple!(arg0: P0, arg1: P1); -impl_param_tuple!(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/signal.rs b/godot-macros/src/class/data_models/signal.rs index 5282c6b07..b2b20e629 100644 --- a/godot-macros/src/class/data_models/signal.rs +++ b/godot-macros/src/class/data_models/signal.rs @@ -257,7 +257,7 @@ fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { #(#signal_cfg_attrs)* impl #individual_struct_name<'_> { pub fn emit(&mut self, #emit_params) { - self.typed.emit((#( #param_names, )*)); + self.typed.emit_tuple((#( #param_names, )*)); } } diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 1715e9b4d..c8409382d 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -178,8 +178,8 @@ pub mod init { /// Register/export Rust symbols to Godot: classes, methods, enums... pub mod register { - pub use godot_core::registry::functional::*; 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 b01b74d58..0f3bb81e1 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -32,10 +32,9 @@ pub use super::obj::{ }; // Make trait methods available. -pub use super::obj::WithBaseField as _; -pub use super::obj::WithSignals as _; // signals() // base(), base_mut(), to_gd() - pub use super::obj::EngineBitfield as _; 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 fe4bce1e8..441372f25 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -53,9 +53,6 @@ fn signal_symbols_internal() { internal.connect_signals_internal(tracker.clone()); drop(internal); - // let check = Signal::from_object_signal(&emitter, "emitter_1"); - // dbg!(check.connections()); - emitter.bind_mut().emit_signals_internal(); // Check that closure is invoked. @@ -97,8 +94,8 @@ fn signal_symbols_external() { let receiver = Receiver::new_alloc(); sig.connect_obj(&receiver, Receiver::receiver_1_mut); - // Emit signal. - sig.emit(987); + // Emit signal (now via tuple). + sig.emit_tuple((987,)); // Check that closure is invoked. assert_eq!(tracker.get(), 987, "Emit failed (closure)"); @@ -317,10 +314,6 @@ impl Receiver { self.used[2].set(true); } - - // This should probably have a dedicated key such as #[godot_api(func_refs)] or so... - #[signal] - fn _just_here_to_generate_funcs(); } const SIGNAL_ARG_STRING: &str = "Signal string arg"; From cf98df3bd78da7866c47e57bbf345c5c97f007b3 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 23 Feb 2025 23:28:13 +0100 Subject: [PATCH 13/14] Reduce code duplication in connect overloads --- .../src/registry/signal/connect_builder.rs | 69 +++++++++------- .../src/registry/signal/typed_signal.rs | 81 ++++++++----------- godot-core/src/registry/signal/variadic.rs | 4 +- 3 files changed, 75 insertions(+), 79 deletions(-) diff --git a/godot-core/src/registry/signal/connect_builder.rs b/godot-core/src/registry/signal/connect_builder.rs index 715ee890a..f27477c60 100644 --- a/godot-core/src/registry/signal/connect_builder.rs +++ b/godot-core/src/registry/signal/connect_builder.rs @@ -7,9 +7,9 @@ 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}; -use crate::{meta, sys}; /// Type-state builder for customizing signal connections. /// @@ -88,19 +88,13 @@ impl<'ts, 'c, CSig: WithBaseField, Ps: meta::ParamTuple> ConnectBuilder<'ts, 'c, where F: SignalReceiver<(), Ps>, { - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); + let godot_fn = make_godot_fn(move |args| { function.call((), args); - - Ok(Variant::nil()) - }; - - let mut data = self.data; - data.callable_name = Some(sys::short_type_name::().into()); + }); ConnectBuilder { parent_sig: self.parent_sig, - data, + data: self.data.with_callable_name::(), godot_fn, receiver_obj: (), } @@ -153,22 +147,15 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> for<'c_rcv> F: SignalReceiver<&'c_rcv mut CRcv, Ps>, { let mut gd: Gd = self.receiver_obj; - - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); + 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); - - Ok(Variant::nil()) - }; - - let mut data = self.data; - data.callable_name = Some(sys::short_type_name::().into()); + }); ConnectBuilder { parent_sig: self.parent_sig, - data, + data: self.data.with_callable_name::(), godot_fn, receiver_obj: (), } @@ -192,22 +179,15 @@ impl<'ts, 'c, CSig: WithBaseField, CRcv: GodotClass, Ps: meta::ParamTuple> for<'c_rcv> F: SignalReceiver<&'c_rcv CRcv, Ps>, { let gd: Gd = self.receiver_obj; - - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); + let godot_fn = make_godot_fn(move |args| { let guard = gd.bind(); let instance = &*guard; method_with_shared_self.call(instance, args); - - Ok(Variant::nil()) - }; - - let mut data = self.data; - data.callable_name = Some(sys::short_type_name::().into()); + }); ConnectBuilder { parent_sig: self.parent_sig, - data, + data: self.data.with_callable_name::(), godot_fn, receiver_obj: (), } @@ -315,7 +295,7 @@ where #[cfg(not(feature = "experimental-threads"))] let callable = Callable::from_local_fn(callable_name, godot_fn); - parent_sig.connect_untyped(&callable, data.connect_flags); + parent_sig.inner_connect_untyped(&callable, data.connect_flags); } } @@ -336,9 +316,36 @@ struct BuilderData { } 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/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs index f57242c57..7f7901b13 100644 --- a/godot-core/src/registry/signal/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -5,10 +5,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::builtin::{Callable, GString, Variant}; +use crate::builtin::{Callable, Variant}; use crate::classes::object::ConnectFlags; use crate::obj::{bounds, Bounds, Gd, GodotClass, WithBaseField}; -use crate::registry::signal::{ConnectBuilder, SignalReceiver}; +use crate::registry::signal::{make_callable_name, make_godot_fn, ConnectBuilder, SignalReceiver}; use crate::{classes, meta}; use std::borrow::Cow; use std::marker::PhantomData; @@ -117,16 +117,11 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { where F: SignalReceiver<(), Ps>, { - let callable_name = std::any::type_name_of_val(&function); - - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); + let godot_fn = make_godot_fn(move |args| { function.call((), args); + }); - Ok(Variant::nil()) - }; - - self.inner_connect_local(callable_name, godot_fn); + self.inner_connect_godot_fn::(godot_fn); } /// Connect a method (member function) with `&mut self` as the first parameter. @@ -137,27 +132,14 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { where for<'c_rcv> F: SignalReceiver<&'c_rcv mut C, Ps>, { - // 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. - let callable_name = std::any::type_name_of_val(&function); - - let object = self.owner.to_owned(); - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); - - // let mut function = function; - // function.call(instance, args); - let mut object = object.clone(); - - // TODO: how to avoid another bind, when emitting directly from Rust? - let mut instance = object.bind_mut(); + 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); + }); - Ok(Variant::nil()) - }; - - self.inner_connect_local(callable_name, godot_fn); + self.inner_connect_godot_fn::(godot_fn); } /// Connect a method (member function) with any `Gd` (not `self`) as the first parameter. @@ -169,20 +151,14 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { OtherC: GodotClass + Bounds, for<'c_rcv> F: SignalReceiver<&'c_rcv mut OtherC, Ps>, { - let callable_name = std::any::type_name_of_val(&function); - - let mut object = object.clone(); - let godot_fn = move |variant_args: &[&Variant]| -> Result { - let args = Ps::from_variant_array(variant_args); - - let mut instance = object.bind_mut(); + 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); + }); - Ok(Variant::nil()) - }; - - self.inner_connect_local(callable_name, godot_fn); + self.inner_connect_godot_fn::(godot_fn); } /// Fully customizable connection setup. @@ -193,19 +169,32 @@ impl<'c, C: WithBaseField, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> { ConnectBuilder::new(self) } - fn inner_connect_local(&mut self, callable_name: impl meta::AsArg, godot_fn: F) - where - F: FnMut(&[&Variant]) -> Result + 'static, - { - let signal_name = self.name.as_ref(); - let callable = Callable::from_local_fn(callable_name, godot_fn); + /// 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); }); } - pub(super) fn connect_untyped(&mut self, callable: &Callable, flags: Option) { + /// 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(); diff --git a/godot-core/src/registry/signal/variadic.rs b/godot-core/src/registry/signal/variadic.rs index 2aff2249a..94912011c 100644 --- a/godot-core/src/registry/signal/variadic.rs +++ b/godot-core/src/registry/signal/variadic.rs @@ -32,7 +32,7 @@ pub trait SignalReceiver: 'static { /// /// 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 { +pub trait ParamTuple: 'static { fn to_variant_array(&self) -> Vec; fn from_variant_array(array: &[&Variant]) -> Self; } @@ -47,7 +47,7 @@ macro_rules! impl_signal_recipient { impl<$($Ps),*> ParamTuple for ($($Ps,)*) where - $($Ps: meta::ToGodot + meta::FromGodot),* + $($Ps: meta::ToGodot + meta::FromGodot + 'static),* { fn to_variant_array(&self) -> Vec { let ($($args,)*) = self; From 4a25b76507b30ee0277c74327363e6853a5782de Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 24 Feb 2025 00:28:20 +0100 Subject: [PATCH 14/14] Simplify signal tests; fix swallowed panic in #[func] --- .../builtin_tests/containers/signal_test.rs | 129 +++++++++++------- 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 441372f25..6aa404948 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -9,7 +9,7 @@ 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, WithSignals}; +use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd, WithSignals}; use godot::register::{godot_api, GodotClass}; use godot::sys; use godot::sys::Global; @@ -21,21 +21,21 @@ 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); - for (i, arg) in args.iter().enumerate() { - let signal_name = format!("emitter_{i}"); - let receiver_name = format!("receiver_{i}"); + 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)); - emitter.connect(&signal_name, &receiver.callable(&receiver_name)); - emitter.emit_signal(&signal_name, arg); - - assert!(receiver.bind().used[i].get()); - } + 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(); @@ -59,7 +59,11 @@ fn signal_symbols_internal() { assert_eq!(tracker.get(), 1234, "Emit failed (closure)"); // Check that instance method is invoked. - assert_eq!(emitter.bind().last_received, 1234, "Emit failed (method)"); + assert_eq!( + emitter.bind().last_received_int, + 1234, + "Emit failed (method)" + ); // Check that static function is invoked. assert_eq!( @@ -76,7 +80,7 @@ fn signal_symbols_internal() { #[itest] fn signal_symbols_external() { let emitter = Emitter::new_alloc(); - let mut sig = emitter.signals().emitter_1(); + let mut sig = emitter.signals().signal_int(); // Local function; deliberately use a !Send type. let tracker = Rc::new(Cell::new(0)); @@ -92,7 +96,7 @@ fn signal_symbols_external() { // Connect to other object. let receiver = Receiver::new_alloc(); - sig.connect_obj(&receiver, Receiver::receiver_1_mut); + sig.connect_obj(&receiver, Receiver::receive_int_mut); // Emit signal (now via tuple). sig.emit_tuple((987,)); @@ -101,11 +105,16 @@ fn signal_symbols_external() { assert_eq!(tracker.get(), 987, "Emit failed (closure)"); // Check that instance method is invoked. - assert_eq!(emitter.bind().last_received, 987, "Emit failed (method)"); + assert_eq!( + emitter.bind().last_received_int, + 987, + "Emit failed (method)" + ); // Check that *other* instance method is invoked. - assert!( - receiver.bind().used[1].get(), + assert_eq!( + receiver.bind().last_received(), + LastReceived::IntMut(987), "Emit failed (other object method)" ); @@ -118,7 +127,7 @@ fn signal_symbols_external() { #[itest] fn signal_symbols_external_builder() { let emitter = Emitter::new_alloc(); - let mut sig = emitter.signals().emitter_1(); + let mut sig = emitter.signals().signal_int(); // Self-modifying method. sig.connect_builder() @@ -130,14 +139,14 @@ fn signal_symbols_external_builder() { let receiver_mut = Receiver::new_alloc(); sig.connect_builder() .object(&receiver_mut) - .method_mut(Receiver::receiver_1_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::receiver_1) + .method_immut(Receiver::receive_int) .done(); let tracker = Rc::new(Cell::new(0)); @@ -156,20 +165,22 @@ fn signal_symbols_external_builder() { // Check that self instance method (mut) is invoked. assert_eq!( - emitter.bind().last_received, + emitter.bind().last_received_int, 552, "Emit failed (mut method)" ); // Check that *other* instance method is invoked. - assert!( - receiver_immut.bind().used[1].get(), + assert_eq!( + receiver_immut.bind().last_received(), + LastReceived::Int(552), "Emit failed (other object, immut method)" ); // Check that *other* instance method is invoked. - assert!( - receiver_mut.bind().used[1].get(), + assert_eq!( + receiver_mut.bind().last_received(), + LastReceived::IntMut(552), "Emit failed (other object, mut method)" ); @@ -187,7 +198,7 @@ fn signal_symbols_sync() { use std::sync::{Arc, Mutex}; let emitter = Emitter::new_alloc(); - let mut sig = emitter.signals().emitter_1(); + let mut sig = emitter.signals().signal_int(); let sync_tracker = Arc::new(Mutex::new(0)); { @@ -239,25 +250,25 @@ static LAST_STATIC_FUNCTION_ARG: Global = Global::default(); struct Emitter { _base: Base, #[cfg(since_api = "4.2")] - last_received: i64, + 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 = arg1; + self.last_received_int = arg1; } } @@ -270,7 +281,7 @@ impl Emitter { #[cfg(since_api = "4.2")] fn connect_signals_internal(&mut self, tracker: Rc>) { - let mut sig = self.signals().emitter_1(); + 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)); @@ -278,46 +289,58 @@ impl Emitter { #[cfg(since_api = "4.2")] fn emit_signals_internal(&mut self) { - self.signals().emitter_1().emit(1234); + 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 { + 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_0(&self) { - self.used[0].set(true); + fn receive_unit(&self) { + self.last_received.set(LastReceived::Unit); } #[func] - fn receiver_1(&self, arg1: i64) { - self.used[1].set(true); - assert_eq!(arg1, 987); + fn receive_int(&self, arg1: i64) { + self.last_received.set(LastReceived::Int(arg1)); } - fn receiver_1_mut(&mut self, arg1: i64) { - self.used[1].set(true); - assert_eq!(arg1, 987); + fn receive_int_mut(&mut self, arg1: i64) { + self.last_received.set(LastReceived::IntMut(arg1)); } #[func] - fn receiver_2(&self, arg1: Gd, arg2: GString) { - assert_eq!(self.base().clone(), arg1); - assert_eq!(SIGNAL_ARG_STRING, arg2.to_string()); - - self.used[2].set(true); + fn receive_obj(&self, obj: Gd) { + self.last_received + .set(LastReceived::Object(obj.instance_id())); } } -const SIGNAL_ARG_STRING: &str = "Signal string arg"; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // 4.2+ custom callables