Skip to content

Commit

Permalink
glib: add gclosure macro
Browse files Browse the repository at this point in the history
  • Loading branch information
jf2048 committed Nov 10, 2021
1 parent 57020be commit 2df4964
Show file tree
Hide file tree
Showing 5 changed files with 437 additions and 2 deletions.
234 changes: 234 additions & 0 deletions glib-macros/src/gclosure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use crate::utils::crate_ident_new;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::Token;

#[derive(Clone, Copy, Debug)]
enum CaptureKind {
Weak,
WeakAllowNone,
Strong,
}

struct Capture {
name: TokenStream,
alias: Option<syn::Ident>,
kind: CaptureKind,
}

impl Capture {
fn alias(&self) -> TokenStream {
if let Some(ref a) = self.alias {
a.to_token_stream()
} else {
self.name.to_token_stream()
}
}
fn outer_before_tokens(&self, crate_ident: &TokenStream) -> TokenStream {
let alias = self.alias();
let name = &self.name;
match self.kind {
CaptureKind::Weak | CaptureKind::WeakAllowNone => quote! {
let #alias = #crate_ident::clone::Downgrade::downgrade(&#name);
},
CaptureKind::Strong => quote! {
let #alias = #name.clone();
},
}
}

fn outer_after_tokens(&self, crate_ident: &TokenStream, closure_ident: &Ident) -> TokenStream {
let name = &self.name;
match self.kind {
CaptureKind::Weak => quote! {
#crate_ident::object::Object::watch_closure(&#name, &#closure_ident);
},
_ => Default::default(),
}
}

fn inner_before_tokens(&self, crate_ident: &TokenStream) -> TokenStream {
let alias = self.alias();
match self.kind {
CaptureKind::Weak => {
let err_msg = format!("failed to upgrade `{}`", alias.to_string());
quote! {
let #alias = match #crate_ident::clone::Upgrade::upgrade(&#alias) {
Some(val) => val,
None => panic!(#err_msg)
};
}
}
CaptureKind::WeakAllowNone => quote! {
let #alias = #crate_ident::clone::Upgrade::upgrade(&#alias);
},
_ => Default::default(),
}
}
}

impl syn::parse::Parse for CaptureKind {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.parse::<Token![@]>()?;
let mut idents = vec![input.parse::<syn::Ident>()?];
while input.peek(Token![-]) {
input.parse::<Token![-]>()?;
idents.push(input.parse()?);
}
let keyword = idents
.into_iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join("-");
Ok(match keyword.as_str() {
"strong" => CaptureKind::Strong,
"weak" => CaptureKind::Weak,
"weak-allow-none" => CaptureKind::WeakAllowNone,
k => panic!(
"Unknown keyword `{}`, only `weak`, `weak-allow-none` and `strong` are allowed",
k,
),
})
}
}

impl syn::parse::Parse for Capture {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let kind = input.parse()?;
let mut name = TokenStream::new();
name.append(input.parse::<syn::Ident>()?);
while input.peek(Token![.]) {
input.parse::<Token![.]>()?;
name.append(proc_macro2::Punct::new('.', proc_macro2::Spacing::Alone));
name.append(input.parse::<syn::Ident>()?);
}
if name.to_string() == "self" {
panic!(
"Can't use `self` as variable name. Try storing it in a temporary variable or \
rename it using `as`."
);
}
let alias = if input.peek(Token![as]) {
input.parse::<Token![as]>()?;
input.parse()?
} else {
None
};
assert!(
!name.to_string().contains('.') || alias.is_some(),
"`{}`: Field accesses are not allowed as is, you must rename it!",
name
);
Ok(Capture { name, alias, kind })
}
}

struct Closure {
captures: Vec<Capture>,
args: Vec<Ident>,
closure: syn::ExprClosure,
constructor: &'static str
}

impl syn::parse::Parse for Closure {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut captures = vec![];
if input.peek(Token![@]) {
loop {
captures.push(input.parse()?);
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if !input.peek(Token![@]) {
break;
}
} else {
break;
}
}
}
if !captures.is_empty() {
input.parse::<Token![=>]>()?;
}
let mut closure = input.parse::<syn::ExprClosure>()?;
if closure.asyncness.is_some() {
panic!("Async closure not allowed");
}
if !captures.is_empty() && closure.capture.is_none() {
panic!(
"Closure with captures needs to be \"moved\" so please add `move` before closure"
)
}
let args = closure
.inputs
.iter()
.enumerate()
.map(|(i, _)| Ident::new(&format!("____value{}", i), Span::call_site()))
.collect();
closure.capture = None;
Ok(Closure {
captures,
args,
closure,
constructor: "new"
})
}
}

