mlua_magic_macros/lib.rs
1mod compile;
2mod load;
3
4extern crate proc_macro;
5
6use crate::compile::parse_compile_args;
7
8use ::proc_macro::TokenStream;
9
10use ::proc_macro2;
11use ::proc_macro2::Ident;
12
13use ::quote::{format_ident, quote};
14
15use ::syn::{Fields, Pat, TypePath, parse_macro_input};
16
17/// Implements a helper function `_to_mlua_fields` for a Rust struct,
18/// enabling automatic registration of named fields with `mlua::UserData`.
19///
20/// When applied to a struct, this macro generates an implementation
21/// of a private helper function that is later invoked by the
22/// `mlua_magic_macros::compile!` macro. This ensures the struct’s fields
23/// are visible in Lua as userdata fields.
24///
25/// # Behavior
26/// * Public and private named fields are exported as readable fields in Lua.
27/// * Getter methods are automatically generated via `add_field_method_get`.
28/// * Fields must implement `Clone` for successful conversion to Lua values.
29///
30/// # Limitations
31/// * Only structs with **named fields** are currently supported.
32/// * Setter support is not yet implemented.
33///
34/// # Usage
35/// Apply the macro directly to the struct definition:
36///
37/// ```ignore
38/// #[derive(Clone, Copy, Default)]
39/// #[mlua_magic_macros::structure]
40/// struct Player {
41/// name: String,
42/// hp: i32,
43/// }
44///
45/// // Later, compile userdata:
46/// mlua_magic_macros::compile!(type_path = Player, fields = true, methods = true);
47/// ```
48///
49/// After registration through `mlua::UserData`,
50/// Lua scripts may access the fields:
51///
52/// ```lua
53/// print(player.name)
54/// print(player.hp)
55/// ```
56///
57/// This macro is designed to work together with:
58/// * `#[mlua_magic_macros::implementation]` — for methods
59/// * `#[mlua_magic_macros::enumeration]` — for enum variants
60/// * `mlua_magic_macros::compile!` — final hookup to `mlua::UserData`
61///
62/// This simplifies mlua integration by reducing boilerplate and
63/// ensuring a consistent interface between Rust types and Lua scripts.
64#[proc_macro_attribute]
65pub fn structure(_attr: TokenStream, item: TokenStream) -> TokenStream {
66 let ast: syn::ItemStruct = parse_macro_input!(item as syn::ItemStruct);
67 let name: &Ident = &ast.ident;
68
69 // TODO: Add type validation?
70 let mut user_data_fields: Vec<proc_macro2::TokenStream> = Vec::new();
71
72 for field in &ast.fields {
73 let field_name: &Ident = field.ident.as_ref().expect("Field must have a name");
74 let field_name_str: String = field_name.to_string();
75 let field_ty: &syn::Type = &field.ty;
76
77 user_data_fields.push(quote! {
78 fields.add_field_method_get(#field_name_str, |_, this| {
79 return Ok(this.#field_name.clone());
80 });
81 });
82
83 user_data_fields.push(quote! {
84 fields.add_field_method_set(#field_name_str, |_, this, val: #field_ty| {
85 this.#field_name = val;
86 return Ok(());
87 });
88 });
89 }
90
91 // Create the helper function `_to_mlua_fields`
92 let helper_fn: proc_macro2::TokenStream = quote! {
93 impl #name {
94 #[doc(hidden)]
95 pub fn _to_mlua_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) -> () {
96 #(#user_data_fields)*
97 }
98 }
99 };
100
101 let original_tokens: proc_macro2::TokenStream = quote! {
102 #ast
103 };
104 let helper_tokens: proc_macro2::TokenStream = quote! {
105 #helper_fn
106 };
107
108 let mut output: proc_macro2::TokenStream = original_tokens;
109 output.extend(helper_tokens);
110
111 return output.into();
112}
113
114/// Implements a helper function `_to_mlua_variants` for a Rust `enum'.
115///
116/// This function registers all variants (e.g.,
117/// as static properties on the Lua UserData. This allows accessing
118/// them in Lua as `MyEnum.VariantA`.
119///
120/// # Example:
121/// ```ignore
122/// #[derive(Clone, Copy)] // Required for UserData methods
123/// #[mlua_magic::enumeration]
124/// enum MyEnum {
125/// VariantA,
126/// VariantB(i32),
127/// }
128/// ```
129///
130/// This is intended to be used with `impl mlua::UserData`.
131#[proc_macro_attribute]
132pub fn enumeration(__attr: TokenStream, item: TokenStream) -> TokenStream {
133 let ast: syn::ItemEnum = parse_macro_input!(item as syn::ItemEnum);
134 let name: &Ident = &ast.ident;
135 // let name_str: String = name.to_string();
136
137 // Build registrations for unit variants (register as static constructors)
138 let mut variant_registrations: Vec<proc_macro2::TokenStream> = Vec::new();
139 for variant in &ast.variants {
140 match &variant.fields {
141 Fields::Unit => {
142 let variant_name: &Ident = &variant.ident;
143 let variant_name_str: String = variant_name.to_string();
144
145 // use add_function to register an associated/static function that returns the enum
146 variant_registrations.push(quote! {
147 // e.g. methods.add_function("Idle", |_, (): ()| Ok(PlayerStatus::Idle));
148 methods.add_function(#variant_name_str, |_, (): ()| {
149 Ok(#name::#variant_name)
150 });
151 });
152 }
153 Fields::Unnamed(fields) => {
154 let variant_name = &variant.ident;
155 let variant_name_str = variant_name.to_string();
156
157 // Extract each field type T1, T2, …
158 let field_types: Vec<_> = fields.unnamed.iter().map(|f| &f.ty).collect();
159
160 let arg_idents: Vec<Ident> = (0..field_types.len())
161 .map(|i: usize| {
162 return format_ident!("arg{}", i);
163 })
164 .collect();
165
166 variant_registrations.push(quote! {
167 methods.add_function(#variant_name_str, |_, (#(#arg_idents),*): (#(#field_types),*)| {
168 Ok(#name::#variant_name(#(#arg_idents),*))
169 });
170 });
171 }
172 Fields::Named(fields) => {
173 // Same pattern as unnamed, except wrap into a struct-like variant:
174 let variant_name = &variant.ident;
175 let variant_name_str = variant_name.to_string();
176
177 let names: Vec<_> = fields
178 .named
179 .iter()
180 .map(|f| f.ident.as_ref().unwrap())
181 .collect();
182 let types: Vec<_> = fields.named.iter().map(|f| &f.ty).collect();
183
184 variant_registrations.push(quote! {
185 methods.add_function(#variant_name_str, |_, tbl: mlua::Table| {
186 Ok(#name::#variant_name {
187 #(#names: tbl.get::<_, #types>(stringify!(#names))?),*
188 })
189 });
190 });
191 }
192 };
193 }
194
195 // Create helper fn _to_mlua_variants, plus FromLua and IntoLua impls for lossless userdata round-trip.
196 // FromLua requires Clone so we can return owned values from borrowed userdata.
197 let helper_fn: proc_macro2::TokenStream = quote! {
198 impl #name {
199 #[doc(hidden)]
200 pub fn _to_mlua_variants<M: mlua::UserDataMethods<Self>>(methods: &mut M) -> () {
201 #(#variant_registrations)*;
202 }
203 }
204 };
205
206 let original_tokens: proc_macro2::TokenStream = quote! {
207 #ast
208 };
209 let helper_tokens: proc_macro2::TokenStream = quote! {
210 #helper_fn
211 };
212
213 let mut output: proc_macro2::TokenStream = original_tokens;
214 output.extend(helper_tokens);
215
216 return output.into();
217}
218
219/// Implements a helper function `_to_mlua_methods` for a Rust `impl` block,
220/// enabling automatic registration of its methods with `mlua::UserData`.
221///
222/// When applied to an `impl` block, this macro scans for functions and
223/// generates an implementation of a private helper function. This function
224/// is later invoked by the `mlua_magic_macros::compile!` macro.
225///
226/// # Behavior
227/// * **Static Functions** (e.g., `fn new() -> Self`) are registered as static
228/// functions on the userdata, accessible in Lua as `MyType.new()`.
229/// * **Immutable Methods** (e.g., `fn my_method(&self)`) are registered as
230/// immutable methods, accessible in Lua as `my_instance:my_method()`.
231/// * **Mutable Methods** (e.g., `fn my_mut_method(&mut self)`) are registered as
232/// mutable methods, accessible in Lua as `my_instance:my_mut_method()`.
233///
234/// # Usage
235/// Apply the macro directly to the `impl` block for the type:
236///
237/// ```ignore
238/// #[mlua_magic_macros::structure]
239/// struct Player { hp: i32 }
240///
241/// #[mlua_magic_macros::implementation]
242/// impl Player {
243/// pub fn new() -> Self { Self { hp: 100 } }
244/// pub fn is_alive(&self) -> bool { self.hp > 0 }
245/// pub fn take_damage(&mut self, amount: i32) { self.hp -= amount; }
246/// }
247///
248/// // Later, compile userdata:
249/// mlua_magic_macros::compile!(type_path = Player, fields = true, methods = true);
250/// ```
251///
252/// Lua scripts may then call these methods:
253///
254/// ```lua
255/// local p = Player.new()
256/// p:take_damage(30)
257/// print(p:is_alive())
258/// ```
259///
260/// This macro is designed to work together with:
261/// * `#[mlua_magic_macros::structure]` — for fields
262/// * `#[mlua_magic_macros::enumeration]` — for enum variants
263/// * `mlua_magic_macros::compile!` — final hookup to `mlua::UserData`
264#[proc_macro_attribute]
265pub fn implementation(_attr: TokenStream, item: TokenStream) -> TokenStream {
266 let ast: syn::ItemImpl = parse_macro_input!(item as syn::ItemImpl);
267 let name: &syn::Type = &ast.self_ty;
268
269 let mut method_registrations: Vec<proc_macro2::TokenStream> = Vec::new();
270
271 for item in &ast.items {
272 if let syn::ImplItem::Fn(fn_item) = item {
273 let fn_name: &Ident = &fn_item.sig.ident;
274 let fn_name_str: String = fn_name.to_string();
275
276 // Extract argument names and types, skipping the `self` receiver
277 let (arg_names, arg_tys): (Vec<_>, Vec<_>) = fn_item
278 .sig
279 .inputs
280 .iter()
281 .filter_map(|arg| {
282 if let syn::FnArg::Typed(pat_type) = arg {
283 if let Pat::Ident(pat_ident) = &*pat_type.pat {
284 Some((&pat_ident.ident, &pat_type.ty))
285 } else {
286 None
287 }
288 } else {
289 None
290 }
291 })
292 .unzip();
293
294 // Check if the function is async
295 let is_async = fn_item.sig.asyncness.is_some();
296
297 // Check for `&self`, `&mut self`, or static
298 if let Some(receiver) = &fn_item.sig.receiver() {
299 if receiver.mutability.is_some() {
300 // Here, `this` is `&mut self`
301 if is_async {
302 method_registrations.push(quote! {
303 methods.add_async_method_mut(#fn_name_str, |_, mut this, (#(#arg_names,)*): (#(#arg_tys,)*)| async move {
304 return Ok(this.#fn_name(#(#arg_names,)*).await);
305 });
306 });
307 } else {
308 method_registrations.push(quote! {
309 methods.add_method_mut(#fn_name_str, |_, this, (#(#arg_names,)*): (#(#arg_tys,)*)| {
310 return Ok(this.#fn_name(#(#arg_names,)*));
311 });
312 });
313 }
314 } else {
315 // Here, `this` is `&self`
316 if is_async {
317 method_registrations.push(quote! {
318 methods.add_async_method(#fn_name_str, |_, this, (#(#arg_names,)*): (#(#arg_tys,)*)| async move {
319 return Ok(this.#fn_name(#(#arg_names,)*).await);
320 });
321 });
322 } else {
323 method_registrations.push(quote! {
324 methods.add_method(#fn_name_str, |_, this, (#(#arg_names,)*): (#(#arg_tys,)*)| {
325 return Ok(this.#fn_name(#(#arg_names,)*));
326 });
327 });
328 }
329 };
330 } else {
331 // This is a static function (like `new`)
332 if is_async {
333 method_registrations.push(quote! {
334 methods.add_async_function(#fn_name_str, |_, (#(#arg_names,)*): (#(#arg_tys,)*)| async {
335 return Ok(#name::#fn_name(#(#arg_names,)*).await);
336 });
337 });
338 } else {
339 method_registrations.push(quote! {
340 methods.add_function(#fn_name_str, |_, (#(#arg_names,)*): (#(#arg_tys,)*)| {
341 return Ok(#name::#fn_name(#(#arg_names,)*));
342 });
343 });
344 }
345 };
346 };
347 }
348
349 // Create the helper function `_to_mlua_methods`
350 let helper_fn: proc_macro2::TokenStream = quote! {
351 impl #name {
352 #[doc(hidden)]
353 pub fn _to_mlua_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) -> () {
354 #(#method_registrations)*
355 }
356 }
357 };
358
359 let original_tokens: proc_macro2::TokenStream = quote! {
360 #ast
361 };
362 let helper_tokens: proc_macro2::TokenStream = quote! {
363 #helper_fn
364 };
365
366 let mut output: proc_macro2::TokenStream = original_tokens;
367 output.extend(helper_tokens);
368
369 return output.into();
370}
371
372// # Bottom of file
373// TODO: Move out of lib.rs when possible
374
375/// Generates the final `impl mlua::UserData` block for a type.
376///
377/// This macro calls the helper functions generated by `#[structure]`,
378/// `#[implementation]`, and `#[enumeration]`.
379///
380/// You must specify which helpers to include.
381///
382/// # Example (for a struct):
383/// ```ignore
384/// #[mlua_magic::structure]
385/// struct Player { health: i32 }
386///
387/// #[mlua_magic::implementation]
388/// impl Player {
389/// // ... methods ...
390/// }
391///
392/// // Generates `impl mlua::UserData for Player`
393/// mlua_magic::compile!(type_path = Player, fields = true, methods true);
394/// ```
395///
396/// # Example (for an enum):
397/// ```ignore
398/// #[mlua_magic::enumeration]
399/// enum Status { Idle, Busy }
400///
401/// #[mlua_magic::implementation]
402/// impl Status {
403/// // ... methods ...
404/// }
405///
406/// // Generates `impl mlua::UserData for Status` and `impl mlua::IntoLua for Status`
407/// mlua_magic::compile!(type_path = Status, variants = true, methods = true);
408/// ```
409#[proc_macro]
410pub fn compile(input: TokenStream) -> TokenStream {
411 let compile_args: compile::CompileArgs = parse_compile_args(input).unwrap();
412 let type_path: TypePath = compile_args.type_path.clone().expect("Type is required.");
413
414 // Conditionally generate the call to the helper function
415 let fields_call: proc_macro2::TokenStream = if compile_args.fields.unwrap_or(false) {
416 quote! {
417 Self::_to_mlua_fields(fields);
418 }
419 } else {
420 quote! { /* Do nothing */ }
421 };
422
423 let methods_call: proc_macro2::TokenStream = if compile_args.methods.unwrap_or(false) {
424 quote! {
425 Self::_to_mlua_methods(methods);
426 }
427 } else {
428 quote! { /* Do nothing */ }
429 };
430
431 let variants_call: proc_macro2::TokenStream = if compile_args.variants.unwrap_or(false) {
432 quote! {
433 Self::_to_mlua_variants(methods);
434 }
435 } else {
436 quote! { /* Do nothing */ }
437 };
438
439 // Assemble the final `impl mlua::UserData` block
440 let output: proc_macro2::TokenStream = quote! {
441 impl mlua::UserData for #type_path {
442 fn add_fields<'lua, F: mlua::UserDataFields<Self>>(fields: &mut F) -> () {
443 #fields_call
444 }
445
446 fn add_methods<'lua, M: mlua::UserDataMethods<Self>>(methods: &mut M) -> () {
447 #methods_call
448 #variants_call
449 }
450 }
451 impl mlua::FromLua for #type_path {
452 fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
453 let output: mlua::Result<Self> = match value {
454 mlua::Value::UserData(user_data) => {
455 return match user_data.borrow::<Self>() {
456 Ok(b) => Ok((*b).clone()),
457 Err(_) => Err(mlua::Error::FromLuaConversionError {
458 from: "UserData",
459 to: stringify!(#type_path).to_string(),
460 message: Some("userdata is not this exact Rust type".into()),
461 })
462 };
463 },
464 _ => Err(mlua::Error::FromLuaConversionError {
465 from: value.type_name(),
466 to: stringify!(#type_path).to_string(),
467 message: Some("expected userdata created by mlua_magic_macros".into()),
468 }),
469 };
470
471 return output;
472 }
473 }
474 /*impl #type_path {
475 #[doc(hidden)]
476 pub fn _to_mlua_skeleton(lua: &mlua::Lua) -> Result<mlua::AnyUserData, mlua::Error> { // Spooky scary skeletons
477 let skeleton: mlua::AnyUserData = lua.create_any_userdata(Self::default())?;
478
479 // TODO: Implement this
480
481 return Ok(skeleton);
482 }
483 }*/
484 /*impl mlua::IntoLua for #type_path {
485 fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
486 let user_data: mlua::AnyUserData = lua.create_any_userdata(self)?;
487 let value: mlua::Value = user_data.to_value();
488
489 return Ok(value);
490 }
491 }*/
492 };
493
494 return output.into();
495}
496
497/// Registers one or more Rust types implementing `mlua::UserData` as global
498/// variables in a `mlua::Lua` instance.
499///
500/// This macro is the final step to make your Rust types accessible from Lua.
501/// It creates a "proxy" for each type (which acts as a constructor table)
502/// and assigns it to a global variable in Lua with the same name as the Rust type.
503///
504/// # Usage
505/// The macro takes the `lua` instance as the first argument, followed by a
506/// comma-separated list of types to register.
507///
508/// ```ignore
509/// // (Assuming Player and PlayerStatus implement mlua::UserData)
510/// use mlua::prelude::*;
511///
512/// fn main() -> LuaResult<()> {
513/// let lua = Lua::new();
514///
515/// // This call...
516/// mlua_magic_macros::load!(lua, Player, PlayerStatus);
517///
518/// // ...is equivalent to this Lua code:
519/// // Player = (proxy for Player UserData)
520/// // PlayerStatus = (proxy for PlayerStatus UserData)
521///
522/// lua.load(r#"
523/// print(Player) -- "userdata: Player"
524/// print(PlayerStatus) -- "userdata: PlayerStatus"
525///
526/// local p = Player.new("Hero")
527/// p.status = PlayerStatus.Walking()
528/// "#).exec()?;
529///
530/// Ok(())
531/// }
532/// ```
533///
534/// # Prerequisites
535/// All types passed to `load!` must implement `mlua::UserData`. This is
536/// typically handled by using the `mlua_magic_macros::compile!` macro.
537#[proc_macro]
538pub fn load(input: TokenStream) -> TokenStream {
539 let load::LoadInput {
540 lua_expr,
541 type_paths,
542 } = parse_macro_input!(input as load::LoadInput);
543
544 let output: proc_macro2::TokenStream = quote! {{
545 let lua: &mlua::Lua = &#lua_expr;
546 let globals: mlua::Table = lua.globals();
547
548 #(
549 // Register type globally under its Rust name
550 globals.set(stringify!(#type_paths), lua.create_proxy::<#type_paths>()?)?;
551 )*
552 }};
553
554 return output.into();
555}