Skip to content
Closed
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
74 changes: 74 additions & 0 deletions glib-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ mod flags_attribute;
mod object_interface_attribute;
mod object_subclass_attribute;
mod shared_boxed_derive;
mod thread_local_object;
mod variant_derive;

mod utils;

use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use quote::ToTokens;
use syn::{parse_macro_input, DeriveInput, NestedMeta};

/// Macro for passing variables as strong or weak references into a closure.
Expand Down Expand Up @@ -820,3 +822,75 @@ pub fn cstr_bytes(item: TokenStream) -> TokenStream {
)
.unwrap_or_else(|e| e.into_compile_error().into())
}

/// Creates a static object that is globally accessible
/// as a thread local variable.
///
/// # Examples
///
/// ```
/// # // Create a module so `use super::*` will work properly.
/// # mod test_wrapper_mod {
/// # use glib_macros::thread_local_object;
/// #
/// # // Imitate a GObject.
/// # #[derive(Default, Clone)]
/// # pub struct MyGObject;
/// #
/// # impl MyGObject {
/// # fn with_label(_: &str) -> Self {
/// # Self
/// # }
/// # }
/// // Specify the name of the function and the type.
/// thread_local_object!(my_object, MyGObject);
///
/// // If your object doesn't have a [`Default`] implementation
/// // or you want to specify a constructor, you can use the
/// // third argument of the macro.
/// thread_local_object!(my_object2, MyGObject, MyGObject::with_label("placeholder"));
/// # }
/// # use test_wrapper_mod::*;
///
/// // Get the thread local object
/// let my_obj = my_object();
/// let my_obj2 = my_object2();
/// ```
///
/// # Panics
///
/// This macro uses [`thread_local`] internally.
/// This means each thread will see its own object without
/// any synchronization between the threads.
/// However, some GObjects such as GTK widgets can only be
/// used from a single thread and might cause a panic when
/// retrieved from a thread where GTK wasn't initialized.
///
/// ```
/// # // Create a module so `use super::*` will work properly.
/// # mod test_wrapper_mod {
/// # use glib_macros::thread_local_object;
/// #
/// # // Imitate a GObject.
/// # #[derive(Default, Clone)]
/// # pub struct MyGObject;
/// #
/// thread_local_object!(my_object, MyGObject);
/// # }
/// # use test_wrapper_mod::*;
///
/// let my_obj = my_object();
///
/// # let join_handle =
/// std::thread::spawn(|| {
/// // Don't do this with GTK widgets!
/// let my_obj = my_object();
/// });
/// # join_handle.join().unwrap();
/// ```
#[proc_macro]
pub fn thread_local_object(input: TokenStream) -> TokenStream {
let thread_local_object_tokens =
parse_macro_input!(input as thread_local_object::ThreadLocalObjectTokens);
thread_local_object_tokens.to_token_stream().into()
}
72 changes: 72 additions & 0 deletions glib-macros/src/thread_local_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Take a look at the license at the top of the repository in the LICENSE file.

use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
token, Expr, Ident, TypePath,
};

pub(crate) struct ThreadLocalObjectTokens {
name: Ident,
ty: TypePath,
init_expr: Option<Expr>,
}

impl Parse for ThreadLocalObjectTokens {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name: Ident = input.parse()?;
let _comma: token::Comma = input.parse()?;
let ty: TypePath = input.parse()?;

let init_expr = if input.is_empty() {
None
} else {
let _comma: token::Comma = input.parse()?;
Some(input.parse()?)
};

Ok(Self {
name,
ty,
init_expr,
})
}
}

impl ToTokens for ThreadLocalObjectTokens {
Copy link
Member

Choose a reason for hiding this comment

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

This could also just be a declarative macro in the glib crate or not? :) Having it as a proc-macro just complicates things

fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
name,
ty,
init_expr,
} = self;

let init_stream = if let Some(init_expr) = init_expr {
init_expr.to_token_stream()
} else {
quote_spanned! {
ty.span() => #ty::default()
}
};

let mod_name = Ident::new(
&format!("__thread_local_object_private_{name}"),
name.span(),
);

tokens.extend(quote! {
mod #mod_name {
use super::*;
::std::thread_local!(static THREAD_LOCAL_OBJ: #ty = #init_stream);

pub fn #name() -> #ty {
THREAD_LOCAL_OBJ.with(|w| w.clone())
Copy link
Member

Choose a reason for hiding this comment

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

Why not use something from once_cell here? Then you wouldn't have to export a function but could export a value that derefs to the object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow, that actually works 😅. Because unsync::Lazy::new is const you can use it in global const variables. I wasn't aware that you could use non-Send values in global constants, but that seems to work pretty well actually. I guess this makes this PR rather obsolete then...

Copy link
Member

Choose a reason for hiding this comment

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

Yay, less code :)

}
}

pub use #mod_name::#name;
});
}
}