Skip to main content

lua_rs_derive/
lib.rs

1//! Derive macros for the lua-rs embedding API.
2//!
3//! - `#[derive(LuaUserData)]` on a struct generates the `UserData` impl. Rust
4//!   visibility is the scriptability boundary: **public** named fields are
5//!   auto-exposed to Lua (`obj.field` reads/writes); private fields stay
6//!   encapsulated unless force-exposed with `#[lua(field)]`. Tuple/newtype and
7//!   unit structs (`struct Handle(App);`) expose no fields and become opaque
8//!   userdata handles — ready for `#[lua(methods)]` and metamethods. Field
9//!   attributes: `#[lua(skip)]`, `#[lua(readonly)]`, `#[lua(name = "...")]`,
10//!   `#[lua(field)]` (force-expose a private field). `IntoLua` comes for free
11//!   from the runtime's blanket `impl<T: UserData> IntoLua for T`.
12//! - Struct attribute `#[lua_impl(Display, PartialEq, PartialOrd)]` wires the matching
13//!   metamethods (`__tostring`, `__eq`, `__lt`/`__le`) from the type's Rust trait impls.
14//! - Struct attribute `#[lua(methods)]` makes the generated `UserData` also register the
15//!   methods declared by `#[lua_methods]` on an `impl` block.
16//! - `#[lua_methods]` on an `impl` block exposes each `pub fn(&self/&mut self, ...)` to
17//!   Lua as `obj:method(args)`.
18
19use proc_macro::TokenStream;
20use quote::quote;
21use syn::{
22    parse_macro_input, Data, DeriveInput, Fields, FnArg, ImplItem, ItemImpl, LitStr, ReturnType,
23    Type,
24};
25
26// ---------------------------------------------------------------------------
27// #[derive(LuaUserData)]
28// ---------------------------------------------------------------------------
29
30struct FieldCfg {
31    ident: syn::Ident,
32    ty: Type,
33    lua_name: String,
34    skip: bool,
35    readonly: bool,
36    force_field: bool,
37}
38
39fn parse_field_cfg(field: &syn::Field) -> syn::Result<FieldCfg> {
40    let ident = field
41        .ident
42        .clone()
43        .ok_or_else(|| syn::Error::new_spanned(field, "LuaUserData requires named fields"))?;
44    let mut cfg = FieldCfg {
45        lua_name: ident.to_string(),
46        ident,
47        ty: field.ty.clone(),
48        skip: false,
49        readonly: false,
50        force_field: false,
51    };
52    for attr in &field.attrs {
53        if !attr.path().is_ident("lua") {
54            continue;
55        }
56        attr.parse_nested_meta(|meta| {
57            if meta.path.is_ident("skip") {
58                cfg.skip = true;
59                Ok(())
60            } else if meta.path.is_ident("readonly") {
61                cfg.readonly = true;
62                Ok(())
63            } else if meta.path.is_ident("field") {
64                cfg.force_field = true;
65                Ok(())
66            } else if meta.path.is_ident("name") {
67                let lit: LitStr = meta.value()?.parse()?;
68                cfg.lua_name = lit.value();
69                Ok(())
70            } else {
71                Err(meta.error(
72                    "unknown #[lua(...)] attribute; expected skip, readonly, field, or name",
73                ))
74            }
75        })?;
76    }
77    Ok(cfg)
78}
79
80/// Struct-level configuration from `#[lua(methods)]` and `#[lua_impl(...)]`.
81struct StructCfg {
82    register_methods: bool,
83    impl_display: bool,
84    impl_partial_eq: bool,
85    impl_partial_ord: bool,
86}
87
88fn parse_struct_cfg(input: &DeriveInput) -> syn::Result<StructCfg> {
89    let mut cfg = StructCfg {
90        register_methods: false,
91        impl_display: false,
92        impl_partial_eq: false,
93        impl_partial_ord: false,
94    };
95    for attr in &input.attrs {
96        if attr.path().is_ident("lua") {
97            attr.parse_nested_meta(|meta| {
98                if meta.path.is_ident("methods") {
99                    cfg.register_methods = true;
100                    Ok(())
101                } else {
102                    Err(meta.error("unknown #[lua(...)] attribute on struct; expected methods"))
103                }
104            })?;
105        } else if attr.path().is_ident("lua_impl") {
106            attr.parse_nested_meta(|meta| {
107                if meta.path.is_ident("Display") {
108                    cfg.impl_display = true;
109                    Ok(())
110                } else if meta.path.is_ident("PartialEq") {
111                    cfg.impl_partial_eq = true;
112                    Ok(())
113                } else if meta.path.is_ident("PartialOrd") {
114                    cfg.impl_partial_ord = true;
115                    Ok(())
116                } else {
117                    Err(meta.error(
118                        "unknown #[lua_impl(...)] trait; expected Display, PartialEq, or PartialOrd",
119                    ))
120                }
121            })?;
122        }
123    }
124    Ok(cfg)
125}
126
127/// Derive `UserData` for a struct: field access plus optional methods/metamethods.
128#[proc_macro_derive(LuaUserData, attributes(lua, lua_impl))]
129pub fn derive_lua_user_data(input: TokenStream) -> TokenStream {
130    let input = parse_macro_input!(input as DeriveInput);
131    expand_derive(input).unwrap_or_else(|e| e.to_compile_error().into())
132}
133
134fn expand_derive(input: DeriveInput) -> syn::Result<TokenStream> {
135    let name = &input.ident;
136
137    if !input.generics.params.is_empty() {
138        return Err(syn::Error::new_spanned(
139            &input.generics,
140            "LuaUserData does not yet support generic types",
141        ));
142    }
143
144    let scfg = parse_struct_cfg(&input)?;
145
146    let fields: Vec<&syn::Field> = match &input.data {
147        Data::Struct(s) => match &s.fields {
148            Fields::Named(named) => named.named.iter().collect(),
149            // Tuple/newtype and unit structs have no named fields, so there is
150            // nothing to auto-expose. The struct still becomes an opaque
151            // `UserData` handle (type name, methods, metamethods) — the common
152            // `struct Handle(App);` engine/resource-wrapper case. (issue #57)
153            Fields::Unnamed(_) | Fields::Unit => Vec::new(),
154        },
155        _ => {
156            return Err(syn::Error::new_spanned(
157                &input.ident,
158                "LuaUserData currently supports only structs",
159            ))
160        }
161    };
162
163    let mut field_regs = Vec::new();
164    for field in fields {
165        let cfg = parse_field_cfg(field)?;
166        // Rust visibility is the scriptability boundary: public fields are
167        // auto-exposed; private fields stay encapsulated unless the author
168        // force-exposes one with `#[lua(field)]`. This lets a struct hold a
169        // non-`Clone`/non-marshalable private field (e.g. `app: App`) and
170        // still derive cleanly, since unexposed fields get no getter/setter
171        // and so pick up no `Clone`/`IntoLua`/`FromLua` bound. (issue #56)
172        let is_pub = matches!(field.vis, syn::Visibility::Public(_));
173        if cfg.skip || (!is_pub && !cfg.force_field) {
174            continue;
175        }
176        let ident = &cfg.ident;
177        let ty = &cfg.ty;
178        let lua_name = &cfg.lua_name;
179        field_regs.push(quote! {
180            __m.add_field_method_get(#lua_name, |_, __this| {
181                ::core::result::Result::Ok(::core::clone::Clone::clone(&__this.#ident))
182            });
183        });
184        if !cfg.readonly {
185            field_regs.push(quote! {
186                __m.add_field_method_set(#lua_name, |_, __this, __value: #ty| {
187                    __this.#ident = __value;
188                    ::core::result::Result::Ok(())
189                });
190            });
191        }
192    }
193
194    let methods_call = if scfg.register_methods {
195        quote! { <Self>::__lua_register_methods(__m); }
196    } else {
197        quote! {}
198    };
199
200    let mut meta_regs = Vec::new();
201    if scfg.impl_display {
202        meta_regs.push(quote! {
203            __m.add_meta_method(::lua_rs_runtime::MetaMethod::ToString, |_, __this, ()| {
204                ::core::result::Result::Ok(::std::string::ToString::to_string(__this))
205            });
206        });
207    }
208    if scfg.impl_partial_eq {
209        meta_regs.push(quote! {
210            __m.add_meta_method(
211                ::lua_rs_runtime::MetaMethod::Eq,
212                |_, __this, __other: ::lua_rs_runtime::Value| {
213                    if let ::lua_rs_runtime::Value::UserData(__ud) = __other {
214                        if let ::core::result::Result::Ok(__o) = __ud.borrow::<#name>() {
215                            return ::core::result::Result::Ok(*__this == *__o);
216                        }
217                    }
218                    ::core::result::Result::Ok(false)
219                },
220            );
221        });
222    }
223    if scfg.impl_partial_ord {
224        meta_regs.push(quote! {
225            __m.add_meta_method(
226                ::lua_rs_runtime::MetaMethod::Lt,
227                |_, __this, __other: ::lua_rs_runtime::Value| {
228                    if let ::lua_rs_runtime::Value::UserData(__ud) = __other {
229                        if let ::core::result::Result::Ok(__o) = __ud.borrow::<#name>() {
230                            return ::core::result::Result::Ok(*__this < *__o);
231                        }
232                    }
233                    ::core::result::Result::Ok(false)
234                },
235            );
236            __m.add_meta_method(
237                ::lua_rs_runtime::MetaMethod::Le,
238                |_, __this, __other: ::lua_rs_runtime::Value| {
239                    if let ::lua_rs_runtime::Value::UserData(__ud) = __other {
240                        if let ::core::result::Result::Ok(__o) = __ud.borrow::<#name>() {
241                            return ::core::result::Result::Ok(*__this <= *__o);
242                        }
243                    }
244                    ::core::result::Result::Ok(false)
245                },
246            );
247        });
248    }
249
250    let add_meta_methods = if meta_regs.is_empty() {
251        quote! {}
252    } else {
253        quote! {
254            fn add_meta_methods<__M: ::lua_rs_runtime::UserDataMethods<Self>>(__m: &mut __M) {
255                #(#meta_regs)*
256            }
257        }
258    };
259
260    let expanded = quote! {
261        impl ::lua_rs_runtime::UserData for #name {
262            fn add_methods<__M: ::lua_rs_runtime::UserDataMethods<Self>>(__m: &mut __M) {
263                #(#field_regs)*
264                #methods_call
265            }
266            #add_meta_methods
267        }
268    };
269
270    Ok(expanded.into())
271}
272
273// ---------------------------------------------------------------------------
274// #[lua_methods]
275// ---------------------------------------------------------------------------
276
277/// Expose an `impl` block's public methods to Lua as `obj:method(args)`.
278#[proc_macro_attribute]
279pub fn lua_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
280    let item = parse_macro_input!(item as ItemImpl);
281    expand_methods(item).unwrap_or_else(|e| e.to_compile_error().into())
282}
283
284fn expand_methods(item: ItemImpl) -> syn::Result<TokenStream> {
285    let self_ty = &item.self_ty;
286    let mut regs = Vec::new();
287
288    for impl_item in &item.items {
289        let ImplItem::Fn(method) = impl_item else {
290            continue;
291        };
292        if !matches!(method.vis, syn::Visibility::Public(_)) {
293            continue;
294        }
295
296        // Must have a self receiver to be callable as obj:method(...).
297        let receiver = method.sig.inputs.first().and_then(|arg| match arg {
298            FnArg::Receiver(r) => Some(r),
299            _ => None,
300        });
301        let Some(receiver) = receiver else {
302            continue;
303        };
304        let is_mut = receiver.mutability.is_some();
305
306        let name = &method.sig.ident;
307        let lua_name = name.to_string();
308
309        // Collect the non-self arguments: names + types.
310        let mut arg_names = Vec::new();
311        let mut arg_types = Vec::new();
312        for (i, arg) in method.sig.inputs.iter().enumerate().skip(1) {
313            let FnArg::Typed(pat) = arg else {
314                return Err(syn::Error::new_spanned(
315                    arg,
316                    "#[lua_methods] does not support a second receiver",
317                ));
318            };
319            let ident = syn::Ident::new(&format!("__a{i}"), proc_macro2::Span::call_site());
320            arg_names.push(ident);
321            arg_types.push((*pat.ty).clone());
322        }
323
324        // Closure argument binding: () for none, `name: T` for one, `(..): (..)` for many.
325        let arg_binding = match arg_names.len() {
326            0 => quote! { () },
327            1 => {
328                let n = &arg_names[0];
329                let t = &arg_types[0];
330                quote! { #n: #t }
331            }
332            _ => {
333                quote! { ( #(#arg_names),* ): ( #(#arg_types),* ) }
334            }
335        };
336
337        // A method that returns a reference can't be marshaled as a Lua value;
338        // instead it names a sub-object reachable from `self`. Register it as
339        // an `add_function` that builds a delegate (a live sub-reference,
340        // re-borrowed from the parent per call). `&mut T` returns become a
341        // mutable delegate, `&T` returns a read-only one.
342        if let ReturnType::Type(_, ty) = &method.sig.output {
343            if let Type::Reference(r) = &**ty {
344                let referent = &*r.elem;
345                let ret_is_mut = r.mutability.is_some();
346                if !ret_is_mut && is_mut {
347                    return Err(syn::Error::new_spanned(
348                        &method.sig,
349                        "#[lua_methods]: a method returning `&T` must take `&self`; \
350                         use `-> &mut T` to expose a mutable delegate",
351                    ));
352                }
353                let func_binding = if arg_names.is_empty() {
354                    quote! { __ud: ::lua_rs_runtime::AnyUserData }
355                } else {
356                    quote! {
357                        ( __ud #(, #arg_names)* ):
358                            ( ::lua_rs_runtime::AnyUserData #(, #arg_types)* )
359                    }
360                };
361                let accessor = quote! { move |__this| <#self_ty>::#name(__this #(, #arg_names)*) };
362                let reg = if ret_is_mut {
363                    quote! {
364                        __m.add_function(#lua_name, |__lua, #func_binding| {
365                            __ud.delegate::<Self, #referent, _>(__lua, #accessor)
366                        });
367                    }
368                } else {
369                    quote! {
370                        __m.add_function(#lua_name, |__lua, #func_binding| {
371                            __ud.delegate_ref::<Self, #referent, _>(__lua, #accessor)
372                        });
373                    }
374                };
375                regs.push(reg);
376                continue;
377            }
378        }
379
380        let call = quote! { <#self_ty>::#name(__this #(, #arg_names)*) };
381        let returns_unit = matches!(&method.sig.output, ReturnType::Default)
382            || matches!(&method.sig.output, ReturnType::Type(_, ty) if is_unit_type(ty));
383        let body = if returns_unit {
384            quote! { { #call; ::core::result::Result::Ok(()) } }
385        } else {
386            quote! { ::core::result::Result::Ok(#call) }
387        };
388
389        if is_mut {
390            regs.push(quote! {
391                __m.add_method_mut(#lua_name, |_, __this, #arg_binding| #body);
392            });
393        } else {
394            regs.push(quote! {
395                __m.add_method(#lua_name, |_, __this, #arg_binding| #body);
396            });
397        }
398    }
399
400    let expanded = quote! {
401        #item
402
403        impl #self_ty {
404            #[doc(hidden)]
405            fn __lua_register_methods<__M: ::lua_rs_runtime::UserDataMethods<Self>>(__m: &mut __M) {
406                #(#regs)*
407            }
408        }
409    };
410
411    Ok(expanded.into())
412}
413
414fn is_unit_type(ty: &Type) -> bool {
415    matches!(ty, Type::Tuple(t) if t.elems.is_empty())
416}