Skip to content

Commit 9082570

Browse files
committed
Add #[mlua::userdata] and #[mlua::userdata_impl] macros
1 parent 4aa6214 commit 9082570

31 files changed

Lines changed: 1539 additions & 119 deletions

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async = ["dep:futures-util"]
4242
send = ["error-send"]
4343
error-send = []
4444
serde = ["dep:serde", "dep:erased-serde", "dep:serde-value", "bstr/serde"]
45-
macros = ["mlua_derive/macros"]
45+
macros = ["mlua_derive/macros", "dep:inventory"]
4646
anyhow = ["dep:anyhow", "error-send"]
4747
userdata-wrappers = ["parking_lot/send_guard"]
4848

@@ -61,6 +61,7 @@ erased-serde = { version = "0.4", optional = true }
6161
serde-value = { version = "0.7", optional = true }
6262
parking_lot = { version = "0.12", features = ["arc_lock"] }
6363
anyhow = { version = "1.0", optional = true }
64+
inventory = { version = "0.3", optional = true }
6465
libc = "0.2"
6566

6667
ffi = { package = "mlua-sys", version = "0.11.0-rc.1", path = "mlua-sys" }

examples/userdata.rs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
1-
use mlua::{Lua, MetaMethod, Result, UserData, chunk};
1+
use mlua::{Lua, Result, chunk};
22

33
#[derive(Default)]
4+
#[mlua::userdata]
45
struct Rectangle {
56
length: u32,
67
width: u32,
78
}
89

