diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index 0e578e0..ff87bfc 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -7,6 +7,20 @@ use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_ use self::attr::LuaAttr; +/// Wrap registration tokens with any `#[cfg]`/`#[cfg_attr]` attributes from the original item. +pub(crate) fn with_cfg(tokens: proc_macro2::TokenStream, attrs: &[Attribute]) -> proc_macro2::TokenStream { + let cfgs: Vec<_> = (attrs.iter()) + .filter(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr")) + .collect(); + if cfgs.is_empty() { + return tokens; + } + quote! { + #(#cfgs)* + #tokens + } +} + /// Parse all `#[lua(...)]` attributes on a field, merging them into one `LuaAttr`. fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { let mut lua_attr = LuaAttr::default(); @@ -85,17 +99,19 @@ pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream { }; if has_get { - field_registrations.push(quote! { + let tokens = quote! { registry.add_field_method_get(#lua_name, |_lua, this| Ok(this.#field_name.clone())); - }); + }; + field_registrations.push(with_cfg(tokens, &field.attrs)); } if has_set { - field_registrations.push(quote! { + let tokens = quote! { registry.add_field_method_set(#lua_name, |_lua, this, val| { this.#field_name = val; Ok(()) }); - }); + }; + field_registrations.push(with_cfg(tokens, &field.attrs)); } } diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index ca299f1..4dbadc6 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -8,6 +8,7 @@ use syn::{ }; use super::attr::LuaAttr; +use super::with_cfg; /// `&T` reference types that mlua provides as wrapper types via `FromLua`. static BORROW_WRAPPERS: &[(&str, &str)] = &[ @@ -235,13 +236,15 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let const_name = &const_item.ident; let lua_name = lua_attr.name(const_name); if lua_attr.meta { - registration_calls.push(quote! { + let tokens = quote! { registry.add_meta_field(#lua_name, #type_path::#const_name); - }); + }; + registration_calls.push(with_cfg(tokens, &const_item.attrs)); } else { - registration_calls.push(quote! { + let tokens = quote! { registry.add_field(#lua_name, #type_path::#const_name); - }); + }; + registration_calls.push(with_cfg(tokens, &const_item.attrs)); } } ImplItem::Fn(method) => { @@ -273,8 +276,14 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let fn_name = &method.sig.ident; let info = try_compile!(analyze_self_and_args(&method.sig)); + let is_async = method.sig.asyncness.is_some(); if lua_attr.getter { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field getter is not supported") + .to_compile_error() + .into(); + } if !matches!(info.self_kind, SelfKind::Ref(RefKind::Ref)) { return syn::Error::new_spanned(&method.sig, "field getter must take `&self`") .to_compile_error() @@ -288,10 +297,16 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { .to_compile_error() .into(); } - registration_calls.push(gen_field_getter(type_path, fn_name, &lua_attr, &info)); + let tokens = gen_field_getter(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); continue; } if lua_attr.setter { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field setter is not supported") + .to_compile_error() + .into(); + } if !matches!(info.self_kind, SelfKind::Ref(_)) { return syn::Error::new_spanned(&method.sig, "field setter must take `&[mut] self`") .to_compile_error() @@ -305,10 +320,16 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { .to_compile_error() .into(); } - registration_calls.push(gen_field_setter(type_path, fn_name, &lua_attr, &info)); + let tokens = gen_field_setter(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); continue; } if lua_attr.field { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field function is not supported") + .to_compile_error() + .into(); + } if !matches!(info.self_kind, SelfKind::None) { return syn::Error::new_spanned(&method.sig, "field function must not take `self`") .to_compile_error() @@ -316,13 +337,15 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { } let lua_name = lua_attr.name(fn_name); if lua_attr.meta { - registration_calls.push(quote! { - registry.add_meta_field(#lua_name, #type_path::#fn_name()); - }); + let tokens = quote! { + registry.add_meta_field(#lua_name, #type_path::#fn_name); + }; + registration_calls.push(with_cfg(tokens, &method.attrs)); } else { - registration_calls.push(quote! { + let tokens = quote! { registry.add_field(#lua_name, #type_path::#fn_name()); - }); + }; + registration_calls.push(with_cfg(tokens, &method.attrs)); } continue; } @@ -336,11 +359,23 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { .to_compile_error() .into(); } - registration_calls.push(gen_meta(type_path, fn_name, &lua_attr, &info)); + if is_async { + let tokens = gen_async_meta(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } else { + let tokens = gen_meta(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } continue; } - registration_calls.push(gen_regular_method(type_path, fn_name, &lua_attr, &info)); + if is_async { + let tokens = gen_async_regular_method(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } else { + let tokens = gen_regular_method(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } } _ => {} } @@ -417,6 +452,33 @@ fn gen_call_args(info: &MethodInfo) -> TokenStream2 { quote! { #(#call_args),* } } +/// Generate call arguments for invoking the original async method. +fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { + let mut call_args: Vec = Vec::new(); + + match info.self_kind { + SelfKind::None => {} + SelfKind::Ref(RefKind::Ref) => call_args.push(quote! { &this }), + SelfKind::Ref(RefKind::Mut) => call_args.push(quote! { &mut this }), + SelfKind::Owned => call_args.push(quote! { this }), + } + + if info.has_lua { + call_args.push(quote! { lua }); + } + + for arg in &info.args { + let ident = &arg.ident; + match arg.userdata_ref { + Some(RefKind::Ref) => call_args.push(quote! { &*#ident }), + Some(RefKind::Mut) => call_args.push(quote! { &mut *#ident }), + None => call_args.push(quote! { #ident }), + } + } + + quote! { #(#call_args),* } +} + /// Generate the closure params for the registration callback. fn gen_closure_params(info: &MethodInfo) -> TokenStream2 { let destructure = gen_closure_destructure(info); @@ -426,6 +488,16 @@ fn gen_closure_params(info: &MethodInfo) -> TokenStream2 { } } +/// Generate the closure params for an async registration callback. +fn gen_async_closure_params(info: &MethodInfo) -> TokenStream2 { + let destructure = gen_closure_destructure(info); + match info.self_kind { + SelfKind::None => quote! { |lua, #destructure| }, + SelfKind::Ref(RefKind::Mut) => quote! { |lua, mut this, #destructure| }, + _ => quote! { |lua, this, #destructure| }, + } +} + fn gen_field_getter( type_path: &syn::Path, fn_name: &Ident, @@ -549,3 +621,77 @@ fn gen_regular_method( }, } } + +fn gen_async_regular_method( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let fn_path = quote! { #type_path::#fn_name }; + let closure_params = gen_async_closure_params(info); + let call_args = gen_async_call_args(info); + let lua_name = lua_attr.name(fn_name); + + let body = if lua_attr.infallible { + quote! { async move { Ok(#fn_path(#call_args).await) } } + } else { + quote! { async move { #fn_path(#call_args).await } } + }; + match info.self_kind { + SelfKind::Ref(RefKind::Ref) => quote! { + registry.add_async_method(#lua_name, #closure_params #body); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_async_method_mut(#lua_name, #closure_params #body); + }, + SelfKind::Owned => quote! { + registry.add_async_method_once(#lua_name, #closure_params #body); + }, + SelfKind::None => quote! { + registry.add_async_function(#lua_name, #closure_params #body); + }, + } +} + +fn gen_async_meta( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let meta_name = match lua_attr.effective_meta_name(fn_name) { + Ok(name) => name, + Err(err) => return err.to_compile_error(), + }; + let closure_params = if matches!(info.self_kind, SelfKind::None) { + if info.args.is_empty() { + quote! { |lua, _this: ::mlua::AnyUserData| } + } else { + let idents: Vec<_> = info.args.iter().map(|a| &a.ident).collect(); + let types: Vec<_> = info.args.iter().map(|a| &a.callback_type).collect(); + quote! { |lua, (_this, #(#idents),*): (::mlua::AnyUserData, #(#types),*) | } + } + } else { + gen_async_closure_params(info) + }; + let call_args = gen_async_call_args(info); + let fn_path = quote! { #type_path::#fn_name }; + + let body = if lua_attr.infallible { + quote! { async move { Ok(#fn_path(#call_args).await) } } + } else { + quote! { async move { #fn_path(#call_args).await } } + }; + match info.self_kind { + SelfKind::None => quote! { + registry.add_async_meta_function(#meta_name, #closure_params #body); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_async_meta_method_mut(#meta_name, #closure_params #body); + }, + _ => quote! { + registry.add_async_meta_method(#meta_name, #closure_params #body); + }, + } +} diff --git a/tests/compile.rs b/tests/compile.rs index 50f70f1..64d2fff 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -36,4 +36,11 @@ fn test_compilation() { t.compile_fail("tests/compile/userdata_meta_owned_self.rs"); t.compile_fail("tests/compile/userdata_const_getter.rs"); } + + #[cfg(all(feature = "macros", feature = "async"))] + { + t.compile_fail("tests/compile/userdata_getter_async.rs"); + t.compile_fail("tests/compile/userdata_setter_async.rs"); + t.compile_fail("tests/compile/userdata_field_async.rs"); + } } diff --git a/tests/compile/async_any_userdata_method.stderr b/tests/compile/async_any_userdata_method.stderr index 3e01c45..116b64f 100644 --- a/tests/compile/async_any_userdata_method.stderr +++ b/tests/compile/async_any_userdata_method.stderr @@ -1,14 +1,18 @@ error[E0596]: cannot borrow `s` as mutable, as it is a captured variable in a `Fn` closure --> tests/compile/async_any_userdata_method.rs:9:49 | - 8 | let mut s = &s; - | ----- `s` declared here, outside the closure - 9 | reg.add_async_method("t", |_, this, ()| async { - | ------------- ^^^^^ cannot borrow as mutable - | | - | in this closure -10 | s = &*this; - | - mutable borrow occurs due to use of `s` in closure + 8 | let mut s = &s; + | ----- `s` declared here, outside the closure + 9 | reg.add_async_method("t", |_, this, ()| async { + | - ------------- ^^^^^ cannot borrow as mutable + | | | + | _____________| in this closure + | | +10 | | s = &*this; + | | - mutable borrow occurs due to use of `s` in closure +11 | | Ok(()) +12 | | }); + | |__________- expects `Fn` instead of `FnMut` error[E0373]: async block may outlive the current function, but it borrows `this`, which is owned by the current function --> tests/compile/async_any_userdata_method.rs:9:49 diff --git a/tests/compile/lua_norefunwindsafe.stderr b/tests/compile/lua_norefunwindsafe.stderr index 2b3e664..e2807c4 100644 --- a/tests/compile/lua_norefunwindsafe.stderr +++ b/tests/compile/lua_norefunwindsafe.stderr @@ -50,6 +50,16 @@ error[E0277]: the type `UnsafeCell` may contain interio | = help: the trait `RefUnwindSafe` is not implemented for `UnsafeCell` = note: required for `Rc>` to implement `RefUnwindSafe` +note: required because it appears within the type `MaybeDangling>>` + --> $RUST/core/src/mem/maybe_dangling.rs + | + | pub struct MaybeDangling(P); + | ^^^^^^^^^^^^^ +note: required because it appears within the type `ManuallyDrop>>` + --> $RUST/core/src/mem/manually_drop.rs + | + | pub struct ManuallyDrop { + | ^^^^^^^^^^^^ note: required because it appears within the type `mlua::state::RawLua` --> src/state/raw.rs | diff --git a/tests/compile/ref_nounwindsafe.stderr b/tests/compile/ref_nounwindsafe.stderr index 07613de..0699059 100644 --- a/tests/compile/ref_nounwindsafe.stderr +++ b/tests/compile/ref_nounwindsafe.stderr @@ -120,6 +120,16 @@ error[E0277]: the type `UnsafeCell` may contain interio | = help: the trait `RefUnwindSafe` is not implemented for `UnsafeCell` = note: required for `Rc>` to implement `RefUnwindSafe` +note: required because it appears within the type `MaybeDangling>>` + --> $RUST/core/src/mem/maybe_dangling.rs + | + | pub struct MaybeDangling(P); + | ^^^^^^^^^^^^^ +note: required because it appears within the type `ManuallyDrop>>` + --> $RUST/core/src/mem/manually_drop.rs + | + | pub struct ManuallyDrop { + | ^^^^^^^^^^^^ note: required because it appears within the type `mlua::state::RawLua` --> src/state/raw.rs | diff --git a/tests/compile/userdata_field_async.rs b/tests/compile/userdata_field_async.rs new file mode 100644 index 0000000..004b412 --- /dev/null +++ b/tests/compile/userdata_field_async.rs @@ -0,0 +1,15 @@ +use mlua::Result; + +#[derive(Clone, Debug)] +#[mlua::userdata] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + async fn description() -> Result { + Ok("foo".into()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_field_async.stderr b/tests/compile/userdata_field_async.stderr new file mode 100644 index 0000000..13d7c68 --- /dev/null +++ b/tests/compile/userdata_field_async.stderr @@ -0,0 +1,13 @@ +error: async field function is not supported + --> tests/compile/userdata_field_async.rs:10:5 + | +10 | async fn description() -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_field_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/compile/userdata_getter_async.rs b/tests/compile/userdata_getter_async.rs new file mode 100644 index 0000000..3591f0b --- /dev/null +++ b/tests/compile/userdata_getter_async.rs @@ -0,0 +1,15 @@ +use mlua::Result; + +#[derive(Clone, Debug)] +#[mlua::userdata] +struct Foo(u64); + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + async fn value(&self) -> Result { + Ok(self.0) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_async.stderr b/tests/compile/userdata_getter_async.stderr new file mode 100644 index 0000000..8b8866e --- /dev/null +++ b/tests/compile/userdata_getter_async.stderr @@ -0,0 +1,13 @@ +error: async field getter is not supported + --> tests/compile/userdata_getter_async.rs:10:5 + | +10 | async fn value(&self) -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_getter_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/compile/userdata_setter_async.rs b/tests/compile/userdata_setter_async.rs new file mode 100644 index 0000000..4d50e96 --- /dev/null +++ b/tests/compile/userdata_setter_async.rs @@ -0,0 +1,16 @@ +use mlua::Result; + +#[derive(Clone, Debug)] +#[mlua::userdata] +struct Foo(u64); + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + async fn set_value(&mut self, val: u64) -> Result<()> { + self.0 = val; + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_async.stderr b/tests/compile/userdata_setter_async.stderr new file mode 100644 index 0000000..46ac54b --- /dev/null +++ b/tests/compile/userdata_setter_async.stderr @@ -0,0 +1,13 @@ +error: async field setter is not supported + --> tests/compile/userdata_setter_async.rs:10:5 + | +10 | async fn set_value(&mut self, val: u64) -> Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_setter_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index 843da7f..b0c5e85 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -372,3 +372,118 @@ fn test_known_borrow_wrappers() -> Result<()> { .unwrap(); Ok(()) } + +#[cfg(feature = "async")] +mod async_tests { + use mlua::{Lua, Result}; + + #[derive(Clone, Debug)] + #[mlua::userdata] + struct AsyncCounter(u64); + + #[mlua::userdata_impl] + impl AsyncCounter { + #[lua(infallible)] + fn new() -> Self { + AsyncCounter(0) + } + + async fn get_value(&self) -> Result { + Ok(self.0) + } + + async fn set_value(&mut self, value: u64) -> Result<()> { + self.0 = value; + Ok(()) + } + + async fn take_value(self) -> Result { + Ok(self.0) + } + + #[lua(infallible)] + async fn get_value_infallible(&self) -> u64 { + self.0 + } + + async fn multiply(&self, factor: u64) -> Result { + Ok(self.0 * factor) + } + + async fn default_value() -> Result { + Ok(42) + } + + #[cfg(not(any(feature = "lua51", feature = "luau")))] + #[lua(meta)] + async fn __tostring(&self) -> Result { + Ok(format!("Counter({})", self.0)) + } + } + + #[tokio::test] + async fn test_async_methods() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(10) + local val = c:get_value() + assert(val == 10, "expected 10, got " .. tostring(val)) + local doubled = c:multiply(3) + assert(doubled == 30, "expected 30, got " .. tostring(doubled)) + local inf = c:get_value_infallible() + assert(inf == 10, "expected infallible 10, got " .. tostring(inf)) + "#, + ) + .exec_async() + .await + .unwrap(); + } + + #[tokio::test] + async fn test_async_consume() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(42) + local val = c:take_value() + assert(val == 42) + local ok, err = pcall(function() c:get_value() end) + assert(not ok and tostring(err):match("userdata has been destructed")) + "#, + ) + .exec_async() + .await + .unwrap(); + } + + #[cfg(not(any(feature = "lua51", feature = "luau")))] + #[tokio::test] + async fn test_async_meta() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(7) + assert(tostring(c) == "Counter(7)") + "#, + ) + .exec_async() + .await + .unwrap(); + } +}