From 2fd321847507ba2efc6cc6ae47151dd129adbfd6 Mon Sep 17 00:00:00 2001 From: Philipp Jungkamp Date: Fri, 22 Nov 2024 18:00:36 +0100 Subject: [PATCH] Add initial glib::signals! macro --- examples/object_subclass/author.rs | 16 +- glib-macros/src/lib.rs | 19 +- glib-macros/src/signals.rs | 482 +++++++++++++++++++++++++++++ glib/src/lib.rs | 4 +- glib/src/subclass/object.rs | 12 + 5 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 glib-macros/src/signals.rs diff --git a/examples/object_subclass/author.rs b/examples/object_subclass/author.rs index 86b6001149b5..e311acb8658b 100644 --- a/examples/object_subclass/author.rs +++ b/examples/object_subclass/author.rs @@ -4,14 +4,13 @@ use glib::prelude::*; use glib::subclass::prelude::*; use glib::subclass::Signal; -use glib::Properties; +use glib::subclass::object::DerivedObjectSignals; use std::cell::RefCell; -use std::sync::OnceLock; mod imp { use super::*; - #[derive(Properties, Default)] + #[derive(glib::Properties, Default)] #[properties(wrapper_type = super::Author)] pub struct Author { #[property(get, set)] @@ -20,11 +19,17 @@ mod imp { surname: RefCell, } + #[glib::signals(wrapper_type = super::Author)] + impl Author { + + #[signal] + fn awarded(&self) {} + } + #[glib::derived_properties] impl ObjectImpl for Author { fn signals() -> &'static [Signal] { - static SIGNALS: OnceLock> = OnceLock::new(); - SIGNALS.get_or_init(|| vec![Signal::builder("awarded").build()]) + Self::derived_signals() } } @@ -38,6 +43,7 @@ mod imp { glib::wrapper! { pub struct Author(ObjectSubclass); } + impl Author { pub fn new(name: &str, surname: &str) -> Self { glib::Object::builder() diff --git a/glib-macros/src/lib.rs b/glib-macros/src/lib.rs index 8f66dc855fd5..b77543915135 100644 --- a/glib-macros/src/lib.rs +++ b/glib-macros/src/lib.rs @@ -14,13 +14,14 @@ mod properties; mod shared_boxed_derive; mod value_delegate_derive; mod variant_derive; +mod signals; mod utils; use flags_attribute::AttrInput; use proc_macro::TokenStream; use proc_macro2::Span; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, Error}; use utils::{parse_nested_meta_items_from_stream, NestedMetaItem}; /// Macro for passing variables as strong or weak references into a closure. @@ -1587,3 +1588,19 @@ pub fn derive_value_delegate(input: TokenStream) -> TokenStream { pub fn async_test(args: TokenStream, item: TokenStream) -> TokenStream { async_test::async_test(args, item) } + +#[proc_macro_attribute] +pub fn signals(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as signals::SignalsArgs); + match syn::parse::(item) { + Ok(input) => signals::impl_signals(input, args) + .unwrap_or_else(syn::Error::into_compile_error) + .into(), + Err(_) => Error::new( + Span::call_site(), + signals::WRONG_PLACE_MSG, + ) + .into_compile_error() + .into(), + } +} diff --git a/glib-macros/src/signals.rs b/glib-macros/src/signals.rs new file mode 100644 index 000000000000..a2707318e163 --- /dev/null +++ b/glib-macros/src/signals.rs @@ -0,0 +1,482 @@ +// Take a look at the license at the top of the repository in the LICENSE file. +#![allow(unused)] + +use std::sync; + +use proc_macro2::{Span, TokenStream}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse::Parse, spanned::Spanned, ExprAsync, Result}; + +use crate::utils::*; + +pub const WRONG_PLACE_MSG: &str = + "This macro should be used on the `impl` block for the `ObjectSubclassImpl` type of a `glib::wrapper!` subclass"; + +pub struct SignalsArgs { + wrapper_type: syn::Type, +} + +impl Parse for SignalsArgs { + fn parse(input: syn::parse::ParseStream) -> Result { + let mut wrapper_type = None; + + while !input.is_empty() { + let ident = input.parse::()?; + + if ident == "wrapper_type" { + let _eq = input.parse::()?; + wrapper_type = Some(input.parse::()?); + } + + if input.peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(Self { + wrapper_type: wrapper_type.ok_or_else(|| { + syn::Error::new( + input.span(), + "missing wrapper_type in #[signal(wrapper_type = ...)]", + ) + })?, + }) + } +} + +enum SignalArg { + Name(syn::Ident, syn::Token![=], syn::LitStr), + RunFirst(syn::Ident), + RunLast(syn::Ident), + RunCleanup(syn::Ident), + NoRecurse(syn::Ident), + Detailed(syn::Ident), + Action(syn::Ident), + NoHooks(syn::Ident), + MustCollect(syn::Ident), + Deprecated(syn::Ident), + Accumulator(syn::Ident, syn::Token![=], syn::Expr), +} + +impl SignalArg { + fn span(&self) -> Span { + match self { + Self::Name(ident, _, _) + | Self::RunFirst(ident) + | Self::RunLast(ident) + | Self::RunCleanup(ident) + | Self::NoRecurse(ident) + | Self::Detailed(ident) + | Self::Action(ident) + | Self::NoHooks(ident) + | Self::MustCollect(ident) + | Self::Deprecated(ident) + | Self::Accumulator(ident, _, _) => ident.span(), + } + } +} + +impl Parse for SignalArg { + fn parse(input: syn::parse::ParseStream) -> Result { + let ident = input.parse::()?; + + if ident == "name" { + let eq_token = input.parse::()?; + let lit_str = input.parse::()?; + Ok(SignalArg::Name(ident, eq_token, lit_str)) + } else if ident == "run_first" { + Ok(SignalArg::RunFirst(ident)) + } else if ident == "run_last" { + Ok(SignalArg::RunLast(ident)) + } else if ident == "run_cleanup" { + Ok(SignalArg::RunCleanup(ident)) + } else if ident == "no_recurse" { + Ok(SignalArg::NoRecurse(ident)) + } else if ident == "detailed" { + Ok(SignalArg::Detailed(ident)) + } else if ident == "action" { + Ok(SignalArg::Action(ident)) + } else if ident == "no_hooks" { + Ok(SignalArg::NoHooks(ident)) + } else if ident == "must_collect" { + Ok(SignalArg::MustCollect(ident)) + } else if ident == "deprecated" { + Ok(SignalArg::Deprecated(ident)) + } else if ident == "accumulator" { + let eq_token = input.parse::()?; + let expr = input.parse::()?; + Ok(SignalArg::Accumulator(ident, eq_token, expr)) + } else { + Err(syn::Error::new_spanned(ident, "invalid argument")) + } + } +} + +#[derive(Default)] +struct SignalArgs(syn::punctuated::Punctuated); + +impl Parse for SignalArgs { + fn parse(input: syn::parse::ParseStream) -> Result { + input + .parse_terminated(SignalArg::parse, syn::Token![,]) + .map(Self) + } +} + +struct SignalBuilder { + wrapper_type: syn::Type, + impl_type: syn::Type, + fn_ident: syn::Ident, + param_types: Vec, + return_type: Option, + class_handler: bool, + + name: Option, + run_first: Option, + run_last: Option, + run_cleanup: Option, + no_recurse: Option, + detailed: Option, + action: Option, + no_hooks: Option, + must_collect: Option, + deprecated: Option, + accumulator: Option<(syn::Ident, syn::Expr)>, +} + +impl SignalBuilder { + fn new( + fn_: &syn::ImplItemFn, + impl_type: syn::Type, + wrapper_type: syn::Type, + args: Vec, + ) -> Result { + if !fn_.sig.inputs.get(0).is_some_and(|fn_arg| { + matches!( + fn_arg, + syn::FnArg::Receiver(syn::Receiver { + reference: Some(_), + mutability: None, + .. + }) + ) + }) { + return Err(syn::Error::new_spanned( + fn_, + "A #[signal] function needs a &self parameter", + )); + } + + let fn_ident = fn_.sig.ident.clone(); + + let param_types = fn_ + .sig + .inputs + .iter() + .filter_map(|fn_arg| match fn_arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(pat) => Some(pat.ty.as_ref().clone()), + }) + .collect(); + + let return_type = match &fn_.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, type_) => Some(type_.as_ref().clone()), + }; + + let class_handler = !fn_.block.stmts.is_empty(); + + let mut builder = Self { + wrapper_type, + impl_type, + fn_ident, + param_types, + return_type, + class_handler, + name: None, + run_first: None, + run_last: None, + run_cleanup: None, + no_recurse: None, + detailed: None, + action: None, + no_hooks: None, + must_collect: None, + deprecated: None, + accumulator: None, + }; + + for arg in args { + builder.add_arg(arg)?; + } + + Ok(builder) + } + + fn add_arg(&mut self, arg: SignalArg) -> Result<()> { + match arg { + SignalArg::Name(_, _, lit_str) => { + self.name = Some(lit_str); + } + + SignalArg::RunFirst(ident) if self.run_first.is_none() => { + self.run_first = Some(ident); + } + + SignalArg::RunLast(ident) if self.run_last.is_none() => { + self.run_last = Some(ident); + } + + SignalArg::RunCleanup(ident) if self.run_cleanup.is_none() => { + self.run_cleanup = Some(ident); + } + + SignalArg::NoRecurse(ident) if self.no_recurse.is_none() => { + self.no_recurse = Some(ident); + } + + SignalArg::Detailed(ident) if self.detailed.is_none() => { + self.detailed = Some(ident); + } + + SignalArg::Action(ident) if self.action.is_none() => { + self.action = Some(ident); + } + + SignalArg::NoHooks(ident) if self.no_hooks.is_none() => { + self.no_hooks = Some(ident); + } + + SignalArg::MustCollect(ident) if self.must_collect.is_none() => { + self.must_collect = Some(ident); + } + + SignalArg::Deprecated(ident) if self.deprecated.is_none() => { + self.deprecated = Some(ident); + } + + SignalArg::Accumulator(ident, _, expr) if self.accumulator.is_none() => { + self.accumulator = Some((ident, expr)); + } + + duplicate => { + return Err(syn::Error::new( + duplicate.span(), + "found duplicate argument for #[signal]", + )) + } + } + + Ok(()) + } + + fn expr(&self) -> TokenStream { + let crate_ident = crate_ident_new(); + + let Self { + wrapper_type, + impl_type, + fn_ident, + param_types, + return_type, + class_handler, + name, + run_first, + run_last, + run_cleanup, + no_recurse, + detailed, + action, + no_hooks, + must_collect, + deprecated, + accumulator, + } = self; + + let name = name.clone().unwrap_or_else(|| { + syn::LitStr::new( + &fn_ident.to_string().trim_matches('_').replace('_', "-"), + fn_ident.span(), + ) + }); + + let param_static_types = (!param_types.is_empty()).then(|| { + param_types.iter().map(|type_| { + quote! { <#type_ as #crate_ident::types::StaticType>::static_type() } + }) + }); + + let class_handler_wrapper = class_handler.then(|| { + let wrapper_ident = syn::Ident::new("wrapper", Span::mixed_site()); + let params_ident = syn::Ident::new("params", Span::mixed_site()); + let param_idents = (0..param_types.len()) + .map(|idx| quote::format_ident!("arg{}", idx, span = Span::mixed_site())) + .collect::>(); + let return_ident = syn::Ident::new("return_", Span::mixed_site()); + + let param_bindings = param_types.iter().zip(¶m_idents).enumerate().map(|(idx, (type_, ident))| { + let real_idx = idx + 1; + quote! { let #ident: #type_ = #params_ident[#real_idx].get().unwrap(); } + }); + + let return_wrapper = if *class_handler { + quote! { + |#return_ident| { + Some(<#return_type as #crate_ident::value::ToValue>::to_value(&#return_ident)) + } + } + } else { + quote! { (|()| None) } + }; + + let unwrapped = quote! { + #impl_type::#fn_ident( + <#impl_type as #crate_ident::subclass::types::ObjectSubclassExt>::from_obj(&#wrapper_ident), + #(#param_idents,)* + ) + }; + + quote! { + |#params_ident: &[#crate_ident::value::Value]| -> ::std::option::Option<#crate_ident::value::Value> { + let #wrapper_ident: #wrapper_type = #params_ident[0].get().unwrap(); + #(#param_bindings)* + (#return_wrapper)(#unwrapped) + } + } + }); + + let return_type = return_type.iter(); + let class_handler = class_handler_wrapper.into_iter(); + let param_types = param_static_types + .map(|static_types| quote! { param_types([#(#static_types),*]) }) + .into_iter(); + let run_first = run_first.iter(); + let run_last = run_last.iter(); + let run_cleanup = run_cleanup.iter(); + let no_recurse = no_recurse.iter(); + let detailed = detailed.iter(); + let action = action.iter(); + let no_hooks = no_hooks.iter(); + let must_collect = must_collect.iter(); + let deprecated = deprecated.iter(); + + quote! { + #crate_ident::subclass::signal::Signal::builder(#name) + #(.#param_types)* + #(.return_type::<#return_type>())* + #(.class_handler(#class_handler))* + #(.#run_first())* + #(.#run_last())* + #(.#run_cleanup())* + #(.#no_recurse())* + #(.#detailed())* + #(.#action())* + #(.#no_hooks())* + #(.#must_collect())* + #(.#deprecated())* + .build() + } + } +} + +fn parse_signal_attr(attr: syn::Attribute) -> Result> { + let args = match attr.meta { + syn::Meta::Path(path) => { + assert!(path.get_ident().is_some_and(|ident| ident == "signal")); + SignalArgs::default() + } + + syn::Meta::List(list) => { + assert!(list.path.get_ident().is_some_and(|ident| ident == "signal")); + syn::parse2(list.tokens)? + } + + other => { + return Err(syn::Error::new_spanned( + other, + "Invalid #[signal] attribute", + )) + } + }; + + Ok(args.0.into_iter()) +} + +fn parse_signal_attrs(attrs: &mut Vec) -> Result>> { + let mut args = None; + let mut idx = 0; + + while let Some(attr) = attrs.get(idx) { + if attr + .path() + .get_ident() + .is_some_and(|ident| ident == "signal") + { + args.get_or_insert_with(Vec::new) + .extend(parse_signal_attr(attrs.remove(idx))?); + } else { + idx += 1; + } + } + + Ok(args) +} + +fn parse_signals( + items: &mut [syn::ImplItem], + impl_type: &syn::Type, + wrapper_type: &syn::Type, +) -> Result> { + let mut signals = Vec::new(); + + for item in items.iter_mut() { + let syn::ImplItem::Fn(fn_) = item else { + continue; + }; + + let Some(args) = parse_signal_attrs(&mut fn_.attrs)? else { + continue; + }; + + if fn_.block.stmts.is_empty() { + fn_.attrs.push(syn::parse_quote!(#[allow(dead_code)])); + } + + signals.push(SignalBuilder::new( + fn_, + impl_type.clone(), + wrapper_type.clone(), + args, + )?); + } + + Ok(signals) +} + +fn impl_derived_signals(signals: &[SignalBuilder], impl_type: &syn::Type) -> TokenStream { + let crate_ident = crate_ident_new(); + let count = signals.len(); + let exprs = signals.iter().map(|builder| builder.expr()); + + quote! { + #[automatically_derived] + impl #crate_ident::subclass::object::DerivedObjectSignals for #impl_type { + fn derived_signals() -> &'static [#crate_ident::subclass::signal::Signal] { + static SIGNALS: ::std::sync::LazyLock<[#crate_ident::subclass::signal::Signal; #count]> = + ::std::sync::LazyLock::new(|| [ #(#exprs),* ]); + + SIGNALS.as_ref() + } + } + } +} + +pub fn impl_signals(mut input: syn::ItemImpl, args: SignalsArgs) -> Result { + let signals = parse_signals(&mut input.items, &input.self_ty, &args.wrapper_type)?; + let derived_signals = impl_derived_signals(&signals, &input.self_ty); + + Ok(quote! { + #input + #derived_signals + }) +} diff --git a/glib/src/lib.rs b/glib/src/lib.rs index 382a3d528894..ef130a01ee1d 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -30,8 +30,8 @@ pub use bitflags; pub use glib_macros::cstr_bytes; pub use glib_macros::{ async_test, clone, closure, closure_local, derived_properties, flags, object_interface, - object_subclass, Boxed, Downgrade, Enum, ErrorDomain, Properties, SharedBoxed, ValueDelegate, - Variant, + object_subclass, signals, Boxed, Downgrade, Enum, ErrorDomain, Properties, SharedBoxed, + ValueDelegate, Variant, }; pub use glib_sys as ffi; pub use gobject_sys as gobject_ffi; diff --git a/glib/src/subclass/object.rs b/glib/src/subclass/object.rs index f3a81c5e4fd0..332a0ce7ae97 100644 --- a/glib/src/subclass/object.rs +++ b/glib/src/subclass/object.rs @@ -192,6 +192,18 @@ pub trait DerivedObjectProperties: ObjectSubclass { } } +// rustdoc-stripper-ignore-next +/// Trait containing only the signals related functions of `ObjectImpl`. +/// Implemented by the `signals!` macro. +/// When implementing `ObjectImpl` you may want to delegate the function calls to this trait. +pub trait DerivedObjectSignals: ObjectSubclass { + // rustdoc-stripper-ignore-next + /// Signals installed for this type. + fn derived_signals() -> &'static [Signal] { + &[] + } +} + // rustdoc-stripper-ignore-next /// Extension trait for `glib::Object`'s class struct. ///