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}