feat: Added attribute to easily register additional lua methods

Previously this could only be done by implementing a trait, like
`AddAdditionalMethods`, that that has an add_methods function where you
can put your custom methods. With this new attribute you can pass in a
register function directly!
This commit is contained in:
2025-09-09 04:00:52 +02:00
parent 2dbd491b81
commit 23355190ca
4 changed files with 70 additions and 15 deletions

1
Cargo.lock generated
View File

@@ -167,6 +167,7 @@ name = "automation_macro"
version = "0.1.0"
dependencies = [
"itertools",
"mlua",
"proc-macro2",
"quote",
"syn 2.0.106",

View File

@@ -11,3 +11,6 @@ itertools = { workspace = true }
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
[dev-dependencies]
mlua = { workspace = true }

View File

@@ -8,6 +8,7 @@ use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr {
Trait(TraitAttr),
AddMethods(AddMethodsAttr),
}
impl Parse for Attr {
@@ -19,7 +20,13 @@ impl Parse for Attr {
let attr = match ident.to_string().as_str() {
"traits" => Attr::Trait(attr.parse()?),
_ => return Err(syn::Error::new(ident.span(), "Expected 'traits'")),
"add_methods" => Attr::AddMethods(attr.parse()?),
_ => {
return Err(syn::Error::new(
ident.span(),
"Expected 'traits' or 'add_methods'",
));
}
};
Ok(attr)
@@ -98,23 +105,38 @@ impl Parse for Generics {
}
}
#[derive(Clone)]
struct AddMethodsAttr(syn::Path);
impl Parse for AddMethodsAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}
impl ToTokens for AddMethodsAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(path) = self;
tokens.extend(quote! {
#path
});
}
}
struct Implementation {
generics: Option<syn::AngleBracketedGenericArguments>,
traits: Traits,
}
impl From<(Option<syn::AngleBracketedGenericArguments>, Traits)> for Implementation {
fn from(value: (Option<syn::AngleBracketedGenericArguments>, Traits)) -> Self {
Self {
generics: value.0,
traits: value.1,
}
}
add_methods: Vec<AddMethodsAttr>,
}
impl quote::ToTokens for Implementation {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self { generics, traits } = &self;
let Self {
generics,
traits,
add_methods,
} = &self;
tokens.extend(quote! {
#generics {
@@ -135,6 +157,10 @@ impl quote::ToTokens for Implementation {
methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) });
#traits
#(
#add_methods(methods);
)*
}
}
});
@@ -145,6 +171,7 @@ struct Implementations(Vec<Implementation>);
impl From<Vec<Attr>> for Implementations {
fn from(attributes: Vec<Attr>) -> Self {
let mut add_methods = Vec::new();
let mut all = Traits::default();
let mut implementations: HashMap<_, Traits> = HashMap::new();
for attribute in attributes {
@@ -161,6 +188,7 @@ impl From<Vec<Attr>> for Implementations {
all.extend(&attribute.traits);
}
}
Attr::AddMethods(attribute) => add_methods.push(attribute),
}
}
@@ -172,7 +200,16 @@ impl From<Vec<Attr>> for Implementations {
}
}
Self(implementations.into_iter().map(Into::into).collect())
Self(
implementations
.into_iter()
.map(|(generics, traits)| Implementation {
generics,
traits,
add_methods: add_methods.clone(),
})
.collect(),
)
}
}

View File

@@ -32,26 +32,40 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream
/// Derive macro generating an impl for the trait `::mlua::UserData`
///
/// # Device traits
/// The `device(traits)` attribute can be used to tell the macro what traits are implemented so that
/// the appropriate methods can automatically be registered.
/// If the struct does not have any type parameters the syntax is very simple:
/// ```
/// ```rust
/// #[device(traits(TraitA, TraitB))]
/// ```
///
/// If the type does have type parameters you will have to manually specify all variations that
/// have the trait available:
/// ```
/// ```rust
/// #[device(traits(TraitA, TraitB for <StateA>, <StateB>))]
/// ```
/// If multiple of these attributes are specified they will all combined appropriately.
///
///
/// # NOTE
/// ## NOTE
/// If your type _has_ type parameters any instance of the traits attribute that does not specify
/// any type parameters will have the traits applied to _all_ other type parameter variations
/// listed in the other trait attributes. This behavior only applies if there is at least one
/// instance with type parameters specified.
///
/// # Additional methods
/// Additional methods can be added by using the `device(add_methods)` attribute. This attribute
/// takes the path to a function with the following signature that can register the additional methods:
///
/// ```rust
/// # struct D;
/// fn top_secret<M: mlua::UserDataMethods<D>>(methods: &mut M) {}
/// ```
/// It can then be registered with:
/// ```rust
/// #[device(add_methods(top_secret))]
/// ```
#[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);