Skip to content
This repository has been archived by the owner on Mar 4, 2024. It is now read-only.

Signal connection wth Builder #359

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ config.status
*.o
*.swp
**Cargo.lock
gtk/tests/*
# gtk/tests/*
145 changes: 145 additions & 0 deletions gtk/tests/builder_handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#[derive(glib::Downgrade)]
pub struct MyWidget(gtk::Widget);

#[gtk3_macros::builder_handlers]
impl MyWidget {
fn handler0(&self) {}
sdroege marked this conversation as resolved.
Show resolved Hide resolved

fn handler1(&self, _p: u32) {}

fn handler2(&self, _x: f64, _y: f64) {}

fn handler3(&self, x: f64, y: f64) -> f64 {
x + y
}

// fn handler4(&self, x: f64, y: f64) -> Option<f64> {
// if x >= 0.0 && y >= 0.0 {
// Some(x + y)
// } else {
// None
// }
// }
}

// Generated code
/*
impl MyWidget {
#[allow(clippy)]
fn get_handler(
&self,
signal: &str,
) -> Option<Box<dyn Fn(&[glib::Value]) -> Option<glib::Value> + 'static>> {
match signal {
"handler0" => Some({
#[allow(unused_variables)]
Box::new(
glib::clone!(@weak self as this => move |values: &[glib::Value]| {
this.handler0();
None
}),
)
}),
"handler1" => Some({
#[allow(unused_variables)]
Box::new(
glib::clone!(@weak self as this => move |values: &[glib::Value]| {
this.handler1(
match values[0usize].get_some() {
Ok(value) => value,
Err(error) => {
glib::g_critical!(
"builder handler",
"Handler {} expects an argument of type {} but received `{:?}`: {}.",
"handler1", stringify! (u32),
values[0usize], error
);
return None;
},
}
);
None
}),
)
}),
"handler2" => Some({
#[allow(unused_variables)]
Box::new(
glib::clone!(@weak self as this => move |values: &[glib::Value]| {
this.handler2(
match values[0usize].get_some() {
Ok(value) => value,
Err(error) => {
glib::g_critical!(
"builder handler",
"Handler {} expects an argument of type {} but received `{:?}`: {}.",
"handler2",
stringify!(f64),
values[0usize],
error
);
return None;
},
},
match values[1usize].get_some() {
Ok(value) => value,
Err(error) => {
glib::g_critical!(
"builder handler",
"Handler {} expects an argument of type {} but received `{:?}`: {}.",
"handler2",
stringify!(f64),
values[1usize],
error
);
return None;
},
}
);
None
}),
)
}),
"handler3" => Some({
#[allow(unused_variables)]
Box::new(
glib::clone!(@weak self as this => move |values: &[glib::Value]| {
let result = this.handler3(
match values[0usize].get_some() {
Ok(value) => value,
Err(error) => {
glib::g_critical!(
"builder handler",
"Handler {} expects an argument of type {} but received `{:?}`: {}.",
"handler3",
stringify!(f64),
values[0usize],
error
);
return None;
},
},
match values[1usize].get_some() {
Ok(value) => value,
Err(error) => {
glib::g_critical!(
"builder handler",
"Handler {} expects an argument of type {} but received `{:?}`: {}.",
"handler3",
stringify!(f64),
values[1usize],
error
);
return None;
},
}
);
Some(glib::value::ToValue::to_value(&result))
}),
)
}),
_ => None,
}
}
}
*/
1 change: 1 addition & 0 deletions gtk3-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", default-features = false, features = ["full"] }
proc-macro-crate = "1.0"
darling = "0.12"
199 changes: 199 additions & 0 deletions gtk3-macros/src/builder_handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, quote_spanned};
use syn::{
parse, spanned::Spanned, Attribute, Error, FnArg, ImplItem, ImplItemMethod, ItemImpl, Meta,
MetaList, NestedMeta, PatType, Signature, Type,
};

#[derive(Debug, Default, FromMeta)]
#[darling(default)]
pub struct HandlersImplAttributes {
get_handler_fn: Option<String>,
}

#[derive(Debug, Default, FromMeta)]
#[darling(default)]
struct HandlerAttributes {
name: Option<String>,
}

#[derive(Debug)]
struct HandlerInfo {
name: String,
sig: Signature,
}

fn generate_handler(info: &HandlerInfo) -> Result<TokenStream2, Error> {
let handler_name = &info.name;
let arguments: Vec<TokenStream2> = info.sig.inputs
.iter()
.skip(1)
.enumerate()
.map(|(index, arg)| {
let arg_type = argument_type(arg)?;
Ok(quote_spanned! { arg.span() =>
match values[#index].get() {
Ok(value) => value,
Err(error) => {
glib::g_critical!("builder handler", "Handler {} expects an argument of type {} but received `{:?}`: {}.", #handler_name, stringify!(#arg_type), values[#index], error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a panic in one way or another. Returning None can easily cause memory safety issues in the C code calling this signal handler.

return None;
},
}
})
})
.collect::<Result<_, Error>>()?;

let signal = &info.name;
let method = &info.sig.ident;
let is_unit = matches!(info.sig.output, syn::ReturnType::Default);
let handler = if is_unit {
quote_spanned! { info.sig.span() =>
#signal => Some({
Box::new(glib::clone!(@weak self as this => @default-return None, move |values: &[glib::Value]| {
this.#method(#(#arguments),*);
None
}))
}),
}
} else {
quote_spanned! { info.sig.span() =>
#signal => Some({
Box::new(glib::clone!(@weak self as this => @default-return None, move |values: &[glib::Value]| {
let result = this.#method(#(#arguments),*);
Some(glib::value::ToValue::to_value(&result))
}))
}),
}
};
Ok(handler)
}

