Skip to content

Commit a1bc818

Browse files
committed
mlua_derive(macros): Allow taking both Lua and &Lua in async functions.
Async functions/methods by default receive owned `Lua` variant. When user want to take `&Lua`, the code fails with "arguments to this function are incorrect". We can dynamically detect "owned" or "ref" varians and pass corresponding type. Closes #713
1 parent 7fbef5a commit a1bc818

2 files changed

Lines changed: 74 additions & 23 deletions

File tree

mlua_derive/src/userdata/userdata_impl.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct ArgInfo {
3838

3939
struct MethodInfo {
4040
self_kind: SelfKind,
41-
has_lua: bool,
41+
lua: Option<LuaArg>,
4242
args: Vec<ArgInfo>,
4343
}
4444

@@ -50,18 +50,31 @@ fn ref_inner_type(ty: &Type) -> Type {
5050
}
5151
}
5252

53-
/// Check if the type is `&Lua` or `&mlua::Lua`.
54-
fn is_lua_ref(ty: &Type) -> bool {
55-
let Type::Reference(ref_ty) = ty else { return false };
56-
match &*ref_ty.elem {
57-
Type::Path(p) if p.path.segments.len() == 1 => p.path.segments[0].ident == "Lua",
58-
Type::Path(p) if p.path.segments.len() == 2 => {
59-
p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua"
60-
}
53+
/// How the `Lua` context parameter is passed to the method.
54+
enum LuaArg {
55+
Ref,
56+
Owned,
57+
}
58+
59+
/// Check if the type is `Lua` or `mlua::Lua`.
60+
fn is_lua_type(ty: &Type) -> bool {
61+
let Type::Path(p) = ty else { return false };
62+
match p.path.segments.len() {
63+
1 => p.path.segments[0].ident == "Lua",
64+
2 => p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua",
6165
_ => false,
6266
}
6367
}
6468

69+
/// Classify the method's `Lua` context parameter, if present.
70+
fn lua_arg_kind(ty: &Type) -> Option<LuaArg> {
71+
match ty {
72+
Type::Reference(r) if r.mutability.is_none() && is_lua_type(&r.elem) => Some(LuaArg::Ref),
73+
ty if is_lua_type(ty) => Some(LuaArg::Owned),
74+
_ => None,
75+
}
76+
}
77+
6578
/// Classify a `&[mut] T` parameter, returning the callback wrapper type.
6679
///
6780
/// Known borrow types come from the mapping table `BORROW_WRAPPERS`.
@@ -128,10 +141,10 @@ fn try_unwrap_option(ty: &Type) -> Option<&Type> {
128141
/// Analyze method signature.
129142
///
130143
/// Determine `self` kind and collect the callback arguments.
131-
/// Auto-detects `&Lua` as the first non-self parameter.
144+
/// Auto-detects `Lua` (owned or reference) as the first non-self parameter.
132145
fn analyze_self_and_args(sig: &Signature) -> syn::Result<MethodInfo> {
133146
let mut self_kind = SelfKind::None;
134-
let mut has_lua = false;
147+
let mut lua = None;
135148
let mut args = Vec::new();
136149
let mut check_first_typed = true;
137150

@@ -147,12 +160,14 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result<MethodInfo> {
147160
self_kind = SelfKind::Owned;
148161
}
149162
FnArg::Typed(typed) => {
150-
if check_first_typed && is_lua_ref(&typed.ty) {
151-
has_lua = true;
163+
if check_first_typed {
152164
check_first_typed = false;
153-
continue;
165+
if let Some(kind) = lua_arg_kind(&typed.ty) {
166+
lua = Some(kind);
167+
continue;
168+
}
154169
}
155-
check_first_typed = false;
170+
156171
if let syn::Pat::Ident(pat_ident) = &*typed.pat {
157172
let arg_type = &*typed.ty;
158173
let mut option_inner = None;
@@ -202,11 +217,7 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result<MethodInfo> {
202217
}
203218
}
204219

205-
Ok(MethodInfo {
206-
self_kind,
207-
has_lua,
208-
args,
209-
})
220+
Ok(MethodInfo { self_kind, lua, args })
210221
}
211222

212223
fn strip_item_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
@@ -343,6 +354,16 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
343354
let info = try_compile!(analyze_self_and_args(&method.sig));
344355
let is_async = method.sig.asyncness.is_some();
345356

357+
// Owned `Lua` is only available to async callbacks.
358+
if !is_async && matches!(info.lua, Some(LuaArg::Owned)) {
359+
return syn::Error::new_spanned(
360+
&method.sig,
361+
"owned `Lua` parameter is only supported for `async` methods (use `&Lua` instead)",
362+
)
363+
.to_compile_error()
364+
.into();
365+
}
366+
346367
if lua_attr.getter {
347368
if is_async {
348369
return syn::Error::new_spanned(&method.sig, "async field getter is not supported")
@@ -520,7 +541,7 @@ fn gen_call_args(info: &MethodInfo) -> TokenStream2 {
520541
_ => call_args.push(quote! { this }),
521542
}
522543

523-
if info.has_lua {
544+
if info.lua.is_some() {
524545
call_args.push(quote! { lua });
525546
}
526547

@@ -542,8 +563,10 @@ fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 {
542563
SelfKind::Owned => call_args.push(quote! { this }),
543564
}
544565

545-
if info.has_lua {
546-
call_args.push(quote! { lua });
566+
match info.lua {
567+
Some(LuaArg::Ref) => call_args.push(quote! { &lua }),
568+
Some(LuaArg::Owned) => call_args.push(quote! { lua }),
569+
None => {}
547570
}
548571

549572
for arg in &info.args {

tests/userdata_macro.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,14 @@ mod async_tests {
452452
Ok(42)
453453
}
454454

455+
async fn lua_version(lua: &Lua, extra: Option<&str>) -> Result<String> {
456+
(lua.globals().get("_VERSION")).map(|s: String| s + extra.unwrap_or(""))
457+
}
458+
459+
async fn lua_version_owned(lua: Lua) -> Result<String> {
460+
lua.globals().get("_VERSION")
461+
}
462+
455463
#[cfg(not(any(feature = "lua51", feature = "luau")))]
456464
#[lua(meta)]
457465
async fn __tostring(&self) -> Result<String> {
@@ -483,6 +491,26 @@ mod async_tests {
483491
.unwrap();
484492
}
485493

494+
#[tokio::test]
495+
async fn test_async_lua_param() {
496+
let lua = Lua::new();
497+
lua.globals()
498+
.set("AsyncCounter", lua.create_proxy::<AsyncCounter>().unwrap())
499+
.unwrap();
500+
501+
lua.load(
502+
r#"
503+
local c = AsyncCounter.new()
504+
assert(type(AsyncCounter.lua_version()) == "string")
505+
assert(string.sub(AsyncCounter.lua_version(" extra"), -5) == "extra", "expected 'extra' at the end")
506+
assert(type(AsyncCounter.lua_version_owned()) == "string")
507+
"#,
508+
)
509+
.exec_async()
510+
.await
511+
.unwrap();
512+
}
513+
486514
#[tokio::test]
487515
async fn test_async_consume() {
488516
let lua = Lua::new();

0 commit comments

Comments
 (0)