diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 656e6f352..80bc5056d 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -38,9 +38,9 @@ pub use crate::{array, dict, real, reals, varray}; // Re-export generated enums. pub use crate::gen::central::global_reexported_enums::{Corner, EulerOrder, Side, VariantOperator}; -pub use crate::sys::VariantType; // Not yet public. pub(crate) use crate::gen::central::VariantDispatch; +pub use crate::sys::VariantType; #[doc(hidden)] pub mod __prelude_reexport { diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index a6f72a1ea..ba487b03d 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -47,7 +47,7 @@ pub(crate) fn display_string( obj: &Gd, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - let string: GString = obj.raw.as_object().to_string(); + let string: GString = obj.raw.as_object_ref().to_string(); ::fmt(&string, f) } diff --git a/godot-core/src/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 1801d5620..2ce3bce22 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -207,11 +207,16 @@ impl RawGd { // self.as_target_mut() // } - pub(crate) fn as_object(&self) -> &classes::Object { + pub(crate) fn as_object_ref(&self) -> &classes::Object { // SAFETY: Object is always a valid upcast target. unsafe { self.as_upcast_ref() } } + pub(crate) fn as_object_mut(&mut self) -> &mut classes::Object { + // SAFETY: Object is always a valid upcast target. + unsafe { self.as_upcast_mut() } + } + /// # Panics /// If this `RawGd` is null. In Debug mode, sanity checks (valid upcast, ID comparisons) can also lead to panics. /// diff --git a/godot-core/src/obj/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..157d274db 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -25,7 +25,6 @@ use std::sync::atomic; #[cfg(debug_assertions)] use std::sync::{Arc, Mutex}; use sys::Global; - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Global variables @@ -120,6 +119,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-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/functional/typed_signal.rs b/godot-core/src/registry/functional/typed_signal.rs new file mode 100644 index 000000000..af6d5c983 --- /dev/null +++ b/godot-core/src/registry/functional/typed_signal.rs @@ -0,0 +1,215 @@ +/* + * 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, Variant}; +use crate::obj::{Gd, GodotClass, WithBaseField}; +use crate::registry::functional::{AsFunc, ParamTuple}; +use crate::{classes, sys}; +use std::borrow::Cow; +use std::fmt; + +#[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 + C: WithBaseField, +{ + fn with_object_mut(&mut self, f: impl FnOnce(&mut classes::Object)) { + match self { + ObjectRef::Internal { obj_mut } => f(obj_mut.base_mut().upcast_object_mut()), + ObjectRef::External { gd } => f(gd.upcast_object_mut()), + } + } + + fn to_owned(&self) -> Gd { + match self { + ObjectRef::Internal { obj_mut } => WithBaseField::to_gd(*obj_mut), + ObjectRef::External { gd } => gd.clone(), + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +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<'a, C: WithBaseField, Ps: ParamTuple> TypedSignal<'a, C, Ps> { + #[doc(hidden)] + pub fn new(owner: ObjectRef<'a, C>, name: &'static str) -> Self { + Self { + owner, + name: Cow::Borrowed(name), + _signature: std::marker::PhantomData, + } + } + + 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()); + }); + } + + /// 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); + }); + } + + /// 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); + }); + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// 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/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 9ce2a155d..26d43d198 100644 --- a/godot-core/src/registry/mod.rs +++ b/godot-core/src/registry/mod.rs @@ -15,6 +15,13 @@ pub mod method; pub mod plugin; pub mod property; +#[cfg(since_api = "4.2")] +pub mod 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")] mod rpc_config; diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 4c1beb7ac..0e9e368dd 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::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 @@ -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..9bee17bb4 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,295 @@ pub struct SignalDefinition { /// The signal's non-gdext attributes (all except #[signal]). pub external_attributes: Vec, + + /// Whether there is going to be a type-safe builder for this signal (true by default). + pub has_builder: bool, +} + +/// Extracted syntax info for a declared signal. +struct SignalDetails<'a> { + /// `fn my_signal(i: i32, s: GString)` + original_decl: &'a venial::Function, + /// `MyClass` + class_name: &'a Ident, + /// `i32`, `GString` + param_types: Vec, + /// `i`, `s` + param_names: Vec, + /// `"i"`, `"s"` + param_names_str: Vec, + /// `(i32, GString)` + param_tuple: TokenStream, + /// `MySignal` + signal_name: &'a Ident, + /// `"MySignal"` + signal_name_str: String, + /// `#[cfg(..)] #[cfg(..)]` + signal_cfg_attrs: Vec<&'a venial::Attribute>, + /// `MyClass_MySignal` + individual_struct_name: Ident, +} + +impl<'a> SignalDetails<'a> { + pub fn extract( + original_decl: &'a venial::Function, + class_name: &'a Ident, + external_attributes: &'a [venial::Attribute], + ) -> ParseResult> { + let mut param_types = vec![]; + let mut param_names = vec![]; + let mut param_names_str = vec![]; + + for (param, _punct) in original_decl.params.inner.iter() { + match param { + venial::FnParam::Typed(param) => { + param_types.push(param.ty.clone()); + param_names.push(param.name.clone()); + param_names_str.push(param.name.to_string()); + } + venial::FnParam::Receiver(receiver) => { + return bail!(receiver, "#[signal] cannot have receiver (self) parameter"); + } + }; + } + + // Transport #[cfg] attributes to the FFI glue, to ensure signals which were conditionally + // removed from compilation don't cause errors. + let signal_cfg_attrs = util::extract_cfg_attrs(external_attributes) + .into_iter() + .collect(); + + let param_tuple = quote! { ( #( #param_types, )* ) }; + let signal_name = &original_decl.name; + let individual_struct_name = format_ident!("{}_{}", class_name, signal_name); + + Ok(Self { + original_decl, + class_name, + param_types, + param_names, + param_names_str, + param_tuple, + signal_name, + signal_name_str: original_decl.name.to_string(), + signal_cfg_attrs, + individual_struct_name, + }) + } } pub fn make_signal_registrations( - signals: Vec, + signals: &[SignalDefinition], + class_name: &Ident, class_name_obj: &TokenStream, -) -> Vec { +) -> ParseResult<(Vec, Option)> { let mut signal_registrations = Vec::new(); + let mut collection_api = SignalCollection::default(); - for signal in signals.iter() { + for signal in signals { let SignalDefinition { signature, external_attributes, + has_builder, } = signal; - let mut param_types: Vec = Vec::new(); - let mut param_names: Vec = Vec::new(); - for param in signature.params.inner.iter() { - match ¶m.0 { - venial::FnParam::Typed(param) => { - param_types.push(param.ty.clone()); - param_names.push(param.name.to_string()); - } - venial::FnParam::Receiver(_) => {} - }; + let details = SignalDetails::extract(signature, class_name, external_attributes)?; + + // Callable custom functions are only supported in 4.2+, upon which custom signals rely. + #[cfg(since_api = "4.2")] + if *has_builder { + collection_api.extend_with(&details); } - let signature_tuple = util::make_signature_tuple_type("e! { () }, ¶m_types); - let indexes = 0..param_types.len(); - let param_array_decl = quote! { - [ - // Don't use raw sys pointers directly; it's very easy to have objects going out of scope. - #( - <#signature_tuple as godot::meta::VarcallSignatureTuple> - ::param_property_info(#indexes, #param_names), - )* - ] - }; + let registration = make_signal_registration(&details, class_name_obj); + signal_registrations.push(registration); + } - // Transport #[cfg] attributes to the FFI glue, to ensure signals which were conditionally - // removed from compilation don't cause errors. - let signal_cfg_attrs: Vec<&venial::Attribute> = - util::extract_cfg_attrs(external_attributes) - .into_iter() - .collect(); - let signal_name_str = signature.name.to_string(); - let signal_parameters_count = param_names.len(); - let signal_parameters = param_array_decl; - - let signal_registration = quote! { + let struct_code = make_signal_collection(class_name, collection_api); + + Ok((signal_registrations, struct_code)) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +fn make_signal_registration(details: &SignalDetails, class_name_obj: &TokenStream) -> TokenStream { + let SignalDetails { + param_types, + param_names, + param_names_str, + signal_name_str, + signal_cfg_attrs, + .. + } = details; + + let signature_tuple = util::make_signature_tuple_type("e! { () }, param_types); + + let indexes = 0..param_types.len(); + let param_property_infos = quote! { + [ + // Don't use raw sys pointers directly; it's very easy to have objects going out of scope. + #( + <#signature_tuple as godot::meta::VarcallSignatureTuple> + ::param_property_info(#indexes, #param_names_str), + )* + ] + }; + + let signal_parameters_count = param_names.len(); + + quote! { + #(#signal_cfg_attrs)* + unsafe { + use ::godot::sys; + let parameters_info: [::godot::meta::PropertyInfo; #signal_parameters_count] = #param_property_infos; + + let mut parameters_info_sys: [sys::GDExtensionPropertyInfo; #signal_parameters_count] = + std::array::from_fn(|i| parameters_info[i].property_sys()); + + let signal_name = ::godot::builtin::StringName::from(#signal_name_str); + + sys::interface_fn!(classdb_register_extension_class_signal)( + sys::get_library(), + #class_name_obj.string_sys(), + signal_name.string_sys(), + parameters_info_sys.as_ptr(), + sys::GDExtensionInt::from(#signal_parameters_count as i64), + ); + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// A collection struct accessible via `.signals()` in the generated impl. +/// +/// Also defines individual signal types. +#[derive(Default)] +struct SignalCollection { + /// The individual `my_signal()` accessors, returning concrete signal types. + collection_methods: Vec, + + /// The actual signal definitions, including both `struct` and `impl` blocks. + individual_structs: Vec, +} + +impl SignalCollection { + fn extend_with(&mut self, details: &SignalDetails) { + let SignalDetails { + signal_name, + signal_name_str, + signal_cfg_attrs, + individual_struct_name, + .. + } = details; + + self.collection_methods.push(quote! { #(#signal_cfg_attrs)* - unsafe { - use ::godot::sys; - let parameters_info: [::godot::meta::PropertyInfo; #signal_parameters_count] = #signal_parameters; - - let mut parameters_info_sys: [sys::GDExtensionPropertyInfo; #signal_parameters_count] = - std::array::from_fn(|i| parameters_info[i].property_sys()); - - let signal_name = ::godot::builtin::StringName::from(#signal_name_str); - - sys::interface_fn!(classdb_register_extension_class_signal)( - sys::get_library(), - #class_name_obj.string_sys(), - signal_name.string_sys(), - parameters_info_sys.as_ptr(), - sys::GDExtensionInt::from(#signal_parameters_count as i64), - ); + fn #signal_name(self) -> #individual_struct_name<'a> { + #individual_struct_name { + typed: ::godot::register::TypedSignal::new(self.object, #signal_name_str) + } } - }; + }); - signal_registrations.push(signal_registration); + self.individual_structs + .push(make_signal_individual_struct(details)) } - signal_registrations + + pub fn is_empty(&self) -> bool { + self.individual_structs.is_empty() + } +} + +fn make_signal_individual_struct(details: &SignalDetails) -> TokenStream { + let emit_params = &details.original_decl.params; + + let SignalDetails { + class_name, + param_names, + param_tuple, + // signal_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)] + 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) { + self.typed.emit((#( #param_names, )*)); + } + } + + #(#signal_cfg_attrs)* + impl<'a> std::ops::Deref for #individual_struct_name<'a> { + type Target = ::godot::register::TypedSignal<'a, #class_name, #param_tuple>; + + fn deref(&self) -> &Self::Target { + &self.typed + } + } + + #(#signal_cfg_attrs)* + impl std::ops::DerefMut for #individual_struct_name<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.typed + } + } + + // #(#signal_cfg_attrs)* + // pub(crate) use #module_name::#individual_struct_name; + } +} + +// See also make_func_collection(). +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_methods = &collection.collection_methods; + let individual_structs = collection.individual_structs; + + let code = quote! { + #[allow(non_camel_case_types)] + 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> + } + + impl<'a> #collection_struct_name<'a> { + #( #collection_struct_methods )* + } + + impl ::godot::obj::cap::WithSignals for #class_name { + type SignalCollection<'a> = #collection_struct_name<'a>; + + fn signals(&mut self) -> Self::SignalCollection<'_> { + Self::SignalCollection { + object: ::godot::register::ObjectRef::Internal { obj_mut: self } + } + } + } + + #( #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 133936c70..f31ba16e5 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..5f8902571 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -183,6 +183,7 @@ 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}; 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..5e33b973a 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::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,34 @@ fn signal_basic_connect_emit() { emitter.free(); } +#[cfg(since_api = "4.2")] +#[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(); + drop(internal); + + // let check = Signal::from_object_signal(&emitter, "emitter_1"); + // dbg!(check.connections()); + + 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)"); + + // 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(); +} + #[itest] fn signal_construction_and_id() { let mut object = RefCounted::new_gd(); @@ -63,9 +92,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 +112,30 @@ impl Emitter { #[signal] fn emitter_2(arg1: Gd, arg2: GString); + + #[func] + fn self_receive(&mut self, arg1: i64) { + *LAST_METHOD_ARG.lock() = Some(arg1); + } + + #[func] + fn self_receive_static(arg1: i64) { + *LAST_STATIC_FUNCTION_ARG.lock() = Some(arg1); + } + + // "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); + } } #[derive(GodotClass)] @@ -106,6 +165,10 @@ 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"; 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]