impl ToTokens for Closure {
fn to_tokens(&self, tokens: &mut TokenStream) {
let closure_ident = Ident::new("____closure", Span::call_site());
let values_ident = Ident::new("____values", Span::call_site());
let crate_ident = crate_ident_new();

let outer_before = self
.captures
.iter()
.map(|c| c.outer_before_tokens(&crate_ident));
let inner_before = self
.captures
.iter()
.map(|c| c.inner_before_tokens(&crate_ident));
let outer_after = self
.captures
.iter()
.map(|c| c.outer_after_tokens(&crate_ident, &closure_ident));

let arg_values = self.args.iter().enumerate().map(|(index, arg)| {
let err_msg = format!("Wrong type for argument {}: {{:?}}", index);
quote! {
let #arg = ::core::result::Result::unwrap_or_else(
#crate_ident::Value::get(&#values_ident[#index]),
|e| panic!(#err_msg, e),
);
}
});
let arg_names = &self.args;
let closure = &self.closure;
let constructor = Ident::new(self.constructor, Span::call_site());

tokens.extend(quote! {
{
let #closure_ident = {
#(#outer_before)*
#crate_ident::closure::Closure::#constructor(move |#values_ident| {
#(#inner_before)*
#(#arg_values)*
#crate_ident::closure::ToOptionValue::to_option_value(
&(#closure)(#(#arg_names),*)
)
})
};
#(#outer_after)*
#closure_ident
}
});
}
}

pub(crate) fn gclosure_inner(input: proc_macro::TokenStream, constructor: &'static str) -> proc_macro::TokenStream {
let mut closure = syn::parse_macro_input!(input as Closure);
closure.constructor = constructor;
closure.into_token_stream().into()
}
96 changes: 96 additions & 0 deletions glib-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod clone;
mod downgrade_derive;
mod gboxed_derive;
mod gboxed_shared_derive;
mod gclosure;
mod genum_derive;
mod gerror_domain_derive;
mod gflags_attribute;
Expand Down Expand Up @@ -265,6 +266,101 @@ pub fn clone(item: TokenStream) -> TokenStream {
clone::clone_inner(item)
}

/// The same as [`gclosure_local!`](crate::gclosure_local) but uses [`Closure::new`] as a
/// constructor.
///
/// [`Closure::new`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/closure/struct.Closure.html#method.new
#[proc_macro]
#[proc_macro_error]
pub fn gclosure(item: TokenStream) -> TokenStream {
gclosure::gclosure_inner(item, "new")
}

/// Macro for creating a [`Closure`] object. This is a wrapper around
/// [`Closure::new_local`] that type checks its arguments at runtime.
///
/// A `Closure` takes [`Value`] objects as inputs and output. This macro will
/// automatically convert the inputs to Rust types when invoking its callback, and then will
/// convert the output back to a `Value`. All inputs must implement the
/// [`FromValue`] trait, and outputs must either implement the
/// [`ToValue`] trait or be the unit type `()`.
///
/// Similarly to `clone`, this macro can be useful in combination with signal handlers to reduce
/// boilerplate when passing references. The `@weak`, `@weak-allow-none` and `@strong` captures are
/// supported. There is one major difference compared to `clone`: references captured with `@weak`
/// will always cause the closure to become invalidated when the object is destroyed. When a
/// closure connected to a signal becomes invalidated, the signal handler will automatically be
/// disconnected. Internally, this is accomplished using
/// [`Object::watch_closure`] on each of the weak references.
///
/// [`Closure`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/closure/struct.Closure.html
/// [`Closure::new_local`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/closure/struct.Closure.html#method.new_local
/// [`Value`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/value/struct.Value.html
/// [`FromValue`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/value/trait.FromValue.html
/// [`ToValue`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/value/trait.ToValue.html
/// [`Object::watch_closure`]: https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/object/trait.ObjectExt.html#tymethod.watch_closure
/// **⚠️ IMPORTANT ⚠️**
///
/// `glib` needs to be in scope, so unless it's one of the direct crate dependencies, you need to
/// import it because `clone!` is using it. For example:
///
/// ```rust,ignore
/// use gtk::glib;
/// ```
///
/// ### Using as a closure object
///
/// ```
/// use glib_macros::gclosure_local;
///
/// let concat_str = gclosure_local!(|s: &str| s.to_owned() + " World");
/// let result = concat_str.invoke(&[&"Hello"]).unwrap().get::<String>().unwrap();
/// assert_eq!(result, "Hello World");
/// ```
///
/// ### Connecting to a signal
///
/// All types must be fully inferrable within the callback.
///
/// ```
/// use glib;
/// use glib::prelude::*;
/// use glib_macros::gclosure_local;
///
/// let obj = glib::Object::new::<glib::Object>(&[]).unwrap();
/// obj.connect_closure(
/// "notify", false,
/// gclosure_local!(|_obj: glib::Object, pspec: glib::ParamSpec| {
/// println!("property notify: {}", pspec.name());
/// }));
/// ```
///
/// ### Weak References
///
/// ```
/// use glib;
/// use glib::prelude::*;
/// use glib_macros::gclosure_local;
///
/// let obj = glib::Object::new::<glib::Object>(&[]).unwrap();
/// {
/// let other = glib::Object::new::<glib::Object>(&[]).unwrap();
/// obj.connect_closure(
/// "notify", false,
/// gclosure_local!(@weak other => move |obj: glib::Object, pspec: glib::ParamSpec| {
/// let value = obj.property_value(pspec.name());
/// other.set_property(pspec.name(), &value);
/// }));
/// // The signal handler will disconnect automatically at the end of this
/// // block when `other` is dropped.
/// }
/// ```
#[proc_macro]
#[proc_macro_error]
pub fn gclosure_local(item: TokenStream) -> TokenStream {
gclosure::gclosure_inner(item, "new_local")
}

#[proc_macro_derive(GEnum, attributes(genum))]
#[proc_macro_error]
pub fn genum_derive(input: TokenStream) -> TokenStream {
Expand Down
Loading

0 comments on commit 2df4964

Please sign in to comment.