diff --git a/Cargo.lock b/Cargo.lock index b0ceb7cb686c..6b68b5a50acd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -507,6 +507,7 @@ dependencies = [ name = "glib-macros" version = "0.21.0" dependencies = [ + "bitflags", "glib", "heck", "proc-macro-crate", diff --git a/gir b/gir index 491114ad76bd..be3ecc201d39 160000 --- a/gir +++ b/gir @@ -1 +1 @@ -Subproject commit 491114ad76bde3d0c2c30312638782c6638c00c2 +Subproject commit be3ecc201d39c77befa1eb3abad2a3eed3c83eb0 diff --git a/gir-files b/gir-files index 56728a5eb215..3ede86d6a218 160000 --- a/gir-files +++ b/gir-files @@ -1 +1 @@ -Subproject commit 56728a5eb2157746f6377b11416c91a5970e7518 +Subproject commit 3ede86d6a21889d58809d8bdaab33e995fda8c15 diff --git a/glib-macros/Cargo.toml b/glib-macros/Cargo.toml index 3b99c21b6ce6..1999a86a4b64 100644 --- a/glib-macros/Cargo.toml +++ b/glib-macros/Cargo.toml @@ -18,6 +18,7 @@ proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0.100", features = ["full"] } proc-macro-crate = "3.3" +bitflags.workspace = true [lib] proc-macro = true diff --git a/glib-macros/src/derived_signals_attribute.rs b/glib-macros/src/derived_signals_attribute.rs new file mode 100644 index 000000000000..e2a9d3bf9164 --- /dev/null +++ b/glib-macros/src/derived_signals_attribute.rs @@ -0,0 +1,53 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use proc_macro2::{Span, TokenStream}; +use quote::quote; + +pub const WRONG_PLACE_MSG: &str = + "This macro should be used on `impl` block for `glib::ObjectImpl` trait"; + +pub fn impl_derived_signals(input: &syn::ItemImpl) -> syn::Result { + let syn::ItemImpl { + attrs, + generics, + trait_, + self_ty, + items, + .. + } = input; + + let trait_path = &trait_ + .as_ref() + .ok_or_else(|| syn::Error::new(Span::call_site(), WRONG_PLACE_MSG))? + .1; + + let mut has_signals = false; + + for item in items { + if let syn::ImplItem::Fn(method) = item { + let ident = &method.sig.ident; + + if ident == "signals" { + has_signals = true; + } + } + } + + let glib = crate::utils::crate_ident_new(); + + let signals = quote!( + fn signals() -> &'static [#glib::subclass::signal::Signal] { + Self::derived_signals() + } + ); + + let generated = [(!has_signals).then_some(signals)]; + + Ok(quote!( + #(#attrs)* + impl #generics #trait_path for #self_ty { + #(#items)* + #(#generated)* + } + )) +} diff --git a/glib-macros/src/lib.rs b/glib-macros/src/lib.rs index 8f66dc855fd5..3ca4d9f94226 100644 --- a/glib-macros/src/lib.rs +++ b/glib-macros/src/lib.rs @@ -5,6 +5,7 @@ mod boxed_derive; mod clone; mod closure; mod derived_properties_attribute; +mod derived_signals_attribute; mod downgrade_derive; mod enum_derive; mod error_domain_derive; @@ -12,6 +13,7 @@ mod flags_attribute; mod object_impl_attributes; mod properties; mod shared_boxed_derive; +mod signals_attribute; mod value_delegate_derive; mod variant_derive; @@ -1501,6 +1503,32 @@ pub fn derived_properties(_attr: TokenStream, item: TokenStream) -> TokenStream .into() } +/// This macro enables you to implement object signals in a quick way. +#[proc_macro_attribute] +pub fn signals(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr_input = syn::parse_macro_input!(attr as signals_attribute::Args); + + syn::parse::(item) + .map_err(|_| syn::Error::new(Span::call_site(), signals_attribute::WRONG_PLACE_MSG)) + .and_then(|item_input| signals_attribute::impl_signals(attr_input, item_input)) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[proc_macro_attribute] +pub fn derived_signals(_attr: TokenStream, item: TokenStream) -> TokenStream { + syn::parse::(item) + .map_err(|_| { + syn::Error::new( + Span::call_site(), + derived_signals_attribute::WRONG_PLACE_MSG, + ) + }) + .and_then(|input| derived_signals_attribute::impl_derived_signals(&input)) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + /// # Example /// ``` /// use glib::prelude::*; diff --git a/glib-macros/src/signals_attribute.rs b/glib-macros/src/signals_attribute.rs new file mode 100644 index 000000000000..029265703f4a --- /dev/null +++ b/glib-macros/src/signals_attribute.rs @@ -0,0 +1,507 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use bitflags::bitflags; +use proc_macro2::{Literal, Span, TokenStream}; +use quote::ToTokens; +use syn::{ext::IdentExt, parse::Parse, punctuated::Punctuated, Token}; + +use crate::utils::crate_ident_new; + +pub const WRONG_PLACE_MSG: &str = + "This macro should be used on a plain `impl` block of the inner object type"; + +/// An incomplete function in an impl block. +/// This is used to declare a signal with no class handler. +#[allow(unused)] +struct ImplItemIncompleteFn { + pub attrs: Vec, + pub vis: syn::Visibility, + pub defaultness: Option, + pub sig: syn::Signature, + pub semi_token: Token![;], +} +impl Parse for ImplItemIncompleteFn { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Self { + attrs: input.call(syn::Attribute::parse_outer)?, + vis: input.parse()?, + defaultness: input.parse()?, + sig: input.parse()?, + semi_token: input.parse()?, + }) + } +} + +/// Arguments to the `#[signals]` attribute. +pub struct Args { + wrapper_ty: syn::Path, + // None => no ext trait, + // Some(None) => derive the ext trait from the wrapper type, + // Some(Some(ident)) => use the given ext trait Ident + ext_trait: Option>, +} + +impl Parse for Args { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut wrapper_ty = None; + let mut ext_trait = None; + + while !input.is_empty() { + let ident = input.parse::()?; + if ident == "wrapper_type" { + let _eq = input.parse::()?; + wrapper_ty = Some(input.parse::()?); + } else if ident == "ext_trait" { + if input.peek(Token![=]) { + let _eq = input.parse::()?; + let ident = input.parse::()?; + ext_trait = Some(Some(ident)); + } else { + ext_trait = Some(None); + } + } + if input.peek(Token![,]) { + input.parse::()?; + } + } + + Ok(Self { + wrapper_ty: wrapper_ty.ok_or_else(|| { + syn::Error::new(input.span(), "missing #[signals(wrapper_type = ...)]") + })?, + ext_trait, + }) + } +} + +/// A single parameter in `#[signal(...)]`. +pub enum SignalAttr { + RunFirst(syn::Ident), + RunLast(syn::Ident), + RunCleanup(syn::Ident), + NoRecurse(syn::Ident), + Detailed(syn::Ident), + Action(syn::Ident), + NoHooks(syn::Ident), + Accum(syn::Expr), +} +impl Parse for SignalAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let name = input.call(syn::Ident::parse_any)?; + let name_str = name.to_string(); + + let result = if input.peek(Token![=]) { + let _assign_token: Token![=] = input.parse()?; + + match &*name_str { + "accum" => Self::Accum(input.parse()?), + _ => { + return Err(syn::Error::new_spanned( + name, + format!("Unsupported option {name_str}"), + )) + } + } + } else { + match &*name_str { + "run_first" => Self::RunFirst(name), + "run_last" => Self::RunLast(name), + "run_cleanup" => Self::RunCleanup(name), + "no_recurse" => Self::NoRecurse(name), + "detailed" => Self::Detailed(name), + "action" => Self::Action(name), + "no_hooks" => Self::NoHooks(name), + _ => { + return Err(syn::Error::new_spanned( + name, + format!("Unsupported option {name_str}"), + )) + } + } + }; + + Ok(result) + } +} +impl SignalAttr { + fn extract_items<'a>( + attrs: impl IntoIterator, + ) -> syn::Result>> { + let attr = match attrs + .into_iter() + .find(|attr| attr.path().is_ident("signal")) + { + Some(attr) => attr, + None => return Ok(None), + }; + match &attr.meta { + syn::Meta::Path(_) => Ok(Some(Vec::new())), + syn::Meta::List(meta_list) => { + let attrs: Punctuated = + meta_list.parse_args_with(Punctuated::parse_separated_nonempty)?; + Ok(Some(attrs.into_iter().collect())) + } + syn::Meta::NameValue(_) => { + return Err(syn::Error::new_spanned( + attr, + "expected #[signal] or #[signal(, ...)]", + )) + } + } + } +} + +bitflags! { + #[derive(Default, Clone, Copy, PartialEq, Eq)] + struct SignalFlags: u16 { + const RUN_FIRST = 1 << 0; + const RUN_LAST = 1 << 1; + const RUN_CLEANUP = 1 << 2; + const NO_RECURSE = 1 << 3; + const DETAILED = 1 << 4; + const ACTION = 1 << 5; + const NO_HOOKS = 1 << 6; + } +} + +/// Full description of an eventual signal, based on the provided +/// method signature and signal tag info. +struct SignalDesc { + name: String, + rs_name: String, + param_types: Vec, + return_type: Option, + flags: Vec, + class_handler: Option, +} +impl SignalDesc { + fn new( + flags: Vec, + signature: &syn::Signature, + complete: bool, + ) -> syn::Result { + const EXPECT_SELF_REF: &str = "signal method must take &self as its first parameter"; + + // ensure function takes &self first + match signature.inputs.get(0) { + Some(syn::FnArg::Receiver(syn::Receiver { + reference: Some(_), + mutability: None, + .. + })) => (), + _ => return Err(syn::Error::new_spanned(signature, EXPECT_SELF_REF)), + } + + // for now, get name from signature + let rs_name = signature.ident.to_string(); + let name = rs_name.replace('_', "-"); + + // parameters are remaining signature types + let param_types = if signature.inputs.len() >= 2 { + signature + .inputs + .iter() + .skip(1) + .map(|arg| match arg { + syn::FnArg::Receiver(_) => panic!("unexpected receiver"), + syn::FnArg::Typed(pat_type) => (&*pat_type.ty).clone(), + }) + .collect() + } else { + Vec::new() + }; + + let return_type = match &signature.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, rt) => match &**rt { + syn::Type::Tuple(syn::TypeTuple { elems, .. }) if elems.is_empty() => None, + rt => Some(rt.clone()), + }, + }; + + let class_handler = complete.then(|| signature.ident.clone()); + + Ok(Self { + name, + rs_name, + param_types, + return_type, + flags, + class_handler, + }) + } +} + +pub fn impl_signals(attr: Args, input: syn::ItemImpl) -> syn::Result { + if let Some((_, trait_path, _)) = &input.trait_ { + return Err(syn::Error::new_spanned(trait_path, WRONG_PLACE_MSG)); + } + let crate_name = crate_ident_new(); + + let mut out_impl = input.clone(); + out_impl.items.clear(); + let mut out_signals = Vec::::new(); + + // Extract signal methods + for item in &input.items { + match item { + item @ syn::ImplItem::Fn(method) => { + let flags = match SignalAttr::extract_items(&method.attrs)? { + Some(flags) => flags, + None => { + out_impl.items.push(item.clone()); + continue; + } + }; + let mut out_method = method.clone(); + out_method + .attrs + .retain(|item| !item.path().is_ident("signal")); + out_impl.items.push(syn::ImplItem::Fn(out_method)); + + let desc = SignalDesc::new(flags, &method.sig, true)?; + out_signals.push(desc); + } + item @ syn::ImplItem::Verbatim(tokens) => { + // try to parse as an incomplete function + let method = match syn::parse2::(tokens.clone()) { + Ok(parsed) => parsed, + Err(_) => { + out_impl.items.push(item.clone()); + continue; + } + }; + // if it has the signal attribute, it's a signal + // let the Rust compiler generate an error if not + let flags = match SignalAttr::extract_items(&method.attrs)? { + Some(flags) => flags, + None => { + out_impl.items.push(item.clone()); + continue; + } + }; + let desc = SignalDesc::new(flags, &method.sig, false)?; + out_signals.push(desc); + } + item => out_impl.items.push(item.clone()), + } + } + + // Implement DerivedObjectSignals + let derive_signals = impl_object_signals(&crate_name, &*input.self_ty, &out_signals); + + // Implement wrapper type + let wrapper_impl = impl_signal_wrapper(attr, &crate_name, &out_signals); + + Ok(quote::quote! { + #out_impl + #derive_signals + #wrapper_impl + }) +} + +fn impl_object_signals<'a>( + glib: &TokenStream, + ty: &syn::Type, + signals: impl IntoIterator< + Item = &'a SignalDesc, + IntoIter = impl Iterator + ExactSizeIterator, + >, +) -> TokenStream { + let signal_iter = signals.into_iter(); + let count = signal_iter.len(); + let builders = signal_iter.map(|signal| { + let name = syn::LitStr::new(&signal.name, Span::call_site()); + let param_types = match signal.param_types.is_empty() { + true => None, + false => { + let param_types = &signal.param_types; + Some(quote::quote! { + .param_types([#(<#param_types as #glib::types::StaticType>::static_type()),*]) + }) + } + }; + let return_type = match signal.return_type.as_ref() { + Some(rt) => Some(quote::quote! { + .return_type::<#rt>() + }), + None => None, + }; + let flags = signal.flags.iter().map(|item| match item { + SignalAttr::RunFirst(ident) => quote::quote! { + .#ident() + }, + SignalAttr::RunLast(ident) => quote::quote! { + .#ident() + }, + SignalAttr::RunCleanup(ident) => quote::quote! { + .#ident() + }, + SignalAttr::NoRecurse(ident) => quote::quote! { + .#ident() + }, + SignalAttr::Detailed(ident) => quote::quote! { + .#ident() + }, + SignalAttr::Action(ident) => quote::quote! { + .#ident() + }, + SignalAttr::NoHooks(ident) => quote::quote! { + .#ident() + }, + SignalAttr::Accum(expr) => quote::quote! { + .accumulator(#expr) + }, + }); + let class_handler = match &signal.class_handler { + Some(handler_fn) => { + let mut param_idents: Vec = + Vec::with_capacity(signal.param_types.len()); + let mut param_stmts: Vec = + Vec::with_capacity(signal.param_types.len()); + + for i in 0..signal.param_types.len() { + let i_h = i + 1; + let ty = &signal.param_types[i]; + let ident = quote::format_ident!("param{}", i); + let err_msg = Literal::string(&format!( + "Parameter {} for signal did not match ({})", + i_h, + ty.to_token_stream().to_string() + )); + + let stmt = quote::quote! { + let #ident = values[#i_h].get::<#ty>() + .expect(#err_msg); + }; + + param_idents.push(ident); + param_stmts.push(stmt); + } + + Some(quote::quote! { + .class_handler(|values| { + let this = values[0].get::< + ::Type + >() + .expect("`Self` parameter for signal did not match"); + #(#param_stmts)* + let result = < + ::Type + as #glib::subclass::types::ObjectSubclassIsExt + >::imp(&this) + .#handler_fn(#(#param_idents),*); + None + }) + }) + } + None => None, + }; + quote::quote! { + #glib::subclass::signal::Signal::builder(#name) + #param_types + #return_type + #(#flags)* + #class_handler + .build() + } + }); + quote::quote! { + #[automatically_derived] + impl #glib::subclass::object::DerivedObjectSignals for #ty { + fn derived_signals() -> &'static [glib::subclass::signal::Signal] { + static SIGNALS: ::std::sync::OnceLock<[#glib::subclass::signal::Signal; #count]> = + ::std::sync::OnceLock::new(); + SIGNALS.get_or_init(|| { + [ + #(#builders),* + ] + }) + } + } + } +} + +fn impl_signal_wrapper<'a>( + args: Args, + glib: &TokenStream, + signals: impl IntoIterator, +) -> TokenStream { + let signal_iter = signals.into_iter(); + let methods = signal_iter + .map(|signal| { + let connect_id = quote::format_ident!("connect_{}", &signal.rs_name); + let emit_id = quote::format_ident!("emit_{}", &signal.rs_name); + + let signal_name = Literal::string(&signal.name); + + let closure_bound = { + let param_types = &signal.param_types; + let return_type = signal + .return_type + .as_ref() + .map_or_else( + || quote::quote! { () }, + |value| value.to_token_stream() + ); + quote::quote! { + Fn(&Self, #(#param_types),*) -> #return_type + 'static + } + }; + + let param_types = &signal.param_types; + + let return_type = signal + .return_type + .as_ref() + .map_or_else( + || quote::quote! { () }, + |value| value.to_token_stream() + ); + + let closure_params: Vec<_> = (0..signal.param_types.len()) + .map(|i| quote::format_ident!("param{}", i)) + .collect(); + + let emit_coda = signal + .return_type + .as_ref() + .map_or_else( + || quote::quote! { ; }, + |ty| quote::quote! { + .unwrap().get::<#ty>().unwrap() + } + ); + + + quote::quote! { + pub fn #connect_id(&self, f: F) -> #glib::SignalHandlerId { + ::connect_closure( + self, + #signal_name, + false, + #glib::closure_local!(|this: &Self, #(#closure_params: #param_types),*| -> #return_type { + f(this, #(#closure_params),*) + }) + ) + } + pub fn #emit_id(&self, #(#closure_params: #param_types),*) -> #return_type { + ::emit_by_name_with_values( + self, + #signal_name, + &[ + #(#glib::value::ToValue::to_value(&#closure_params)),* + ] + ) + #emit_coda + } + } + }); + let ty = &args.wrapper_ty; + + quote::quote! { + impl #ty { + #(#methods)* + } + } +} diff --git a/glib-macros/tests/signals.rs b/glib-macros/tests/signals.rs new file mode 100644 index 000000000000..bd087af161b2 --- /dev/null +++ b/glib-macros/tests/signals.rs @@ -0,0 +1,67 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use std::{cell::Cell, rc::Rc}; + +use glib::object::ObjectExt; + +mod base { + use std::sync::LazyLock; + + use glib::object::ObjectSubclassIs; + use glib::prelude::*; + use glib::subclass::{prelude::*, SignalId}; + + pub mod imp { + use super::*; + + #[derive(Default)] + pub struct Base {} + + #[glib::signals(wrapper_type = super::Base)] + impl Base { + #[signal(run_first, action)] + fn run_first(&self) -> (); + + #[signal] + fn has_params(&self, int: i32, float: f32) -> i32; + } + + #[glib::object_subclass] + impl ObjectSubclass for Base { + const NAME: &'static str = "MyBase"; + type Type = super::Base; + } + + #[glib::derived_signals] + impl ObjectImpl for Base { + fn constructed(&self) {} + } + } + + glib::wrapper! { + pub struct Base(ObjectSubclass); + } +} + +#[test] +fn basic_test() { + let foo = glib::Object::new::(); + + let check: Rc> = Rc::new(Cell::new(false)); + + let h_id = foo.connect_run_first({ + let check = Rc::clone(&check); + move |_| { + check.set(true); + } + }); + + foo.emit_run_first(); + assert_eq!(check.get(), true, "Signal handler should have run"); + + foo.disconnect(h_id); + check.set(false); + + foo.emit_run_first(); + assert_eq!(check.get(), false, "Signal handler should not have run"); +} diff --git a/glib/src/lib.rs b/glib/src/lib.rs index 827c49b3fd59..b16bb8d8e07a 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -30,9 +30,9 @@ pub use bitflags; #[doc(hidden)] 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, + async_test, clone, closure, closure_local, derived_properties, derived_signals, flags, + object_interface, 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/mod.rs b/glib/src/subclass/mod.rs index 2ac82b03ee0f..977fd80bbcaa 100644 --- a/glib/src/subclass/mod.rs +++ b/glib/src/subclass/mod.rs @@ -445,7 +445,10 @@ pub mod prelude { pub use super::{ boxed::BoxedType, interface::{ObjectInterface, ObjectInterfaceExt, ObjectInterfaceType}, - object::{DerivedObjectProperties, ObjectClassSubclassExt, ObjectImpl, ObjectImplExt}, + object::{ + DerivedObjectProperties, DerivedObjectSignals, ObjectClassSubclassExt, ObjectImpl, + ObjectImplExt, + }, shared::{RefCounted, SharedType}, type_module::{TypeModuleImpl, TypeModuleImplExt}, type_plugin::{TypePluginImpl, TypePluginImplExt, TypePluginRegisterImpl}, diff --git a/glib/src/subclass/object.rs b/glib/src/subclass/object.rs index b6e548c6cd7e..53873943380a 100644 --- a/glib/src/subclass/object.rs +++ b/glib/src/subclass/object.rs @@ -192,6 +192,16 @@ pub trait DerivedObjectProperties: ObjectSubclass { } } +// rustdoc-stripper-ignore-next +/// Trait containing only the signal related functions of [`ObjectImpl`]. +/// Implemented by the [`signals`](crate::signals) macro. +/// When implementing `ObjectImpl` you may want to delegate the function calls to this trait. +pub trait DerivedObjectSignals: ObjectSubclass { + fn derived_signals() -> &'static [Signal] { + &[] + } +} + // rustdoc-stripper-ignore-next /// Extension trait for `glib::Object`'s class struct. ///