diff --git a/derive/src/attribute_ops.rs b/derive/src/attribute_ops.rs index 3b089a1..5b7b2a5 100644 --- a/derive/src/attribute_ops.rs +++ b/derive/src/attribute_ops.rs @@ -33,6 +33,9 @@ pub struct FieldExportOps { custom_type: Option>, } +#[derive(FromMeta, Debug)] +pub struct FieldSignalOps(pub WithOriginal, Meta>); + impl FieldExportOps { pub fn hint(&self, ty: &Type) -> Result<(TokenStream, TokenStream), TokenStream> { let godot_types = godot_types(); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 654e6e8..45fef51 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -10,14 +10,14 @@ mod impl_attribute; mod type_paths; use attribute_ops::{FieldOpts, GodotScriptOpts}; -use darling::{util::SpannedValue, FromAttributes, FromDeriveInput}; +use darling::{util::SpannedValue, FromAttributes, FromDeriveInput, FromMeta}; use itertools::Itertools; use proc_macro2::TokenStream; use quote::{quote, quote_spanned, ToTokens}; use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Ident, Type}; use type_paths::{godot_types, property_hints, string_name_ty, variant_ty}; -use crate::attribute_ops::{FieldExportOps, PropertyOpts}; +use crate::attribute_ops::{FieldExportOps, FieldSignalOps, PropertyOpts}; #[proc_macro_derive(GodotScript, attributes(export, script, prop, signal))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -47,7 +47,7 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { export_field_state, ): ( TokenStream, - TokenStream, + (TokenStream, TokenStream), TokenStream, TokenStream, TokenStream, @@ -92,12 +92,12 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { (is_public && !is_signal).then(|| derive_property_state_export(field)); let signal_metadata = match (is_public, is_signal) { - (false, false) | (true, false) => TokenStream::default(), + (false, false) | (true, false) => (TokenStream::default(), TokenStream::default()), (true, true) => derive_signal_metadata(field), (false, true) => { let err = compile_error("Signals must be public!", signal_attr); - quote! {#err,} + (quote! {#err,}, TokenStream::default()) } }; @@ -133,6 +133,8 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { acc }); + let (signal_metadata, signal_const_assert) = signal_metadata; + let output = quote! { impl ::godot_rust_script::GodotScript for #script_type_ident { type Base = #base_class; @@ -156,6 +158,8 @@ pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #default_impl } + #signal_const_assert + ::godot_rust_script::register_script_class!( #script_type_ident, #base_class, @@ -433,7 +437,7 @@ fn get_field_description(field: &FieldOpts) -> Option { }) } -fn derive_signal_metadata(field: &SpannedValue) -> TokenStream { +fn derive_signal_metadata(field: &SpannedValue) -> (TokenStream, TokenStream) { let signal_name = field .ident .as_ref() @@ -441,14 +445,59 @@ fn derive_signal_metadata(field: &SpannedValue) -> TokenStream { .unwrap_or_default(); let signal_description = get_field_description(field); let signal_type = &field.ty; + let signal_ops = match field + .attrs + .iter() + .find(|attr| attr.path().is_ident("signal")) + .and_then(|attr| match &attr.meta { + syn::Meta::Path(_) => None, + syn::Meta::List(_) => Some(FieldSignalOps::from_meta(&attr.meta)), + syn::Meta::NameValue(_) => Some(Err(darling::Error::custom( + "Signal attribute does not support assigning a value!", + ) + .with_span(&attr.meta))), + }) + .transpose() + { + Ok(ops) => ops, + Err(err) => return (TokenStream::default(), err.write_errors()), + }; - quote! { + let const_assert = signal_ops.as_ref().map(|ops| { + let count = ops.0.parsed.len(); + + quote_spanned! { ops.0.original.span() => + const _: () = { + assert!(<#signal_type>::ARG_COUNT == #count as u8, "argument names do not match number of arguments."); + }; + } + }); + + let argument_names = signal_ops + .map(|names| { + let span = names.0.original.span(); + #[expect(unstable_name_collisions)] + let names: TokenStream = names + .0 + .parsed + .iter() + .map(|name| name.to_token_stream()) + .intersperse(quote!(,).into_token_stream()) + .collect(); + + quote_spanned! { span => Some(&[#names]) } + }) + .unwrap_or_else(|| quote!(None)); + + let metadata = quote! { ::godot_rust_script::private_export::RustScriptSignalDesc { name: #signal_name, - arguments: <#signal_type as ::godot_rust_script::ScriptSignal>::argument_desc(), + arguments: <#signal_type>::argument_desc(#argument_names), description: concat!(#signal_description), }, - } + }; + + (metadata, const_assert.unwrap_or_default()) } #[proc_macro_attribute] diff --git a/rust-script/src/interface.rs b/rust-script/src/interface.rs index 622f111..b40f6e9 100644 --- a/rust-script/src/interface.rs +++ b/rust-script/src/interface.rs @@ -18,6 +18,7 @@ use godot::prelude::{ConvertError, Gd, Object, StringName, Variant}; pub use crate::runtime::Context; pub use export::GodotScriptExport; +#[expect(deprecated)] pub use signals::{ScriptSignal, Signal}; pub trait GodotScript: Debug + GodotScriptImpl { diff --git a/rust-script/src/interface/signals.rs b/rust-script/src/interface/signals.rs index 13de2e8..5e10220 100644 --- a/rust-script/src/interface/signals.rs +++ b/rust-script/src/interface/signals.rs @@ -7,47 +7,32 @@ use std::marker::PhantomData; use godot::builtin::{ - Callable, Dictionary, GString, NodePath, StringName, Variant, Vector2, Vector3, + Callable, Dictionary, GString, NodePath, StringName, Variant, Vector2, Vector3, Vector4, }; use godot::classes::Object; use godot::global::{Error, PropertyHint}; use godot::meta::{GodotConvert, GodotType, ToGodot}; -use godot::obj::Gd; +use godot::obj::{Gd, GodotClass}; use crate::static_script_registry::RustScriptPropDesc; - -pub trait ScriptSignal { - type Args: SignalArguments; - - fn new(host: Gd, name: &'static str) -> Self; - - fn emit(&self, args: Self::Args); - - fn connect(&mut self, callable: Callable) -> Result<(), Error>; - - fn argument_desc() -> Box<[RustScriptPropDesc]>; - - fn name(&self) -> &str; -} +use crate::{GodotScript, RsRef}; pub trait SignalArguments { - fn count() -> u8; + const COUNT: u8; fn to_variants(&self) -> Vec; - fn argument_desc() -> Box<[RustScriptPropDesc]>; + fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]>; } impl SignalArguments for () { - fn count() -> u8 { - 0 - } + const COUNT: u8 = 0; fn to_variants(&self) -> Vec { vec![] } - fn argument_desc() -> Box<[RustScriptPropDesc]> { + fn argument_desc(_arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { Box::new([]) } } @@ -60,9 +45,7 @@ macro_rules! count_tts { macro_rules! tuple_args { (impl $($arg: ident),+) => { impl<$($arg: ToGodot),+> SignalArguments for ($($arg,)+) { - fn count() -> u8 { - count_tts!($($arg)+) - } + const COUNT: u8 = count_tts!($($arg)+); fn to_variants(&self) -> Vec { #[allow(non_snake_case)] @@ -73,9 +56,12 @@ macro_rules! tuple_args { ] } - fn argument_desc() -> Box<[RustScriptPropDesc]> { + fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { + #[expect(non_snake_case)] + let [$($arg),+] = arg_names.unwrap_or(&[$(stringify!($arg)),+]).try_into().unwrap(); //.unwrap_or_else(|| [$(stringify!($arg)),+]); + Box::new([ - $(signal_argument_desc!("0", $arg)),+ + $(signal_argument_desc!($arg, $arg)),+ ]) } } @@ -98,17 +84,17 @@ macro_rules! tuple_args { macro_rules! single_args { (impl $arg: ty) => { impl SignalArguments for $arg { - fn count() -> u8 { - 1 - } + const COUNT: u8 = 1; fn to_variants(&self) -> Vec { vec![self.to_variant()] } - fn argument_desc() -> Box<[RustScriptPropDesc]> { + fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { + let [arg_name] = arg_names.unwrap_or_else(|| &["0"]).try_into().unwrap(); + Box::new([ - signal_argument_desc!("0", $arg), + signal_argument_desc!(arg_name, $arg), ]) } } @@ -120,7 +106,7 @@ macro_rules! single_args { } macro_rules! signal_argument_desc { - ($name:literal, $type:ty) => { + ($name:expr, $type:ty) => { RustScriptPropDesc { name: $name, ty: <<<$type as GodotConvert>::Via as GodotType>::Ffi as godot::sys::GodotFfi>::VARIANT_TYPE.variant_as_nil(), @@ -135,21 +121,61 @@ macro_rules! signal_argument_desc { tuple_args!(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10); single_args!( - bool, u8, u16, u32, u64, i8, i16, i32, i64, f64, GString, StringName, NodePath, Vector2, - Vector3, Dictionary + bool, u8, u16, u32, u64, i8, i16, i32, i64, f32, f64, GString, StringName, NodePath, Vector2, + Vector3, Vector4, Dictionary ); +impl SignalArguments for Gd { + const COUNT: u8 = 1; + + fn to_variants(&self) -> Vec { + vec![self.to_variant()] + } + + fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { + let name = arg_names + .and_then(|list| list.first()) + .copied() + .unwrap_or("0"); + + Box::new([signal_argument_desc!(name, Self)]) + } +} + +impl SignalArguments for RsRef { + const COUNT: u8 = 1; + + fn to_variants(&self) -> Vec { + vec![self.to_variant()] + } + + fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { + Box::new([signal_argument_desc!( + arg_names + .and_then(|list| list.first()) + .copied() + .unwrap_or("0"), + Self + )]) + } +} + #[derive(Debug)] -pub struct Signal { +pub struct ScriptSignal { host: Gd, name: &'static str, args: PhantomData, } -impl ScriptSignal for Signal { - type Args = T; +#[deprecated( + note = "The Signal type has been deprecated and will be removed soon. Please use the ScriptSignal instead." +)] +pub type Signal = ScriptSignal; + +impl ScriptSignal { + pub const ARG_COUNT: u8 = T::COUNT; - fn new(host: Gd, name: &'static str) -> Self { + pub fn new(host: Gd, name: &'static str) -> Self { Self { host, name, @@ -157,33 +183,34 @@ impl ScriptSignal for Signal { } } - fn emit(&self, args: Self::Args) { + pub fn emit(&self, args: T) { self.host .clone() .emit_signal(self.name, &args.to_variants()); } - fn connect(&mut self, callable: Callable) -> Result<(), Error> { + pub fn connect(&mut self, callable: Callable) -> Result<(), Error> { match self.host.connect(self.name, &callable) { Error::OK => Ok(()), error => Err(error), } } - fn argument_desc() -> Box<[RustScriptPropDesc]> { - ::argument_desc() + #[doc(hidden)] + pub fn argument_desc(arg_names: Option<&[&'static str]>) -> Box<[RustScriptPropDesc]> { + ::argument_desc(arg_names) } - fn name(&self) -> &str { + pub fn name(&self) -> &str { self.name } } -impl GodotConvert for Signal { +impl GodotConvert for ScriptSignal { type Via = godot::builtin::Signal; } -impl ToGodot for Signal { +impl ToGodot for ScriptSignal { type ToVia<'v> = Self::Via where diff --git a/rust-script/tests/script_derive.rs b/rust-script/tests/script_derive.rs index 53e0456..c527b4b 100644 --- a/rust-script/tests/script_derive.rs +++ b/rust-script/tests/script_derive.rs @@ -7,7 +7,9 @@ use godot::builtin::{Array, GString}; use godot::classes::{Node, Node3D}; use godot::obj::{Gd, NewAlloc}; -use godot_rust_script::{godot_script_impl, Context, GodotScript, GodotScriptEnum, Signal}; +use godot_rust_script::{ + godot_script_impl, CastToScript, Context, GodotScript, GodotScriptEnum, RsRef, ScriptSignal, +}; #[derive(Debug, Default, GodotScriptEnum)] #[script_enum(export)] @@ -30,10 +32,16 @@ struct TestScript { pub enum_prop: u8, #[signal] - pub changed: Signal<()>, + pub changed: ScriptSignal<()>, + + #[signal("Expected", "Actual")] + pub ready: ScriptSignal<(u32, u32)>, + + #[signal("Base_Node")] + pub ready_base: ScriptSignal>, #[signal] - pub ready: Signal<(u32, u32)>, + pub ready_self: ScriptSignal>, pub node_prop: Option>, @@ -68,6 +76,10 @@ impl TestScript { base.emit_signal("hit", &[]); }); + self.ready.emit((1, 2)); + self.ready_base.emit(self.base.clone()); + self.ready_self.emit(self.base.to_script()); + ctx.reentrant_scope(self, |mut base: Gd| { base.set_owner(&Node::new_alloc()); });