9-
impl UserData for Rectangle {
10-
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
11-
fields.add_field_method_get("length", |_, this| Ok(this.length));
12-
fields.add_field_method_set("length", |_, this, val| {
13-
this.length = val;
14-
Ok(())
15-
});
16-
fields.add_field_method_get("width", |_, this| Ok(this.width));
17-
fields.add_field_method_set("width", |_, this, val| {
18-
this.width = val;
19-
Ok(())
20-
});
10+
#[mlua::userdata_impl]
11+
impl Rectangle {
12+
const NAME: &str = "Rectangle";
13+
14+
#[lua(infallible)]
15+
fn new(length: u32, width: u32) -> Self {
16+
Self { length, width }
17+
}
18+
19+
#[lua(getter, name = "area", infallible)]
20+
fn calculate_area(&self) -> u32 {
21+
self.length * self.width
2122
}
2223

23-
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
24-
methods.add_method("area", |_, this, ()| Ok(this.length * this.width));
25-
methods.add_method("diagonal", |_, this, ()| {
26-
Ok((this.length.pow(2) as f64 + this.width.pow(2) as f64).sqrt())
27-
});
24+
fn diagonal(&self) -> Result<f64> {
25+
Ok((self.length.pow(2) as f64 + self.width.pow(2) as f64).sqrt())
26+
}
2827

29-
// Constructor
30-
methods.add_meta_function(MetaMethod::Call, |_, ()| Ok(Rectangle::default()));
28+
// Constructor via `__call` metamethod
29+
#[lua(meta, infallible)]
30+
fn __call(length: u32, width: u32) -> Self {
31+
Rectangle::new(length, width)
3132
}
3233
}
3334

3435
fn main() -> Result<()> {
3536
let lua = Lua::new();
36-
let rectangle = Rectangle::default();
37+
lua.globals().set("Rectangle", lua.create_proxy::<Rectangle>()?)?;
3738
lua.load(chunk! {
38-
local rect = $rectangle()
39-
rect.width = 10
40-
rect.length = 5
41-
assert(rect:area() == 50)
42-
assert(rect:diagonal() - 11.1803 < 0.0001)
39+
local rect = Rectangle(10, 5)
40+
rect.width = rect.width + 5
41+
rect.length = rect.length + 5
42+
assert(rect.NAME == "Rectangle")
43+
assert(rect.area == 150)
44+
assert(math.floor(rect:diagonal()) == 18)
4345
})
4446
.exec()
4547
}

mlua_derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "mlua_derive"
33
version = "0.11.0"
44
authors = ["Aleksandr Orlenko <zxteam@pm.me>"]
5-
edition = "2021"
5+
edition = "2024"
66
description = "Procedural macros for the mlua crate."
77
repository = "https://github.com/mlua-rs/mlua"
88
keywords = ["lua", "mlua"]

mlua_derive/src/attr.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use proc_macro2::Span;
2+
use syn::meta::ParseNestedMeta;
3+
use syn::{Ident, LitStr, Result};
4+
5+
/// Parsed `#[lua(...)]` attribute.
6+
///
7+
/// Some flags are context-dependent:
8+
/// - Struct fields: `get`, `set`, `name`, `skip`
9+
/// - Impl methods: `getter`, `setter`, `static`, `meta`, `infallible`, `name`, `skip`
10+
#[derive(Default)]
11+
pub(crate) struct LuaAttr {
12+
pub(crate) name: Option<String>,
13+
pub(crate) infallible: bool,
14+
pub(crate) skip: bool,
15+
16+
// Struct field context flags
17+
pub(crate) get: bool,
18+
pub(crate) set: bool,
19+
20+
// Impl method context flags
21+
pub(crate) getter: bool,
22+
pub(crate) setter: bool,
23+
pub(crate) field: bool,
24+
pub(crate) meta: bool,
25+
}
26+
27+
impl LuaAttr {
28+
pub(crate) fn parse_inner(&mut self, meta: ParseNestedMeta) -> Result<()> {
29+
match &meta.path {
30+
path if path.is_ident("skip") => {
31+
if meta.value().is_ok() {
32+
return Err(meta.error("`skip` does not take a value"));
33+
}
34+
self.skip = true;
35+
}
36+
path if path.is_ident("infallible") => {
37+
if meta.value().is_ok() {
38+
return Err(meta.error("`infallible` does not take a value"));
39+
}
40+
self.infallible = true;
41+
}
42+
path if path.is_ident("get") => self.get = true,
43+
path if path.is_ident("set") => self.set = true,
44+
path if path.is_ident("getter") => self.getter = true,
45+
path if path.is_ident("setter") => self.setter = true,
46+
path if path.is_ident("field") => self.field = true,
47+
path if path.is_ident("meta") => self.meta = true,
48+
path if path.is_ident("name") => {
49+
let value = meta.value()?;
50+
let lit: LitStr = value.parse()?;
51+
self.name = Some(lit.value());
52+
}
53+
_ => {
54+
return Err(meta.error(
55+
"unsupported lua attribute, expected: ".to_string()
56+
+ "`skip`, `infallible`, `get`, `set`, `getter`, `setter`, `field`, `meta`, `name`",
57+
));
58+
}
59+
}
60+
Ok(())
61+
}
62+
63+
/// Returns the effective Lua name.
64+
pub(crate) fn name(&self, ident: &Ident) -> String {
65+
self.name.clone().unwrap_or_else(|| ident.to_string())
66+
}
67+
68+
/// Returns the effective Lua metamethod name.
69+
///
70+
/// If `name` is set via attribute, use it. Otherwise, if the function name
71+
/// starts with `__`, use that. Returns an error if neither is available.
72+
pub(crate) fn effective_meta_name(&self, fn_name: &Ident) -> Result<String> {
73+
if let Some(ref name) = self.name {
74+
return Ok(name.clone());
75+
}
76+
let fn_name = fn_name.to_string();
77+
if fn_name.starts_with("__") {
78+
return Ok(fn_name);
79+
}
80+
Err(syn::Error::new(
81+
Span::call_site(),
82+
format!(
83+
"could not infer metamethod name from `{fn_name}`, either add `name = \"...\"` to `#[lua(meta, ...)]` or prefix the function with `__`"
84+
),
85+
))
86+
}
87+
}

mlua_derive/src/from_lua.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro::TokenStream;
22
use quote::quote;
3-
use syn::{parse_macro_input, DeriveInput};
3+
use syn::{DeriveInput, parse_macro_input};
44

55
pub fn from_lua(input: TokenStream) -> TokenStream {
66
let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput);

mlua_derive/src/lib.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ use proc_macro::TokenStream;
22
use proc_macro2::{Ident, Span};
33
use quote::quote;
44
use syn::meta::ParseNestedMeta;
5-
use syn::{parse_macro_input, ItemFn, LitStr, Result};
5+
use syn::{ItemFn, LitStr, Result, parse_macro_input};
66

77
#[cfg(feature = "macros")]
88
use {
9-
crate::chunk::Chunk, proc_macro::TokenTree, proc_macro2::TokenStream as TokenStream2,
10-
proc_macro_error2::proc_macro_error,
9+
crate::chunk::Chunk, proc_macro::TokenTree, proc_macro_error2::proc_macro_error,
10+
proc_macro2::TokenStream as TokenStream2,
1111
};
1212

13+
#[cfg(feature = "macros")]
14+
macro_rules! try_compile {
15+
($expr:expr) => {
16+
match $expr {
17+
Ok(val) => val,
18+
Err(err) => return err.to_compile_error().into(),
19+
}
20+
};
21+
}
22+
1323
#[derive(Default)]
1424
struct ModuleAttributes {
1525
name: Option<Ident>,
@@ -151,9 +161,29 @@ pub fn from_lua(input: TokenStream) -> TokenStream {
151161
from_lua::from_lua(input)
152162
}
153163

164+
/// Attribute macro for exposing a Rust type as a Lua userdata.
165+
#[cfg(feature = "macros")]
166+
#[proc_macro_attribute]
167+
pub fn userdata(attr: TokenStream, item: TokenStream) -> TokenStream {
168+
userdata::userdata_type(attr, item)
169+
}
170+
171+
/// Attribute macro for exposing impl block methods to Lua userdata.
172+
#[cfg(feature = "macros")]
173+
#[proc_macro_attribute]
174+
pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
175+
userdata_impl::userdata_impl(attr, item)
176+
}
177+
178+
#[cfg(feature = "macros")]
179+
mod attr;
154180
#[cfg(feature = "macros")]
155181
mod chunk;
156182
#[cfg(feature = "macros")]
157183
mod from_lua;
158184
#[cfg(feature = "macros")]
159185
mod token;
186+
#[cfg(feature = "macros")]
187+
mod userdata;
188+
#[cfg(feature = "macros")]
189+
mod userdata_impl;

mlua_derive/src/userdata.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use proc_macro::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input};
4+
5+
use crate::attr::LuaAttr;
6+
7+
/// Parse all `#[lua(...)]` attributes on a field, merging them into one `LuaAttr`.
8+
fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result<LuaAttr> {
9+
let mut lua_attr = LuaAttr::default();
10+
for attr in attrs {
11+
if attr.path().is_ident("lua")
12+
&& let Meta::List(_) = &attr.meta
13+
{
14+
attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?;
15+
}
16+
}
17+
Ok(lua_attr)
18+
}
19+
20+
/// Strip `#[lua(...)]` attributes from a field, keeping all others.
21+
fn strip_lua_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
22+
(attrs.iter())
23+
.filter(|attr| !attr.path().is_ident("lua"))
24+
.cloned()
25+
.collect()
26+
}
27+
28+
pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream {
29+
if !attr.is_empty() {
30+
return Error::new_spanned(
31+
proc_macro2::TokenStream::from(attr),
32+
"`#[userdata]` does not accept arguments",
33+
)
34+
.to_compile_error()
35+
.into();
36+
}
37+
38+
let mut input = parse_macro_input!(item as DeriveInput);
39+
let type_name = &input.ident;
40+
41+
let mut named_fields: Option<&mut FieldsNamed> = match &mut input.data {
42+
Data::Struct(data) => match &mut data.fields {
43+
Fields::Named(fields) => Some(fields),
44+
Fields::Unnamed(_) | Fields::Unit => None,
45+
},
46+
Data::Enum(_) => None,
47+
Data::Union(_) => {
48+
return Error::new_spanned(&input, "`#[userdata]` cannot be applied to unions")
49+
.to_compile_error()
50+
.into();
51+
}
52+
};
53+
54+
// Check for generic type parameters (not supported)
55+
let has_type_params = input.generics.type_params().next().is_some();
56+
if has_type_params {
57+
return Error::new_spanned(
58+
&input.generics,
59+
"`#[userdata]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead."
60+
)
61+
.to_compile_error()
62+
.into();
63+
}
64+
65+
let mut field_registrations = Vec::new();
66+
if let Some(fields) = &mut named_fields {
67+
for field in &fields.named {
68+
let field_name = field.ident.as_ref().unwrap();
69+
70+
let lua_attr = try_compile!(parse_field_lua_attr(&field.attrs));
71+
if lua_attr.skip {
72+
continue;
73+
}
74+
75+
let lua_name = lua_attr.name.unwrap_or_else(|| field_name.to_string());
76+
77+
// Assume get/set by default (unless explicitly specified)
78+
let (has_get, has_set) = if lua_attr.get || lua_attr.set {
79+
(lua_attr.get, lua_attr.set)
80+
} else {
81+
(true, true)
82+
};
83+
84+
if has_get {
85+
field_registrations.push(quote! {
86+
registry.add_field_method_get(#lua_name, |_lua, this| Ok(this.#field_name.clone()));
87+
});
88+
}
89+
if has_set {
90+
field_registrations.push(quote! {
91+
registry.add_field_method_set(#lua_name, |_lua, this, val| {
92+
this.#field_name = val;
93+
Ok(())
94+
});
95+
});
96+
}
97+
}
98+
99+
// Strip mlua-specific attributes from fields before re-emitting
100+
for field in &mut fields.named {
101+
field.attrs = strip_lua_attrs(&field.attrs);
102+
}
103+
}
104+
105+
let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}");
106+
let register_fields_fn_name = format_ident!("__mlua_register_{type_name}_fields");
107+
108+
let output = quote! {
109+
#input
110+
111+
#[doc(hidden)]
112+
#[allow(non_camel_case_types)]
113+
struct #registration_type_name {
114+
register: fn(&mut ::mlua::userdata::UserDataRegistry<#type_name>),
115+
}
116+
117+
::mlua::__inventory::collect!(#registration_type_name);
118+
119+
#[allow(non_snake_case)]
120+
fn #register_fields_fn_name(registry: &mut ::mlua::userdata::UserDataRegistry<#type_name>) {
121+
use ::mlua::userdata::UserDataFields as _;
122+
#(#field_registrations)*
123+
}
124+
125+
::mlua::__inventory::submit! {
126+
#registration_type_name { register: #register_fields_fn_name }
127+
}
128+
129+
impl ::mlua::userdata::UserData for #type_name {
130+
fn register(registry: &mut ::mlua::userdata::UserDataRegistry<Self>) {
131+
for item in ::mlua::__inventory::iter::<#registration_type_name> {
132+
(item.register)(registry);
133+
}
134+
}
135+
}
136+
};
137+
138+
output.into()
139+
}

0 commit comments

Comments
 (0)