mlua_magic_macros/
lib.rs

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