fn combine_errors(error_acc: &mut Option<Error>, error: Error) {
match error_acc {
Some(ref mut error_acc) => {
error_acc.combine(error);
}
None => {
error_acc.replace(error);
}
}
}

fn attributes_to_metas(attributes: Vec<Attribute>) -> Result<Vec<NestedMeta>, Error> {
let mut metas = Vec::new();
let mut error = None;
for attr in attributes {
let meta = attr.parse_meta()?;
match meta {
Meta::List(MetaList { nested, .. }) => metas.extend(nested),
_ => combine_errors(&mut error, Error::new(attr.span(), "Unexpected attribute")),
}
}
if let Some(error) = error {
Err(error)
} else {
Ok(metas)
}
}

fn is_assoc(sig: &Signature) -> bool {
sig.inputs
.first()
.map_or(false, |arg| matches!(arg, FnArg::Receiver(..)))
}

fn argument_type(arg: &FnArg) -> Result<&Type, Error> {
match arg {
FnArg::Typed(PatType { ty, .. }) => Ok(&*ty),
_ => Err(Error::new(
arg.span(),
"Cannot extract type of an argument.",
)),
}
}

fn generate_connect_method(
attrs: &HandlersImplAttributes,
actions: &[TokenStream2],
) -> ImplItemMethod {
let get_handler_fn = format_ident!(
"{}",
attrs.get_handler_fn.as_deref().unwrap_or("get_handler")
);
let builder_connect_method = quote! {
#[allow(clippy)]
#[allow(unused_variables, unused_braces)]
fn #get_handler_fn(&self, builder: &gtk::Builder, signal: &str) -> Option<Box<dyn Fn(&[glib::Value]) -> Option<glib::Value> + 'static>> {
match signal {
#(
#actions
)*
_ => None,
}
}
};
parse(builder_connect_method.into()).unwrap()
}

pub fn handlers(
attrs: HandlersImplAttributes,
mut input: ItemImpl,
) -> Result<TokenStream, TokenStream> {
let mut handlers: Vec<HandlerInfo> = Vec::new();
for item in input.items.iter_mut() {
if let ImplItem::Method(method) = item {
if !is_assoc(&method.sig) {
return Err(Error::new(
method.sig.span(),
"Unsupported signature of method. Only associated methods are supported.",
)
.to_compile_error()
.into());
}

let attributes =
extract_from_vec(&mut method.attrs, |attr| attr.path.is_ident("handler"));
let metas = attributes_to_metas(attributes).map_err(|err| err.to_compile_error())?;
let attrs = HandlerAttributes::from_list(&metas)
.map_err(|err| TokenStream::from(err.write_errors()))?;

let info = HandlerInfo {
name: attrs.name.unwrap_or_else(|| method.sig.ident.to_string()),
sig: method.sig.clone(),
};
handlers.push(info);
}
}

let connects: Vec<TokenStream2> = handlers
.iter()
.map(generate_handler)
.collect::<Result<_, _>>()
.map_err(|err| err.to_compile_error())?;

let connect_method = generate_connect_method(&attrs, &connects);
input.items.push(ImplItem::Method(connect_method));

let s = quote!(#input);
// println!("{}", s);
Ok(s.into())
}

// TODO: Replace this by Vec::drain_filter as soon as it is stabilized.
fn extract_from_vec<T>(vec: &mut Vec<T>, predicate: impl Fn(&T) -> bool) -> Vec<T> {
let mut i = 0;
let mut result: Vec<T> = Vec::new();
while i != vec.len() {
if (predicate)(&vec[i]) {
let item = vec.remove(i);
result.push(item);
} else {
i += 1;
}
}
result
}
18 changes: 17 additions & 1 deletion gtk3-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Take a look at the license at the top of the repository in the LICENSE file.

mod attribute_parser;
mod builder_handlers;
mod composite_template_derive;
mod util;

use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use syn::{parse_macro_input, DeriveInput};
use syn::DeriveInput;
use syn::{parse_macro_input, AttributeArgs, ItemImpl};

/// Derive macro for using a composite template in a widget.
///
Expand Down Expand Up @@ -61,3 +64,16 @@ pub fn composite_template_derive(input: TokenStream) -> TokenStream {
let gen = composite_template_derive::impl_composite_template(&input);
gen.into()
}

#[proc_macro_attribute]
pub fn builder_handlers(args: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemImpl);
let attribute_args = parse_macro_input!(args as AttributeArgs);
let attrs = match builder_handlers::HandlersImplAttributes::from_list(&attribute_args) {
Ok(v) => v,
Err(e) => {
return TokenStream::from(e.write_errors());
}
};
builder_handlers::handlers(attrs, input).unwrap_or_else(|err| err)
}