From 1ad95928f4d6ed5d8217f09c997ae259bf8806a1 Mon Sep 17 00:00:00 2001 From: Zachary Boehm Date: Mon, 20 Apr 2026 17:14:37 -0500 Subject: [PATCH 1/3] [feat] add UserData impl proc-macro --- Cargo.lock | 173 ++++++++++++- Cargo.toml | 4 + examples/enum_and_tuple.rs | 2 +- examples/macros.rs | 72 ++++++ mlua_extras_derive/Cargo.lock | 183 ++++++++++++++ mlua_extras_derive/Cargo.toml | 2 + mlua_extras_derive/src/extract.rs | 387 +++++++++++++++++++++++++++++ mlua_extras_derive/src/lib.rs | 42 ++-- mlua_extras_derive/src/methods.rs | 196 +++++++++++++++ mlua_extras_derive/src/userdata.rs | 211 ++++++++++++++++ src/lib.rs | 10 +- 11 files changed, 1252 insertions(+), 30 deletions(-) create mode 100644 examples/macros.rs create mode 100644 mlua_extras_derive/src/extract.rs create mode 100644 mlua_extras_derive/src/methods.rs create mode 100644 mlua_extras_derive/src/userdata.rs diff --git a/Cargo.lock b/Cargo.lock index e5c82ad..6b34e5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.3.0" @@ -51,6 +57,81 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "deluxe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" +dependencies = [ + "deluxe-core", + "deluxe-macros", + "once_cell", + "proc-macro2", + "syn 2.0.87", +] + +[[package]] +name = "deluxe-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" +dependencies = [ + "arrayvec", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.87", +] + +[[package]] +name = "deluxe-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +dependencies = [ + "deluxe-core", + "heck 0.4.1", + "if_chain", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "either" version = "1.13.0" @@ -63,6 +144,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "erased-serde" version = "0.4.5" @@ -132,12 +219,46 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itertools" version = "0.14.0" @@ -248,6 +369,8 @@ dependencies = [ name = "mlua-extras-derive" version = "11.6.0" dependencies = [ + "darling", + "deluxe", "proc-macro-error", "proc-macro2", "quote", @@ -350,6 +473,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -550,6 +683,18 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.2" @@ -565,7 +710,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.87", @@ -605,6 +750,23 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "typeid" version = "1.0.2" @@ -732,6 +894,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/Cargo.toml b/Cargo.toml index 80ceef1..617e27b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ mlua-extras-derive = { path = "./mlua_extras_derive", version = "11.6.0", option mlua = { version = "0.11.6", optional = true, default-features = false } strum = { version = "0.27.2", features = ["derive"], default-features = false } +[[example]] +name = "macros" +required-features = ["mlua", "derive"] + [[example]] name = "enum_and_tuple" required-features = ["mlua"] diff --git a/examples/enum_and_tuple.rs b/examples/enum_and_tuple.rs index 6cf5f75..9e7b401 100644 --- a/examples/enum_and_tuple.rs +++ b/examples/enum_and_tuple.rs @@ -45,7 +45,7 @@ impl TypedUserData for Kind { }); fields - .coerce(Type::named("KindEnum")) + .coerce(Type::named("KindVariant")) .add_field_function_get("_variant", |_lua, this| { match *this.borrow::().unwrap() { Self::A(_) => Ok("A"), diff --git a/examples/macros.rs b/examples/macros.rs new file mode 100644 index 0000000..2a5cede --- /dev/null +++ b/examples/macros.rs @@ -0,0 +1,72 @@ +use mlua::{IntoLua, FromLua, Lua, StdLib}; +use mlua_extras::{UserData, user_data_impl}; + +#[derive(Clone, UserData)] +struct Data { + #[field(rename = 1)] + name: String +} + +#[user_data_impl] +impl Data { + #[method] + fn get_data(&self) -> mlua::Result { + Ok(self.name.clone()) + } + + /// This method is called first, if it returns a nil/none value then + /// the auto implementation will fallback to it's implementation. + /// + /// # Example + /// + /// If this function returned a value for `1` then the auto impl with + /// return that value. If this function returns nil then the auto impl + /// will return the value for `self.name` since it is exposed at index `1` + #[metamethod(Index)] + fn index(&self, lua: &Lua, idx: isize) -> mlua::Result { + match idx { + -1 => "TESTING".into_lua(lua), + _ => Ok(mlua::Value::Nil) + } + } + + + /// This method is called first, if it returns an error value then + /// the auto implementation will fallback to it's implementation. + /// + /// # Example + /// + /// If this function returned `Ok(())` for an index of `1` then the auto impl + /// would just return that to the runtime. However, if this function returns + /// an error value then the auto impl will attempt to use it's impl. If the + /// auto impl doesn't now the index then the original error is returned. + #[metamethod(NewIndex)] + fn new_index(&mut self, lua: &Lua, idx: isize, value: mlua::Value) -> mlua::Result<()> { + match idx { + 1 => self.name = ::from_lua(value, lua)?, + // It is recommended to return some sort of error from this implementation. + // + // This enforces strict indexing into userdata types and tells the auto impl + // to fallback to it's implementation. If the internal implementation also fails + // then the original error from this impl is returned to the lua runtime. + _ => return Err(mlua::Error::runtime(format!("invalid index '{idx}'"))) + } + Ok(()) + } +} + +fn main() -> mlua::Result<()> { + let lua = unsafe { Lua::unsafe_new_with(StdLib::ALL, Default::default()) }; + + lua.globals().set("data", Data { name: "MluaExtras".into() })?; + + lua.load(" + print('Index [1]:', data[1]) + data[1] = 'HelloWorld' + print('Set data[1] to \\'HelloWorld\\'') + print('Get Data:', data:get_data()) + print('Index [-1]:', data[-1]) + ").exec()?; + + Ok(()) +} diff --git a/mlua_extras_derive/Cargo.lock b/mlua_extras_derive/Cargo.lock index e79a8b5..8e01610 100644 --- a/mlua_extras_derive/Cargo.lock +++ b/mlua_extras_derive/Cargo.lock @@ -2,10 +2,139 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.77", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "deluxe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" +dependencies = [ + "deluxe-core", + "deluxe-macros", + "once_cell", + "proc-macro2", + "syn 2.0.77", +] + +[[package]] +name = "deluxe-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" +dependencies = [ + "arrayvec", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.77", +] + +[[package]] +name = "deluxe-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +dependencies = [ + "deluxe-core", + "heck", + "if_chain", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "if_chain" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "mlua-extras-derive" version = "11.6.0" dependencies = [ + "darling", + "deluxe", "proc-macro-error", "proc-macro2", "quote", @@ -13,6 +142,22 @@ dependencies = [ "venial", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -55,6 +200,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -76,6 +233,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.13" @@ -97,3 +271,12 @@ name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/mlua_extras_derive/Cargo.toml b/mlua_extras_derive/Cargo.toml index 2e260f5..6076f82 100644 --- a/mlua_extras_derive/Cargo.toml +++ b/mlua_extras_derive/Cargo.toml @@ -14,6 +14,8 @@ repository = "https://github.com/Tired-Fox/mlua-extras/tree/main/mlua_extras_der keywords = ["lua", "types", "mlua", "macros", "derive"] [dependencies] +darling = "0.23.0" +deluxe = "0.5.0" proc-macro-error = "1.0.4" proc-macro2 = "1.0.86" quote = "1.0.37" diff --git a/mlua_extras_derive/src/extract.rs b/mlua_extras_derive/src/extract.rs new file mode 100644 index 0000000..a56674f --- /dev/null +++ b/mlua_extras_derive/src/extract.rs @@ -0,0 +1,387 @@ +use darling::FromMeta; +use deluxe::{ParseAttributes, ParseMetaItem}; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + spanned::Spanned, Attribute, Expr, ExprLit, FnArg, ImplItemFn, Lit, Meta, MetaNameValue, Pat, + PatIdent, ReturnType, +}; + +pub fn doc_comment(attrs: &[syn::Attribute]) -> Option { + let docs: Vec = attrs + .iter() + .filter_map(|attr| { + if !attr.path().is_ident("doc") { + return None; + } + if let Meta::NameValue(nv) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &nv.value { + if let Lit::Str(s) = &expr_lit.lit { + return Some(s.value().trim().to_string()); + } + } + } + None + }) + .collect(); + + if docs.is_empty() { + None + } else { + Some(docs.join("\n")) + } +} + +fn docs(attrs: Vec) -> darling::Result> { + let docs = attrs + .into_iter() + .filter_map(|Attribute { meta, .. }| { + if let Meta::NameValue(MetaNameValue { path, value, .. }) = &meta { + if path.is_ident("doc") { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) = &value + { + return Some(lit.value().trim().to_string()); + } + } + } + None + }) + .collect::>(); + + Ok((!docs.is_empty()).then_some(docs.join("\n"))) +} + +#[derive(Debug, darling::FromField)] +#[darling(attributes(field), forward_attrs(doc))] +pub struct UserDataField { + pub ident: Option, + pub ty: syn::Type, + + #[allow(dead_code)] + #[darling(with = "docs")] + pub attrs: Option, + + #[darling(default)] + pub skip: bool, + #[darling(default)] + pub readonly: bool, + #[darling(default)] + pub writeonly: bool, + #[darling(default)] + pub rename: Option, +} + +#[derive(Debug)] +pub enum PassBy { + Ref, + RefMut, + Value, +} +impl PassBy { + fn from_fn_arg(value: Option<&FnArg>) -> Option { + match value { + Some(FnArg::Receiver(recv)) => { + if recv.reference.is_some() { + if recv.mutability.is_some() { + Some(PassBy::RefMut) + } else { + Some(PassBy::Ref) + } + } else { + Some(PassBy::Value) + } + } + Some(FnArg::Typed(typed)) => { + if let Pat::Ident(PatIdent { + ident, + by_ref, + mutability, + .. + }) = &*typed.pat + { + if ident == "self" { + if by_ref.is_some() { + if mutability.is_some() { + Some(PassBy::RefMut) + } else { + Some(PassBy::Ref) + } + } else { + Some(PassBy::Value) + } + } else { + None + } + } else { + None + } + } + _ => None, + } + } +} + +#[derive(Debug)] +pub enum MethodKind { + Regular, + Meta, +} +impl MethodKind { + pub fn is_meta(&self) -> bool { + match self { + Self::Regular => false, + Self::Meta => true, + } + } +} + +#[derive(Debug, ParseAttributes)] +#[deluxe(attributes(metamethod))] +struct MetaMethod(IdentOrCustom); + +#[derive(Default, Debug, ParseAttributes)] +#[deluxe(default, attributes(method))] +struct Method { + rename: Option, +} + +pub fn is_method_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("method") +} + +pub fn is_metamethod_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("metamethod") +} + +#[derive(Debug)] +pub struct UserDataMethod { + #[allow(dead_code)] + pub doc: Option, + pub r#async: bool, + pub name: syn::Ident, + pub lua_name: TokenStream, + pub instance: Option, + pub lua: bool, + pub params: Vec<(syn::Ident, syn::Type)>, + pub fallible: bool, + pub returnable: bool, + pub kind: MethodKind, +} +impl UserDataMethod { + pub fn from_imp_fn(method: &ImplItemFn) -> Option { + let method_attr = method + .attrs + .iter() + .find(|a| is_method_attr(a)) + .map(|a| deluxe::parse_attributes::<_, Method>(a).unwrap_or_default()); + let metamethod_attr = match method + .attrs + .iter() + .find(|a| is_metamethod_attr(a)) + { + Some(a) => match deluxe::parse_attributes::<_, MetaMethod>(a) { + Ok(v) => Some(v), + Err(err) => proc_macro_error::abort!(method.span(), "{}", err), + }, + None => None, + }; + + if method_attr.is_some() && metamethod_attr.is_some() { + return None; + } + + let fn_name = method.sig.ident.clone(); + let is_async = method.sig.asyncness.is_some(); + let instance = PassBy::from_fn_arg(method.sig.inputs.first()); + let doc = doc_comment(&method.attrs); + + let (lua_name, kind) = if let Some(Method { rename }) = method_attr { + let name = rename.unwrap_or_else(|| fn_name.to_string()); + (quote!(#name), MethodKind::Regular) + } else if let Some(MetaMethod(target)) = metamethod_attr { + if (target.is_ident() && target == "Index") || (target == "__index") { + let replace = "__usr_index"; + (quote!(#replace), MethodKind::Meta) + } else if (target.is_ident() && target == "NewIndex") || (target == "__newindex") { + let replace = "__usr_newindex"; + (quote!(#replace), MethodKind::Meta) + } else { + (quote!(#target), MethodKind::Meta) + } + } else { + return None; + }; + + // Check for async metamethod conflict + if is_async { + if kind.is_meta() { + proc_macro_error::abort!( + method.sig.asyncness, + "async metamethods are not supported by mlua" + ); + } + } + + // Collect non-self parameters + let mut params_iter = method.sig.inputs.iter().peekable(); + + // Skip the instance arg if present + match &instance { + Some(_) => { + params_iter.next(); + } // skip self + None => { + // Check if the first arg is a typed `self` pattern + if let Some(FnArg::Typed(pat_type)) = method.sig.inputs.first() { + if let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat { + if ident == "self" { + params_iter.next(); + } + } + } + } + } + + // Detect lua parameter (first param named "lua") + let mut has_lua: bool = false; + if let Some(FnArg::Typed(pat_type)) = params_iter.peek() { + if let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat { + if ident == "lua" { + has_lua = true; + params_iter.next(); + } + } + } + + let params: Vec<_> = params_iter + .filter_map(|arg| { + if let FnArg::Typed(pat_type) = arg { + if let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat { + return Some((ident.clone(), (*pat_type.ty).clone())); + } + } + None + }) + .collect(); + + // Analyze return type + let (is_fallible, has_return) = match &method.sig.output { + ReturnType::Default => (false, false), + ReturnType::Type(_, ty) => { + let result_type = match &**ty { + syn::Type::Path(path) => { + if let Some(last) = path.path.segments.last() { + last.ident == "Result" + } else { + false + } + } + _ => false, + }; + + if result_type { + (true, true) + } else { + // Check if it's () type + let is_unit = matches!(&**ty, syn::Type::Tuple(t) if t.elems.is_empty()); + (false, !is_unit) + } + } + }; + + Some(Self { + doc, + r#async: is_async, + name: fn_name, + instance, + lua: has_lua, + lua_name, + params, + fallible: is_fallible, + returnable: has_return, + kind, + }) + } +} + +#[derive(Debug, Clone)] +pub enum IdentOrCustom { + Ident(syn::Ident), + Custom(String), +} +impl IdentOrCustom { + pub fn is_ident(&self) -> bool { + match self { + Self::Ident(_) => true, + _ => false + } + } +} +impl PartialEq<&str> for IdentOrCustom { + fn eq(&self, other: &&str) -> bool { + match self { + Self::Custom(v) => v == *other, + Self::Ident(v) => v == *other, + } + } +} +impl ParseMetaItem for IdentOrCustom { + fn parse_meta_item(input: syn::parse::ParseStream, _mode: deluxe::ParseMode) -> deluxe::Result { + if input.peek(syn::LitStr) { + let lit: syn::LitStr = input.parse()?; + Ok(IdentOrCustom::Custom(lit.value())) + } else { + let ident: syn::Ident = input.parse()?; + Ok(IdentOrCustom::Ident(ident)) + } + } +} +impl ToTokens for IdentOrCustom { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Ident(ident) => quote!(mlua_extras::mlua::MetaMethod::#ident).to_tokens(tokens), + Self::Custom(v) => v.to_tokens(tokens), + } + } +} + +#[derive(Debug, Clone)] +pub enum Index { + Int(isize), + Str(String), +} +impl std::fmt::Display for Index { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Int(v) => write!(f, "{v}"), + Self::Str(s) => write!(f, "{s}"), + } + } +} + +impl FromMeta for Index { + fn from_meta(item: &Meta) -> darling::Result { + if let syn::Meta::NameValue(MetaNameValue { + value: Expr::Lit(ExprLit { lit, .. }), + .. + }) = item + { + match lit { + Lit::Str(s) => return Ok(Self::Str(s.value())), + Lit::Int(i) => return Ok(Self::Int(i.base10_parse()?)), + _ => (), + } + } + Err(darling::Error::custom("Expected string or integer literal")) + } +} +impl ToTokens for Index { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Int(i) => i.to_tokens(tokens), + Self::Str(s) => s.to_tokens(tokens), + } + } +} diff --git a/mlua_extras_derive/src/lib.rs b/mlua_extras_derive/src/lib.rs index 0010294..7746c11 100644 --- a/mlua_extras_derive/src/lib.rs +++ b/mlua_extras_derive/src/lib.rs @@ -7,38 +7,26 @@ use proc_macro_error::{proc_macro_error, abort}; use syn::spanned::Spanned; use venial::{Item, parse_item}; +mod methods; +mod userdata; +pub(crate) mod extract; + #[proc_macro_error] -#[proc_macro_derive(UserData)] +#[proc_macro_derive(UserData, attributes(field))] pub fn derive_user_data(input: TokenStream) -> TokenStream { - let input = TokenStream2::from(input); - let name = match parse_item(input.clone()) { - Ok(Item::Struct(struct_type)) => { - struct_type.name.clone() - }, - Ok(Item::Enum(enum_type)) => { - enum_type.name.clone() - }, - Err(err) => abort!(err.span(), "{}", err), - _ => abort!(input.span(), "only `struct` and `enum` types are supported for TypedUserData") - }; - - quote!( - impl mlua_extras::mlua::UserData for #name { - fn add_fields>(fields: &mut F) { - let mut wrapper = mlua_extras::typed::WrappedBuilder::new(fields); - <#name as mlua_extras::typed::TypedUserData>::add_fields(&mut wrapper); - } + let input = syn::parse_macro_input!(input as syn::DeriveInput); + userdata::derive(input).into() +} - fn add_methods>(methods: &mut M) { - let mut wrapper = mlua_extras::typed::WrappedBuilder::new(methods); - <#name as mlua_extras::typed::TypedUserData>::add_methods(&mut wrapper); - } - } - ).into() +#[proc_macro_error] +#[proc_macro_attribute] +pub fn user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item = syn::parse_macro_input!(item as syn::ItemImpl); + methods::derive(item).into() } #[proc_macro_error] -#[proc_macro_derive(Typed, attributes(typed))] +#[proc_macro_derive(Typed)] pub fn derive_typed(input: TokenStream) -> TokenStream { let input = TokenStream2::from(input); match parse_item(input.clone()) { @@ -84,7 +72,7 @@ pub fn derive_typed(input: TokenStream) -> TokenStream { }) .collect::>(); - let enum_alt = format!("{label}Enum"); + let enum_alt = format!("{label}Variant"); // TODO: This should be a union alias quote!( impl mlua_extras::typed::Typed for #name { diff --git a/mlua_extras_derive/src/methods.rs b/mlua_extras_derive/src/methods.rs new file mode 100644 index 0000000..278f06f --- /dev/null +++ b/mlua_extras_derive/src/methods.rs @@ -0,0 +1,196 @@ +use proc_macro2::TokenStream; +use syn::{ImplItem, ItemImpl, Type}; +use quote::quote; + +use crate::extract::{PassBy, UserDataMethod}; + +pub fn derive(item: ItemImpl) -> TokenStream { + let self_ty = &item.self_ty; + + let mut user_data = Vec::new(); + let mut cleaned_items = Vec::new(); + + for impl_item in &item.items { + match impl_item { + ImplItem::Fn(method) => if let Some(udm) = UserDataMethod::from_imp_fn(method) { + user_data.push(udm); + + let mut cleaned = method.clone(); + cleaned.attrs.retain(|a| !is_method_attr(a) && !is_metamethod_attr(a)); + cleaned_items.push(ImplItem::Fn(cleaned)); + } else { + cleaned_items.push(impl_item.clone()); + } + _ => { + cleaned_items.push(impl_item.clone()); + } + } + } + + let registrations: Vec<_> = user_data + .iter() + .map(|info| generate_registration(info, self_ty)) + .collect(); + + // Reconstruct the cleaned impl block + let attrs = &item.attrs; + let unsafety = &item.unsafety; + let impl_token = &item.impl_token; + let generics = &item.generics; + + quote! { + #(#attrs)* + #unsafety #impl_token #generics #self_ty { + #(#cleaned_items)* + } + + impl #generics #self_ty { + #[doc(hidden)] + fn __auto_add_methods>(methods: &mut M) { + #(#registrations)* + } + } + } +} + +fn is_method_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("method") +} + +fn is_metamethod_attr(attr: &syn::Attribute) -> bool { + attr.path().is_ident("metamethod") +} + +fn generate_registration(info: &UserDataMethod, self_ty: &Type) -> TokenStream { + let fn_name = &info.name; + let lua_name = &info.lua_name; + + let param_names: Vec<_> = info.params.iter().map(|(name, _)| name).collect(); + let param_types: Vec<_> = info.params.iter().map(|(_, ty)| ty).collect(); + + // Build the parameter destructuring for the closure + let params_destructure = if param_names.is_empty() { + quote! { _: () } + } else { + quote! { (#(#param_names,)*): (#(#param_types,)*) } + }; + + // Build the method call arguments + let call_args = if info.lua { + let args = ¶m_names; + quote! { lua, #(#args,)* } + } else { + let args = ¶m_names; + quote! { #(#args,)* } + }; + + let lua_ident = if info.lua { + quote! { lua } + } else { + quote! { _lua } + }; + + // Build the method call and return wrapping + let build_call_and_return = |call: TokenStream| -> TokenStream { + if info.fallible { + quote! { #call.map_err(|e| e.into()) } + } else if info.returnable { + quote! { + let result = #call; + Ok(result) + } + } else { + quote! { + #call; + Ok(()) + } + } + }; + + let is_meta = info.kind.is_meta(); + + if info.r#async { + // Async methods + match &info.instance { + Some(PassBy::Ref|PassBy::Value) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + quote! { + methods.add_async_method(#lua_name, |#lua_ident, this, #params_destructure| async move { + #body + }); + } + } + Some(PassBy::RefMut) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args).await }); + quote! { + methods.add_async_method_mut(#lua_name, |#lua_ident, this, #params_destructure| async move { + #body + }); + } + } + None => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args).await }); + quote! { + methods.add_async_function(#lua_name, |#lua_ident, #params_destructure| async move { + #body + }); + } + } + } + } else { + // Sync methods + match (&info.instance, is_meta) { + (Some(PassBy::Ref|PassBy::Value), false) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + methods.add_method(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::Ref|PassBy::Value), true) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + methods.add_meta_method(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::RefMut), false) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + methods.add_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (Some(PassBy::RefMut), true) => { + let body = build_call_and_return(quote! { this.#fn_name(#call_args) }); + quote! { + methods.add_meta_method_mut(#lua_name, |#lua_ident, this, #params_destructure| { + #body + }); + } + } + (None, false) => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + quote! { + methods.add_function(#lua_name, |#lua_ident, #params_destructure| { + #body + }); + } + } + (None, true) => { + let body = + build_call_and_return(quote! { #self_ty::#fn_name(#call_args) }); + quote! { + methods.add_meta_function(#lua_name, |#lua_ident, #params_destructure| { + #body + }); + } + } + } + } +} \ No newline at end of file diff --git a/mlua_extras_derive/src/userdata.rs b/mlua_extras_derive/src/userdata.rs new file mode 100644 index 0000000..fd2ead5 --- /dev/null +++ b/mlua_extras_derive/src/userdata.rs @@ -0,0 +1,211 @@ +use darling::FromField; +use proc_macro::Literal; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use syn::{Data, DeriveInput, Fields, LitInt, spanned::Spanned}; +use quote::quote; + +use crate::extract::{Index, UserDataField}; + +pub fn derive(input: DeriveInput) -> TokenStream2 { + let name = &input.ident; + + let fields: Vec<_> = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(named) => named.named.iter().collect(), + Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(), + Fields::Unit => Vec::new(), + }, + Data::Enum(_) => { + proc_macro_error::abort!(name, "TypedUserData does not support enums"); + } + Data::Union(_) => { + proc_macro_error::abort!(name, "TypedUserData does not support unions"); + } + }; + + let user_fields = fields + .iter() + .map(|field| { + match UserDataField::from_field(field) { + Ok(uf) => uf, + Err(err) => proc_macro_error::abort!(field, "{}", err) + } + }) + .collect::>(); + + let field_registrations = user_fields + .iter() + .enumerate() + .filter_map(|(i, fi)| { + let (index, field_ident) = match (&fi.rename, &fi.ident) { + (None, None) | (Some(Index::Int(_)), _) => return None, + (Some(Index::Str(v)), _) => (Index::Str(v.clone()), match &fi.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::usize_unsuffixed(i).to_string(), fi.ty.span()); + quote!(#i) + } + }), + (None, Some(ident)) => (Index::Str(ident.to_string()), quote!(#ident)), + }; + + let field_ty = &fi.ty; + + match (fi.skip, fi.readonly, fi.writeonly) { + (true, _, _) => None, + (_, true, true) | (_, false, false) => Some(quote! { + mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( + fields, + #index, + |_lua, this| Ok(this.#field_ident.clone()), + |_lua, this, val: #field_ty| { this.#field_ident = val; Ok(()) }, + ); + }), + (_, true, false) => Some(quote! { + fields.add_field_method_get( + #index, + |_lua, this| Ok(this.#field_ident.clone()), + ); + }), + (_, false, true) => Some(quote! { + fields.add_field_method_set( + #index, + |_lua, this, val: #field_ty| { this.#field_ident = val; Ok(()) }, + ); + }), + } + }); + + let mut method_registrations = Vec::::new(); + + // Add a custom __index and __newindex for the tuple struct/enum fields + // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] + { + let indexes = user_fields + .iter() + .enumerate() + .filter_map(|(i, f)| { + match f { + UserDataField { skip: true, .. } + | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } + | UserDataField { rename: Some(Index::Str(_)), .. } + | UserDataField { readonly: false, writeonly: true, .. } => None, + UserDataField { readonly: true, writeonly: true, .. } + | UserDataField { readonly: false, writeonly: false, .. } + | UserDataField { readonly: true, writeonly: false, .. } => { + let idx = match &f.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); + quote!(#i) + } + }; + let lua_idx = LitInt::new(&Literal::isize_unsuffixed(match f.rename { + Some(Index::Int(v)) => v, + _ => i as isize + 1 + }).to_string(), Span::call_site()); + + Some(quote!(Some(#lua_idx) => return mlua_extras::mlua::IntoLua::into_lua(this.#idx.clone(), lua),)) + }, + } + }).collect::>(); + + let new_indexes = user_fields + .iter() + .enumerate() + .filter_map(|(i, f)| { + match f { + UserDataField { skip: true, .. } + | UserDataField { ident: Some(_), rename: None|Some(Index::Str(_)), .. } + | UserDataField { rename: Some(Index::Str(_)), .. } + | UserDataField { readonly: true, writeonly: false, .. } => None, + UserDataField { readonly: true, writeonly: true, .. } + | UserDataField { readonly: false, writeonly: false, .. } + | UserDataField { readonly: false, writeonly: true, .. } => { + let idx = match &f.ident { + Some(i) => quote!(#i), + None => { + let i = LitInt::new(&Literal::isize_unsuffixed(i as isize).to_string(), Span::call_site()); + quote!(#i) + } + }; + let lua_idx = Some(LitInt::new(&Literal::isize_unsuffixed(match f.rename { + Some(Index::Int(v)) => v, + _ => i as isize + 1 + }).to_string(), Span::call_site())); + let ty = &f.ty; + + Some(quote!(Some(#lua_idx) => this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(value, lua)?,)) + }, + } + }).collect::>(); + + method_registrations.push(quote!{ + methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |lua, (this, idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + let metatable = this.metatable()?; + + if let Ok(usr) = metatable.get::("__usr_index") { + match usr.call::((this.clone(), idx.clone()))? { + mlua_extras::mlua::Value::Nil => (), + other => return Ok(other) + }; + } + + let this = this.borrow::()?; + match idx.as_integer() { + #(#indexes)* + _ => Ok(mlua_extras::mlua::Value::Nil) + } + }); + + methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |lua, (this, idx, value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { + let metatable = this.metatable()?; + + let mut error = None; + if let Ok(usr) = metatable.get::("__usr_newindex") { + match usr.call::>((this.clone(), idx.clone(), value.clone())) { + Ok(v) => return Ok(()), + Err(err) => error = Some(err), + } + } + + let mut this = this.borrow_mut::()?; + match idx.as_integer() { + #(#new_indexes)* + _ => return Err(error.unwrap_or(mlua_extras::mlua::Error::runtime(match idx { + mlua_extras::mlua::Value::Integer(i) => format!("invalid index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("invalid index '{}'", s.to_string_lossy()), + _ => "invalid index".into() + }))) + } + Ok(()) + }); + }); + } + + quote! { + impl #name { + #[doc(hidden)] + fn __auto_add_fields>(fields: &mut F) { + #(#field_registrations)* + } + #[doc(hidden)] + fn __implicit_methods>(methods: &mut M) { + #(#method_registrations)* + } + } + + impl mlua_extras::mlua::UserData for #name { + fn add_fields>(fields: &mut F) { + Self::__auto_add_fields(fields); + } + + fn add_methods>(methods: &mut M) { + Self::__implicit_methods(methods); + + use mlua_extras::__DefaultAutoMethods as _; + Self::__auto_add_methods(methods); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a140ae6..0aa21d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ pub mod extras; pub use mlua; #[cfg(feature="derive")] -pub use mlua_extras_derive::{Typed, UserData}; +pub use mlua_extras_derive::{Typed, UserData, user_data_impl}; #[cfg(feature = "send")] /// Used by the `send` feature @@ -20,3 +20,11 @@ impl MaybeSend for T {} pub trait MaybeSend {} #[cfg(not(feature = "send"))] impl MaybeSend for T {} + +#[cfg(feature = "derive")] +#[doc(hidden)] +pub trait __DefaultAutoMethods: Sized { + fn __auto_add_methods(_m: &mut M) {} +} +#[cfg(feature = "derive")] +impl __DefaultAutoMethods for T {} \ No newline at end of file From fae7527a6e99cd5b993d8f8bc0513e19c8b8d1d3 Mon Sep 17 00:00:00 2001 From: Zachary Boehm Date: Mon, 20 Apr 2026 18:55:49 -0500 Subject: [PATCH 2/3] [feat] add enum UserData proc-macro support --- examples/macros.rs | 61 +++-- mlua_extras_derive/src/extract.rs | 40 ++- mlua_extras_derive/src/lib.rs | 129 ++++++++- mlua_extras_derive/src/userdata.rs | 409 +++++++++++++++++++++++++---- 4 files changed, 558 insertions(+), 81 deletions(-) diff --git a/examples/macros.rs b/examples/macros.rs index 2a5cede..6e3ed8c 100644 --- a/examples/macros.rs +++ b/examples/macros.rs @@ -1,9 +1,8 @@ use mlua::{IntoLua, FromLua, Lua, StdLib}; -use mlua_extras::{UserData, user_data_impl}; +use mlua_extras::{UserData, Typed, user_data_impl}; #[derive(Clone, UserData)] struct Data { - #[field(rename = 1)] name: String } @@ -14,51 +13,65 @@ impl Data { Ok(self.name.clone()) } - /// This method is called first, if it returns a nil/none value then - /// the auto implementation will fallback to it's implementation. + /// This method is called last. /// - /// # Example - /// - /// If this function returned a value for `1` then the auto impl with - /// return that value. If this function returns nil then the auto impl - /// will return the value for `self.name` since it is exposed at index `1` + /// use `#[field(skip)]` for fields that are assigned to the index + /// to allow for them to overridden in this impl #[metamethod(Index)] fn index(&self, lua: &Lua, idx: isize) -> mlua::Result { match idx { -1 => "TESTING".into_lua(lua), + 1 => self.name.clone().into_lua(lua), _ => Ok(mlua::Value::Nil) } } - - - /// This method is called first, if it returns an error value then - /// the auto implementation will fallback to it's implementation. - /// - /// # Example + + /// This method is called last. /// - /// If this function returned `Ok(())` for an index of `1` then the auto impl - /// would just return that to the runtime. However, if this function returns - /// an error value then the auto impl will attempt to use it's impl. If the - /// auto impl doesn't now the index then the original error is returned. + /// use `#[field(skip)]` for fields that are assigned to the index + /// to allow for them to overridden in this impl #[metamethod(NewIndex)] fn new_index(&mut self, lua: &Lua, idx: isize, value: mlua::Value) -> mlua::Result<()> { match idx { 1 => self.name = ::from_lua(value, lua)?, // It is recommended to return some sort of error from this implementation. // - // This enforces strict indexing into userdata types and tells the auto impl - // to fallback to it's implementation. If the internal implementation also fails - // then the original error from this impl is returned to the lua runtime. + // This enforces strict indexing into userdata types. _ => return Err(mlua::Error::runtime(format!("invalid index '{idx}'"))) } Ok(()) } } +#[derive(Clone, UserData)] +enum Kind { + A, + B(String), + C { + name: String, + age: u8, + }, + D(u32), +} + +#[user_data_impl] +impl Kind { + #[method] + fn message(&self) -> String { + match self { + Self::A => "Hello, world!".into(), + Self::B(msg) => msg.clone(), + Self::C{ name, age } => format!("{name} age {age}"), + Self::D(count) => count.to_string() + } + } +} + fn main() -> mlua::Result<()> { let lua = unsafe { Lua::unsafe_new_with(StdLib::ALL, Default::default()) }; lua.globals().set("data", Data { name: "MluaExtras".into() })?; + lua.globals().set("kind", Kind::A)?; lua.load(" print('Index [1]:', data[1]) @@ -66,6 +79,10 @@ fn main() -> mlua::Result<()> { print('Set data[1] to \\'HelloWorld\\'') print('Get Data:', data:get_data()) print('Index [-1]:', data[-1]) + print('Kind:', kind._variant, kind:message()) + + local ok, value = pcall(function() return kind[1] end) + print('Kind [1]: OK', ok, value) ").exec()?; Ok(()) diff --git a/mlua_extras_derive/src/extract.rs b/mlua_extras_derive/src/extract.rs index a56674f..b526e95 100644 --- a/mlua_extras_derive/src/extract.rs +++ b/mlua_extras_derive/src/extract.rs @@ -1,6 +1,6 @@ use darling::FromMeta; use deluxe::{ParseAttributes, ParseMetaItem}; -use proc_macro2::TokenStream; +use proc_macro2::{Literal, TokenStream}; use quote::ToTokens; use syn::{ spanned::Spanned, Attribute, Expr, ExprLit, FnArg, ImplItemFn, Lit, Meta, MetaNameValue, Pat, @@ -73,6 +73,32 @@ pub struct UserDataField { pub rename: Option, } +#[derive(Debug, darling::FromField)] +#[darling(attributes(field), forward_attrs(doc))] +pub struct UserDataEnumField { + pub ident: Option, + pub ty: syn::Type, + + #[darling(default, skip)] + pub variant: TokenStream, + #[darling(default, skip)] + pub accessor: TokenStream, + + #[allow(dead_code)] + #[darling(with = "docs")] + pub attrs: Option, + + #[darling(default)] + pub skip: bool, + #[darling(default)] + pub readonly: bool, + #[darling(default)] + pub writeonly: bool, + #[darling(default)] + pub rename: Option, +} + + #[derive(Debug)] pub enum PassBy { Ref, @@ -347,11 +373,19 @@ impl ToTokens for IdentOrCustom { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Index { Int(isize), Str(String), } +impl Index { + pub fn is_str(&self) -> bool { + match self { + Self::Str(_) => true, + _ => false, + } + } +} impl std::fmt::Display for Index { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -380,7 +414,7 @@ impl FromMeta for Index { impl ToTokens for Index { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { match self { - Self::Int(i) => i.to_tokens(tokens), + Self::Int(i) => Literal::isize_unsuffixed(*i).to_tokens(tokens), Self::Str(s) => s.to_tokens(tokens), } } diff --git a/mlua_extras_derive/src/lib.rs b/mlua_extras_derive/src/lib.rs index 7746c11..0af2b3e 100644 --- a/mlua_extras_derive/src/lib.rs +++ b/mlua_extras_derive/src/lib.rs @@ -11,6 +11,53 @@ mod methods; mod userdata; pub(crate) mod extract; +/// Generates a [mlua::UserData] implementation from struct fields. +/// +/// Each named and unnamed field is automatically exposed to Lua as a read and/or write property or index. +/// +/// Use `#[field(...)]` attributes to controll access and naming: +/// +/// - `readonly`: Set the field to only be readable within Lua +/// - `writeonly`: Set the field to only be writable within Lua +/// - `skip`: Ignore generating and exposing the field +/// - `rename`: Rename the field to a string for a named field and a digit for an indexed field +/// +/// > Note: `readonly` + `writeonly` together is the same as having neither, the field will be exposed +/// > for both read and write. +/// +/// Optionally combine with [`macro@user_data_impl`] to also register methods in a rust like manner. +/// +/// # Example +/// +/// ```ignore +/// #[derive(Clone, UserData)] +/// struct Player { +/// name: String, +/// health: f64, +/// #[field(skip)] +/// handle: u64, +/// #[field(readonly)] +/// score: i32, +/// #[field(rename = "pos_x")] +/// position_x: f64, +/// } +/// ``` +/// +/// ```ignore +/// #[derive(Clone, UserData)] +/// enum PlayerAction { +/// Idle, +/// Move { +/// x: i32, +/// y: i32 +/// }, +/// Attack( +/// #[field(rename = "name")] +/// String +/// ), +/// Quit, +/// } +/// ``` #[proc_macro_error] #[proc_macro_derive(UserData, attributes(field))] pub fn derive_user_data(input: TokenStream) -> TokenStream { @@ -18,6 +65,66 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { userdata::derive(input).into() } +/// Attribute macro that registers methods from an `impl` block for use in Lua. +/// +/// Used on an `impl` block for a type that derives [`UserData`](macro@UserData), this +/// macro will register methods annotated with `#[method]` and `#[metamethod(...)]`. +/// +/// # Attributes +/// +/// - `#[method]` +/// - `#[method(rename = "name")`: register as a method with the provided name +/// - `#[metamethod(...)]` +/// - `#[metamethod(ToString)]`: register as a metamethod as a [`mlua::MetaMethod`] variant +/// - `#[metamethod("__custom")]`: register as a custom named metamethod +/// +/// # Patterns +/// +/// - `&self`: registered with `mlua::UserDataMethods::add_method` or `mlua::UserDataMethods::add_meta_method` +/// - `&mut self`: registered with `mlua::UserDataMethods::add_method_mut` or `mlua::UserDataMethods::add_meta_method_mut` +/// - without `self`: registered with `mlua::UserDataMethods::add_function` or `mlua::UserDataMethods::add_meta_function` +/// - `async fn`: registered with the `mlua::UserDataMethods::add_async_*` variant that matches the above arguments +/// - If the first non `self` parameter is `lua` then `&mlua::Lua` is passed to that parameter from the Lua context +/// +/// # Return +/// +/// - `Result` where `E: Into`: Method is fallible and the error is automatically converted to a [`mlua::Error`]. +/// This includes any error type that implements [`mlua::ExternalError`] and any return type that has the name `Result`. +/// - `T`: Method is infallible and is wrapped with `Ok(...)` when registered +/// - `()`: Method is infallible and has no return value. Registration returns `Ok(())` +/// +/// All methods stay as is and stay as regular callable rust functions. Any methods without `#[method]` or `#[metamethod(...)]` will not be registered. +/// +/// # Example +/// +/// ```ignore +/// #[derive(Clone, UserData)] +/// struct Counter { value: i64 } +/// +/// #[user_data_impl] +/// impl Counter { +/// #[method] +/// fn get(&self) -> i64 { self.value } +/// +/// #[method] +/// fn increment(&mut self) { self.value += 1 } +/// +/// #[method] +/// fn create_table(&self, lua: &mlua::Lua) -> mlua::Result { +/// lua.create_table() +/// } +/// +/// #[metamethod(ToString)] +/// fn to_string(&self) -> String { format!("Counter({})", self.value) } +/// +/// // Requires the `async` feature +/// // Must be accessed from lua code with an entry of `mlua::Chunk::eval_async` or `mlua::Chunk::exec_async` +/// #[method] +/// async fn fetch(&self, url: String) -> mlua::Result { +/// Ok(format!("fetched: {url}")) +/// } +/// } +/// ``` #[proc_macro_error] #[proc_macro_attribute] pub fn user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { @@ -25,6 +132,22 @@ pub fn user_data_impl(_attr: TokenStream, item: TokenStream) -> TokenStream { methods::derive(item).into() } +/// Generates a [`Typed`](mlua_extras::Typed) implementation from fields. +/// +/// Only supports structs and enums. +/// +/// This registers the target as a new Lua type that can be used +/// to generate documentation. +/// +/// # Structs +/// +/// Assigned as a lua `class` with it's registered fields, indexes, methods, +/// functions and their meta variants. +/// +/// # Enums +/// +/// Assigned as an alias to a union of `class`es where each `class` is a enum variant. This +/// is to best represent rust's use of enums as unions. #[proc_macro_error] #[proc_macro_derive(Typed)] pub fn derive_typed(input: TokenStream) -> TokenStream { @@ -54,7 +177,6 @@ pub fn derive_typed(input: TokenStream) -> TokenStream { let label = name.to_string(); let underscore_name = format!("_{name}"); - let names = enum_type.variants.iter().map(|(variant, _)| variant.name.to_string()).collect::>(); let named = enum_type.variants.iter().map(|(variant, _)| format!("{label}{}", variant.name)).collect::>(); let variants = enum_type.variants .iter() @@ -72,7 +194,6 @@ pub fn derive_typed(input: TokenStream) -> TokenStream { }) .collect::>(); - let enum_alt = format!("{label}Variant"); // TODO: This should be a union alias quote!( impl mlua_extras::typed::Typed for #name { @@ -84,10 +205,6 @@ pub fn derive_typed(input: TokenStream) -> TokenStream { fn implicit() -> impl IntoIterator { [ - ( - #enum_alt, - mlua_extras::typed::Type::r#enum(vec![#(mlua_extras::typed::Type::literal(#names),)*]) - ), ( #underscore_name, mlua_extras::typed::Type::class(mlua_extras::typed::TypedClassBuilder::new::()) diff --git a/mlua_extras_derive/src/userdata.rs b/mlua_extras_derive/src/userdata.rs index fd2ead5..f5beae6 100644 --- a/mlua_extras_derive/src/userdata.rs +++ b/mlua_extras_derive/src/userdata.rs @@ -1,38 +1,109 @@ +use std::collections::{BTreeMap, btree_map::Entry}; + use darling::FromField; use proc_macro::Literal; use proc_macro2::{Span, TokenStream as TokenStream2}; use syn::{Data, DeriveInput, Fields, LitInt, spanned::Spanned}; use quote::quote; -use crate::extract::{Index, UserDataField}; +use crate::extract::{Index, UserDataEnumField, UserDataField}; pub fn derive(input: DeriveInput) -> TokenStream2 { let name = &input.ident; - let fields: Vec<_> = match &input.data { - Data::Struct(data) => match &data.fields { - Fields::Named(named) => named.named.iter().collect(), - Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(), - Fields::Unit => Vec::new(), + match &input.data { + Data::Struct(data) => { + let fields: Vec<_> = match &data.fields { + Fields::Named(named) => named.named.iter().collect(), + Fields::Unnamed(unnamed) => unnamed.unnamed.iter().collect(), + Fields::Unit => Vec::new(), + }; + + let user_fields = fields + .iter() + .map(|field| { + match UserDataField::from_field(field) { + Ok(uf) => uf, + Err(err) => proc_macro_error::abort!(field, "{}", err) + } + }) + .collect::>(); + + derive_struct(name, user_fields) }, - Data::Enum(_) => { - proc_macro_error::abort!(name, "TypedUserData does not support enums"); + Data::Enum(data) => { + let mut enum_fields: BTreeMap> = Default::default(); + let mut variants = Vec::new(); + + for variant in data.variants.iter() { + let vn = &variant.ident; + let (variant, fields) = match &variant.fields { + Fields::Named(named) => { + let fields = named.named.iter().filter_map(|v| v.ident.as_ref().map(|v| { + let n = format_ident!("_{v}"); + quote!(#v: #n) + })); + + variants.push((quote!(#vn{ .. }), vn)); + (quote!(#vn{ #(#fields,)* }), named.named.iter().collect()) + }, + Fields::Unnamed(unnamed) => { + let fields = (0..unnamed.unnamed.len()).map(|v| format_ident!("_{v}")); + + let v = quote!(#vn( #(#fields,)* )); + variants.push((v.clone(), vn)); + (v, unnamed.unnamed.iter().collect()) + }, + Fields::Unit => { + let v = quote!(#vn); + variants.push((v.clone(), vn)); + (v, Vec::new()) + } + }; + + for (i, field) in fields.iter().enumerate() { + match UserDataEnumField::from_field(field) { + Ok(mut uf) => { + if uf.skip { + continue; + } + + uf.variant = variant.clone(); + uf.accessor = match uf.ident.as_ref() { + Some(ident) => { + let i = format_ident!("_{ident}"); + quote!(#i) + }, + None => { + let i = format_ident!("_{i}"); + quote!(#i) + } + }; + + let idx = match uf.rename.clone().or_else(|| uf.ident.as_ref().map(|v| Index::Str(v.to_string()))) { + Some(n) => n, + None => Index::Int(i as isize + 1) + }; + + match enum_fields.entry(idx) { + Entry::Occupied(mut entry) => entry.get_mut().push(uf), + Entry::Vacant(entry) => { entry.insert(vec![uf]); } + } + }, + Err(err) => proc_macro_error::abort!(field, "{}", err) + } + } + } + + derive_enum(name, variants, enum_fields) } Data::Union(_) => { proc_macro_error::abort!(name, "TypedUserData does not support unions"); } - }; - - let user_fields = fields - .iter() - .map(|field| { - match UserDataField::from_field(field) { - Ok(uf) => uf, - Err(err) => proc_macro_error::abort!(field, "{}", err) - } - }) - .collect::>(); + } +} +fn derive_struct(name: &syn::Ident, user_fields: Vec) -> TokenStream2 { let field_registrations = user_fields .iter() .enumerate() @@ -58,7 +129,7 @@ pub fn derive(input: DeriveInput) -> TokenStream2 { fields, #index, |_lua, this| Ok(this.#field_ident.clone()), - |_lua, this, val: #field_ty| { this.#field_ident = val; Ok(()) }, + |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, ); }), (_, true, false) => Some(quote! { @@ -70,7 +141,7 @@ pub fn derive(input: DeriveInput) -> TokenStream2 { (_, false, true) => Some(quote! { fields.add_field_method_set( #index, - |_lua, this, val: #field_ty| { this.#field_ident = val; Ok(()) }, + |_lua, this, _value: #field_ty| { this.#field_ident = _value; Ok(()) }, ); }), } @@ -105,7 +176,7 @@ pub fn derive(input: DeriveInput) -> TokenStream2 { _ => i as isize + 1 }).to_string(), Span::call_site()); - Some(quote!(Some(#lua_idx) => return mlua_extras::mlua::IntoLua::into_lua(this.#idx.clone(), lua),)) + Some(quote!(Some(#lua_idx) => return mlua_extras::mlua::IntoLua::into_lua(this.#idx.clone(), _lua),)) }, } }).collect::>(); @@ -135,58 +206,296 @@ pub fn derive(input: DeriveInput) -> TokenStream2 { }).to_string(), Span::call_site())); let ty = &f.ty; - Some(quote!(Some(#lua_idx) => this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(value, lua)?,)) + Some(quote!(Some(#lua_idx) => this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?,)) }, } }).collect::>(); method_registrations.push(quote!{ - methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |lua, (this, idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + { + let this = this.borrow::()?; + match _idx.as_integer() { + #(#indexes)* + _ => () + } + } + let metatable = this.metatable()?; - if let Ok(usr) = metatable.get::("__usr_index") { - match usr.call::((this.clone(), idx.clone()))? { - mlua_extras::mlua::Value::Nil => (), - other => return Ok(other) - }; + return usr.call::((this.clone(), _idx.clone())); } - - let this = this.borrow::()?; - match idx.as_integer() { - #(#indexes)* - _ => Ok(mlua_extras::mlua::Value::Nil) + + Ok(mlua_extras::mlua::Value::Nil) + }); + + methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { + { + let mut this = this.borrow_mut::()?; + match _idx.as_integer() { + #(#new_indexes)* + _ => () + } + } + + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_newindex") { + return usr.call::>((this.clone(), _idx.clone(), _value)); } + + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), + _ => "type does not contain index".into() + })) }); + }); + } + + quote! { + impl #name { + #[doc(hidden)] + fn __auto_add_fields>(fields: &mut F) { + #(#field_registrations)* + } + #[doc(hidden)] + fn __implicit_methods>(methods: &mut M) { + #(#method_registrations)* + } + } + + impl mlua_extras::mlua::UserData for #name { + fn add_fields>(fields: &mut F) { + Self::__auto_add_fields(fields); + } + + fn add_methods>(methods: &mut M) { + Self::__implicit_methods(methods); + + use mlua_extras::__DefaultAutoMethods as _; + Self::__auto_add_methods(methods); + } + } + } +} + +fn derive_enum(name: &syn::Ident, enum_variants: Vec<(TokenStream2, &syn::Ident)>, user_fields: BTreeMap>) -> TokenStream2 { + let count = enum_variants.len(); + + let field_registrations = user_fields + .iter() + .filter(|(idx, _fi)| idx.is_str()) + .map(|(idx, fi)| { + let has_get = fi.iter().any(|f| f.readonly || (!f.readonly && !f.writeonly)); + let has_set = fi.iter().any(|f| f.writeonly || (!f.readonly && !f.writeonly)); + + let getter = { + let variants: Vec<_> = fi + .iter() + .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) + .map(|v| { + let variant = &v.variant; + let accessor = &v.accessor; + quote!(Self::#variant => #accessor.clone(),) + }) + .collect(); + + let err_msg = format!("type variant does not contain field '{idx}'"); + let catchall = if variants.len() < count { + quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } + } else { + quote!() + }; + + quote!(Ok(match this { + #(#variants)* + #catchall + })) + }; + + let setter = { + let variants: Vec<_> = fi + .iter() + .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) + .map(|v| { + let variant = &v.variant; + let accessor = &v.accessor; + let ty = &v.ty; + quote!(Self::#variant => *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value, _lua)?,) + }) + .collect(); + + let err_msg = format!("type variant does not contain field '{idx}'"); + let catchall = if variants.len() < count { + quote!{ _ => return Err(mlua_extras::mlua::Error::runtime(#err_msg)), } + } else { + quote!() + }; + + quote!({ + match this { + #(#variants)* + #catchall + } + Ok(()) + }) + }; + + match (has_get, has_set) { + (true, true) | (false, false) => Some(quote! { + mlua_extras::extras::UserDataGetSet::::add_field_method_get_set( + fields, + #idx, + |_lua, this| #getter, + |_lua, this, _value: mlua_extras::mlua::Value| #setter, + ); + }), + (true, false) => Some(quote! { + fields.add_field_method_get( + #idx, + |_lua, this| #getter, + ); + }), + (false, true) => Some(quote! { + fields.add_field_method_set( + #idx, + |_lua, this, _value: mlua_extras::mlua::Value| #setter, + ); + }), + } + }); + + let mut method_registrations = Vec::::new(); + + // Add a custom __index and __newindex for the tuple struct/enum fields + // this will always attempt to fallback to the user definend #[metamethod(Index)] or #[metamethod(NewIndex)] + { + let indexes = user_fields + .iter() + .filter(|(idx, _fi)| !idx.is_str()) + .filter_map(|(idx, f)| { + let variants: Vec<_> = f + .iter() + .filter(|f| f.readonly || (!f.readonly && !f.writeonly)) + .map(|f| { + let variant = &f.variant; + let accessor = &f.accessor; + quote!{ + Self::#variant => return mlua_extras::mlua::IntoLua::into_lua(#accessor.clone(), _lua), + } + }) + .collect(); + + if variants.is_empty() { + return None; + } + + let catchall = if variants.len() < count { + quote!{ _ => () } + } else { + quote!() + }; + + Some(quote! { + Some(#idx) => match &*this { + #(#variants)* + #catchall + } + }) + }).collect::>(); + + let new_indexes = user_fields + .iter() + .filter(|(idx, _fi)| !idx.is_str()) + .filter_map(|(idx, f)| { + let variants: Vec<_> = f + .iter() + .filter(|f| f.writeonly || (!f.readonly && !f.writeonly)) + .map(|f| { + let variant = &f.variant; + let accessor = &f.accessor; + let ty = &f.ty; + quote!{ + Self::#variant => *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?, + } + }) + .collect(); + + if variants.is_empty() { + return None; + } + + let catchall = if variants.len() < count { + quote!{ _ => () } + } else { + quote!() + }; + + Some(quote! { + Some(#idx) => match &mut *this { + #(#variants)* + #catchall + } + }) + }).collect::>(); + + method_registrations.push(quote!{ + methods.add_meta_function(mlua_extras::mlua::MetaMethod::Index, |_lua, (this, _idx): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value)| { + { + let this = this.borrow::()?; + match _idx.as_integer() { + #(#indexes)* + _ => () + } + } - methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |lua, (this, idx, value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_index") { + return usr.call::((this.clone(), _idx.clone())); + } + + Ok(mlua_extras::mlua::Value::Nil) + }); - let mut error = None; - if let Ok(usr) = metatable.get::("__usr_newindex") { - match usr.call::>((this.clone(), idx.clone(), value.clone())) { - Ok(v) => return Ok(()), - Err(err) => error = Some(err), + methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { + { + let mut this = this.borrow_mut::()?; + match _idx.as_integer() { + #(#new_indexes)* + _ => () } } - let mut this = this.borrow_mut::()?; - match idx.as_integer() { - #(#new_indexes)* - _ => return Err(error.unwrap_or(mlua_extras::mlua::Error::runtime(match idx { - mlua_extras::mlua::Value::Integer(i) => format!("invalid index '{i}'"), - mlua_extras::mlua::Value::String(s) => format!("invalid index '{}'", s.to_string_lossy()), - _ => "invalid index".into() - }))) + let metatable = this.metatable()?; + if let Ok(usr) = metatable.get::("__usr_newindex") { + return usr.call::>((this.clone(), _idx.clone(), _value)); } - Ok(()) + + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), + _ => "type variant does not contain index".into() + })) }); }); } + let variants = enum_variants.iter().map(|(v, n)| { + let name = n.to_string(); + quote!(Self::#v => #name) + }); + + quote! { impl #name { #[doc(hidden)] fn __auto_add_fields>(fields: &mut F) { + fields.add_field_method_get("_variant", |_lua, this| { + Ok(match this { + #(#variants,)* + }) + }); + #(#field_registrations)* } #[doc(hidden)] @@ -208,4 +517,4 @@ pub fn derive(input: DeriveInput) -> TokenStream2 { } } } -} +} \ No newline at end of file From 7a772af6eb9bd9b9793a15e8c06ae8d1e39ad406 Mon Sep 17 00:00:00 2001 From: Zachary Boehm Date: Tue, 21 Apr 2026 12:24:40 -0500 Subject: [PATCH 3/3] [feat] add test cases for new UserData macros --- Cargo.lock | 22 ++ Cargo.toml | 1 + examples/enum_and_tuple.rs | 35 +- examples/types/enum_and_tuple.d.lua | 12 +- mlua_extras_derive/src/lib.rs | 3 +- mlua_extras_derive/src/methods.rs | 10 +- mlua_extras_derive/src/userdata.rs | 22 +- tests/user_data.rs | 493 ++++++++++++++++++++++++++++ 8 files changed, 570 insertions(+), 28 deletions(-) create mode 100644 tests/user_data.rs diff --git a/Cargo.lock b/Cargo.lock index 6b34e5d..2f6d979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,7 @@ dependencies = [ "serde", "strum", "tempfile", + "tokio", ] [[package]] @@ -750,6 +751,27 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "toml_datetime" version = "0.6.11" diff --git a/Cargo.toml b/Cargo.toml index 617e27b..c2a6735 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ derive = ["dep:mlua-extras-derive"] [dev-dependencies] serde = { version = "1.0.228", features = ["derive"] } tempfile = "3" +tokio = { version = "1", features = ["macros", "rt"] } [dependencies] mlua-extras-derive = { path = "./mlua_extras_derive", version = "11.6.0", optional = true } diff --git a/examples/enum_and_tuple.rs b/examples/enum_and_tuple.rs index 9e7b401..f3bac17 100644 --- a/examples/enum_and_tuple.rs +++ b/examples/enum_and_tuple.rs @@ -1,17 +1,21 @@ -use mlua::MetaMethod; +use mlua::{MetaMethod, UserData}; use mlua_extras::{ - Typed, UserData, mlua, + Typed, mlua, typed::{ - Type, TypedDataFields, TypedDataMethods, TypedUserData, + Type, TypedDataFields, TypedDataMethods, TypedUserData, WrappedBuilder, generator::{Definition, DefinitionFileGenerator, Definitions}, }, }; use std::path::PathBuf; -#[derive(Typed, UserData)] +#[derive(Typed)] enum Kind { A(String), - B { name: String, data: String }, + B { + name: String, + data: String, + }, + #[allow(unused)] C, } @@ -25,6 +29,18 @@ impl Kind { } } +impl UserData for Kind { + fn add_fields>(fields: &mut F) { + let mut wrapper = WrappedBuilder::new(fields); + ::add_fields(&mut wrapper); + } + + fn add_methods>(methods: &mut M) { + let mut wrapper = mlua_extras::typed::WrappedBuilder::new(methods); + ::add_methods(&mut wrapper); + } +} + impl TypedUserData for Kind { fn add_fields>(fields: &mut T) { fields.add_field_method_get("name", |_lua, this: &Self| match this { @@ -97,7 +113,7 @@ fn main() -> mlua::Result<()> { .load( r#" print(KindA[1]) - print(KindA["1"]) + -- print(KindA["1"]) print(KindB.name) print(KindB.data) @@ -120,7 +136,12 @@ fn main() -> mlua::Result<()> { } let definitions: Definitions = Definitions::start() - .define("enum_and_tuple", Definition::start().register::("Kind")) + .define( + "enum_and_tuple", + Definition::start() + .register::("Kind") + .register_as("KindVariant", Type::literal("A") | "B" | "C"), + ) .finish(); let types_path = PathBuf::from("examples/types"); diff --git a/examples/types/enum_and_tuple.d.lua b/examples/types/enum_and_tuple.d.lua index 9a40d58..bd3cdaf 100644 --- a/examples/types/enum_and_tuple.d.lua +++ b/examples/types/enum_and_tuple.d.lua @@ -1,13 +1,7 @@ --- @meta ---- @alias Kind KindA | KindB | KindC - ---- @alias KindEnum "A" ---- | "B" ---- | "C" - --- @class _Kind ---- @field _variant KindEnum +--- @field _variant KindVariant --- Kind::A variant data --- @field [1] string --- @field data string @@ -30,3 +24,7 @@ local _CLASS__Kind_ = { --- @class KindC: _Kind +--- @alias Kind KindA | KindB | KindC + +--- @alias KindVariant "A" | "B" | "C" + diff --git a/mlua_extras_derive/src/lib.rs b/mlua_extras_derive/src/lib.rs index 0af2b3e..9e159df 100644 --- a/mlua_extras_derive/src/lib.rs +++ b/mlua_extras_derive/src/lib.rs @@ -84,7 +84,8 @@ pub fn derive_user_data(input: TokenStream) -> TokenStream { /// - `&mut self`: registered with `mlua::UserDataMethods::add_method_mut` or `mlua::UserDataMethods::add_meta_method_mut` /// - without `self`: registered with `mlua::UserDataMethods::add_function` or `mlua::UserDataMethods::add_meta_function` /// - `async fn`: registered with the `mlua::UserDataMethods::add_async_*` variant that matches the above arguments -/// - If the first non `self` parameter is `lua` then `&mlua::Lua` is passed to that parameter from the Lua context +/// - If the first non `self` parameter is `lua` then `&mlua::Lua` is passed to non async methods/functions +/// and `mlua::Lua` is passed into async methods/functions /// /// # Return /// diff --git a/mlua_extras_derive/src/methods.rs b/mlua_extras_derive/src/methods.rs index 278f06f..d1e0ca8 100644 --- a/mlua_extras_derive/src/methods.rs +++ b/mlua_extras_derive/src/methods.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use syn::{ImplItem, ItemImpl, Type}; use quote::quote; -use crate::extract::{PassBy, UserDataMethod}; +use crate::extract::{PassBy, UserDataMethod, is_metamethod_attr, is_method_attr}; pub fn derive(item: ItemImpl) -> TokenStream { let self_ty = &item.self_ty; @@ -53,14 +53,6 @@ pub fn derive(item: ItemImpl) -> TokenStream { } } -fn is_method_attr(attr: &syn::Attribute) -> bool { - attr.path().is_ident("method") -} - -fn is_metamethod_attr(attr: &syn::Attribute) -> bool { - attr.path().is_ident("metamethod") -} - fn generate_registration(info: &UserDataMethod, self_ty: &Type) -> TokenStream { let fn_name = &info.name; let lua_name = &info.lua_name; diff --git a/mlua_extras_derive/src/userdata.rs b/mlua_extras_derive/src/userdata.rs index f5beae6..9e43d2f 100644 --- a/mlua_extras_derive/src/userdata.rs +++ b/mlua_extras_derive/src/userdata.rs @@ -206,7 +206,10 @@ fn derive_struct(name: &syn::Ident, user_fields: Vec) -> TokenStr }).to_string(), Span::call_site())); let ty = &f.ty; - Some(quote!(Some(#lua_idx) => this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?,)) + Some(quote!(Some(#lua_idx) => { + this.#idx = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; + return Ok(None) + },)) }, } }).collect::>(); @@ -226,7 +229,11 @@ fn derive_struct(name: &syn::Ident, user_fields: Vec) -> TokenStr return usr.call::((this.clone(), _idx.clone())); } - Ok(mlua_extras::mlua::Value::Nil) + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type does not contain field '{}'", s.to_string_lossy()), + _ => "type does not contain index".into() + })) }); methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { @@ -416,7 +423,10 @@ fn derive_enum(name: &syn::Ident, enum_variants: Vec<(TokenStream2, &syn::Ident) let accessor = &f.accessor; let ty = &f.ty; quote!{ - Self::#variant => *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?, + Self::#variant => { + *#accessor = <#ty as mlua_extras::mlua::FromLua>::from_lua(_value.clone(), _lua)?; + return Ok(None); + }, } }) .collect(); @@ -454,7 +464,11 @@ fn derive_enum(name: &syn::Ident, enum_variants: Vec<(TokenStream2, &syn::Ident) return usr.call::((this.clone(), _idx.clone())); } - Ok(mlua_extras::mlua::Value::Nil) + Err(mlua_extras::mlua::Error::runtime(match _idx { + mlua_extras::mlua::Value::Integer(i) => format!("type variant does not contain index '{i}'"), + mlua_extras::mlua::Value::String(s) => format!("type variant does not contain field '{}'", s.to_string_lossy()), + _ => "type variant does not contain index".into() + })) }); methods.add_meta_function(mlua_extras::mlua::MetaMethod::NewIndex, |_lua, (this, _idx, _value): (mlua_extras::mlua::AnyUserData, mlua_extras::mlua::Value, mlua_extras::mlua::Value)| { diff --git a/tests/user_data.rs b/tests/user_data.rs new file mode 100644 index 0000000..28ab31c --- /dev/null +++ b/tests/user_data.rs @@ -0,0 +1,493 @@ +#![cfg(all(feature = "mlua", feature = "derive"))] + +use mlua::{AnyUserData, FromLua}; +use mlua_extras::{ + UserData, + mlua::{self, Lua, Value}, + user_data_impl, +}; + +// Test 1: Feidl attribute parsing + +#[allow(dead_code)] +#[derive(Clone, UserData)] +struct TestNamedFields { + normal: String, + #[field(skip)] + skipped: bool, + #[field(readonly)] + readonly: i32, + #[field(writeonly)] + writeonly: f64, + #[field(rename = "colour")] + color: String, + #[field(rename = -1)] + value: Option, +} + +#[test] +fn test_named_field_registration() { + let lua = Lua::new(); + + lua.globals() + .set( + "obj", + TestNamedFields { + normal: "test".into(), + skipped: true, + readonly: 4, + writeonly: 3.14, + color: "red".into(), + value: Some("Hello, world!".into()), + }, + ) + .unwrap(); + + // Read + Write field + let val: String = lua.load("return obj.normal").eval().unwrap(); + assert_eq!(val, "test"); + lua.load("obj.normal = 'testing'").exec().unwrap(); + let val: String = lua.load("return obj.normal").eval().unwrap(); + assert_eq!(val, "testing"); + + // Skip field: not accessible (throws error) + let result = lua.load("return obj.skipped").eval::(); + println!("{result:?}"); + assert!(result.is_err()); + + // Readonly field: Fails on write + let val: i32 = lua.load("return obj.readonly").eval().unwrap(); + assert_eq!(val, 4); + let result = lua.load("obj.readonly = 100").exec(); + assert!(result.is_err()); + + // Writeonly field: Fails on read + let result = lua.load("return obj.writeonly").eval::(); + assert!(result.is_err()); + let result = lua.load("obj.writeonly = 6.28").exec(); + assert!(result.is_ok()); + + // Renamed field: accessible only via the rename value + let val: String = lua.load("return obj.colour").eval().unwrap(); + assert_eq!(val, "red"); + lua.load("obj.colour = 'blue'").exec().unwrap(); + let val: String = lua.load("return obj.colour").eval().unwrap(); + assert_eq!(val, "blue"); + + let result = lua.load("return obj.color").eval::(); + assert!(result.is_err()); + + // Rename field: Named fields renamed to an index are only indexable + let val: Option = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val.as_deref(), Some("Hello, world!")); + lua.load("obj[-1] = nil").exec().unwrap(); + let val: Option = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, None); + let result = lua.load("return obj.value").eval::(); + assert!(result.is_err()); +} + +#[allow(dead_code)] +#[derive(Clone, UserData)] +struct TestIndexedFields( + String, + #[field(skip)] bool, + #[field(readonly)] i32, + #[field(writeonly)] f64, + #[field(rename = -1)] String, + #[field(rename = "value")] Option, +); + +#[test] +fn test_indexed_field_registration() { + let lua = Lua::new(); + + lua.globals() + .set( + "obj", + TestIndexedFields( + "test".into(), + true, + 4, + 3.14, + "red".into(), + Some("Hello, world!".into()), + ), + ) + .unwrap(); + + // Read + Write field + let val: String = lua.load("return obj[1]").eval().unwrap(); + assert_eq!(val, "test"); + lua.load("obj[1] = 'testing'").exec().unwrap(); + let val: String = lua.load("return obj[1]").eval().unwrap(); + assert_eq!(val, "testing"); + + // Skip field: not accessible (throws error) + let result = lua.load("return obj[2]").eval::(); + assert!(result.is_err()); + + // Readonly field: Fails on write + let val: i32 = lua.load("return obj[3]").eval().unwrap(); + assert_eq!(val, 4); + let result = lua.load("obj[3] = 100").exec(); + assert!(result.is_err()); + + // Writeonly field: Fails on read + let result = lua.load("return obj[4]").eval::(); + assert!(result.is_err()); + let result = lua.load("obj[4] = 6.28").exec(); + assert!(result.is_ok()); + + // Renamed field: accessible only via the rename value + let val: String = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, "red"); + lua.load("obj[-1] = 'blue'").exec().unwrap(); + let val: String = lua.load("return obj[-1]").eval().unwrap(); + assert_eq!(val, "blue"); + + let result = lua.load("return obj[5]").eval::(); + assert!(result.is_err()); + + // Rename field: Named fields renamed to an index are only indexable + let val: Option = lua.load("return obj.value").eval().unwrap(); + assert_eq!(val.as_deref(), Some("Hello, world!")); + lua.load("obj.value = nil").exec().unwrap(); + let val: Option = lua.load("return obj.value").eval().unwrap(); + assert_eq!(val, None); + let result = lua.load("return obj[6]").eval::(); + assert!(result.is_err()); +} + +// Test 2: Methods with rename + +#[derive(Clone, UserData)] +struct Calculator { + value: f64, +} + +#[user_data_impl] +impl Calculator { + #[method] + fn add(&self, x: f64) -> f64 { + self.value + x + } + + #[method(rename = "divide")] + fn checked_divide(&self, x: f64) -> mlua::Result { + if x == 0.0 { + Err(mlua::Error::runtime("division by zero")) + } else { + Ok(self.value / x) + } + } + + #[method] + fn get_value_and_double(&self) -> (f64, f64) { + (self.value, self.value * 2.0) + } +} + +#[test] +fn test_method_registration() { + let lua = Lua::new(); + + lua.globals() + .set("calc", Calculator { value: 10.0 }) + .unwrap(); + + let val: f64 = lua.load("return calc.value").eval().unwrap(); + assert_eq!(val, 10.0); + + // Infallible + let result: f64 = lua.load("return calc:add(5)").eval().unwrap(); + assert_eq!(result, 15.0); + + // Rename (fallible) + let result: f64 = lua.load("return calc:divide(2)").eval().unwrap(); + assert_eq!(result, 5.0); + + // Fallible + let result = lua.load("return calc:divide(0)").exec(); + assert!(result.is_err()); + + // Multi-return method + let (a, b): (f64, f64) = lua + .load("return calc:get_value_and_double()") + .eval() + .unwrap(); + assert_eq!(a, 10.0); + assert_eq!(b, 20.0); +} + +// Test 3: Metamethods + +#[derive(Clone, UserData)] +struct Stringable { + value: String, +} + +#[user_data_impl] +impl Stringable { + #[metamethod(ToString)] + fn to_string_repr(&self) -> String { + format!("Stringable({})", self.value) + } + + #[metamethod(Len)] + fn len(&self) -> usize { + self.value.len() + } + + #[metamethod("__half")] + fn first_half(&self) -> String { + let c = self.len(); + self.value[0..c / 2].to_string() + } +} + +#[test] +fn test_metamethods() { + let lua = Lua::new(); + lua.globals() + .set( + "obj", + Stringable { + value: "hello, world!".into(), + }, + ) + .unwrap(); + lua.globals() + .set( + "half", + lua.create_function(|_lua, this: AnyUserData| { + let metatable = this.metatable()?; + if let Ok(half) = metatable.get::("__half") { + return half.call::(this); + } + Err(mlua::Error::runtime( + "type does not implememnt __half metamethod", + )) + }) + .unwrap(), + ) + .unwrap(); + + let result: String = lua.load("return tostring(obj)").eval().unwrap(); + assert_eq!(result, "Stringable(hello, world!)"); + + let result: i64 = lua.load("return #obj").eval().unwrap(); + assert_eq!(result, 13); + + let result: String = lua.load("return half(obj)").eval().unwrap(); + assert_eq!(result, "hello,"); +} + +// Test 4: Mutable Methods + +#[derive(Clone, UserData)] +struct MutCalc { + value: f64, +} + +#[user_data_impl] +impl MutCalc { + #[method] + fn set_value(&mut self, x: f64) { + self.value = x; + } +} + +#[test] +fn test_mut_method() { + let lua = Lua::new(); + lua.globals().set("calc", MutCalc { value: 0.0 }).unwrap(); + + lua.load("calc:set_value(42)").exec().unwrap(); + let result: f64 = lua.load("return calc.value").eval().unwrap(); + assert_eq!(result, 42.0); +} + +// Test 5: Optional lua parameter + +#[derive(Clone, UserData)] +struct LuaAccess; + +#[user_data_impl] +impl LuaAccess { + #[method] + fn create_table(&self, lua: &Lua) -> mlua::Result { + lua.create_table() + } + + #[method] + fn no_lua(&self) -> String { + "test".into() + } +} + +#[test] +fn test_optional_lua_param() { + let lua = Lua::new(); + lua.globals().set("obj", LuaAccess).unwrap(); + let result: mlua::Table = lua.load("return obj:create_table()").eval().unwrap(); + assert!(result.is_empty()); + let result: String = lua.load("return obj:no_lua()").eval().unwrap(); + assert_eq!(result, "test"); +} + +// Test 6: Static functions (no self) + +#[derive(Clone, UserData)] +struct MathUtils; + +#[user_data_impl] +impl MathUtils { + #[method] + fn add(a: f64, b: f64) -> f64 { + a + b + } + + #[method(rename = "create")] + fn new_instance(lua: &Lua) -> mlua::Result { + lua.create_table() + } +} + +#[test] +fn test_static_functions() { + let lua = Lua::new(); + lua.globals().set("math", MathUtils).unwrap(); + + let result: f64 = lua.load("return math.add(3, 4)").eval().unwrap(); + assert_eq!(result, 7.0); + + let result: mlua::Table = lua.load("return math.create()").eval().unwrap(); + assert!(result.is_empty()); +} + +// Test 7: Static meta functions (no self) + +#[derive(Debug, Clone, UserData, PartialEq)] +struct Vec2(f64, f64); +impl FromLua for Vec2 { + fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { + let tn = value.type_name(); + match value { + Value::UserData(usr_data) => { + if usr_data.is::() { + return usr_data.take::(); + } + } + Value::Table(tbl) => { + return Ok(Vec2(tbl.get(1)?, tbl.get(2)?)); + } + Value::Number(n) => return Ok(Vec2(n, n)), + _ => (), + } + + Err(mlua::Error::FromLuaConversionError { + from: tn, + to: "Vec2".to_string(), + message: Some("failed to convert to userdata Vec2".into()), + }) + } +} + +#[user_data_impl] +impl Vec2 { + #[metamethod(Add)] + fn add(a: Self, b: Self) -> Self { + Vec2(a.0 + b.0, a.1 + b.1) + } + + #[metamethod("__dot")] + fn dot_product(a: Self, b: Self) -> f64 { + (a.0 * b.0) + (a.1 * b.1) + } +} + +#[test] +fn test_static_meta_functions() { + let lua = Lua::new(); + lua.globals() + .set( + "vec2", + lua.create_function(|_lua, (x, y): (f64, f64)| Ok(Vec2(x, y))) + .unwrap(), + ) + .unwrap(); + lua.globals() + .set( + "dot", + lua.create_function(|_lua, (a, b): (AnyUserData, AnyUserData)| { + if a.type_id() != b.type_id() { + return Err(mlua::Error::runtime("both parameters but be the same type")); + } + + let am = a.metatable()?; + am.get::("__dot")?.call::((a, b)) + }) + .unwrap(), + ) + .unwrap(); + + let result: Vec2 = lua.load("return vec2(1, 2) + vec2(3, 4)").eval().unwrap(); + assert_eq!(result, Vec2(4.0, 6.0)); + + let result: f64 = lua.load("return dot(vec2(2, 4), vec2(4, 2))").eval().unwrap(); + assert_eq!(result, 16.0); +} + +// Test 8: Async Methods + +#[cfg(feature = "async")] +mod async_tests { + use super::*; + + #[derive(Clone, UserData)] + struct AsyncWorker { + prefix: String, + } + + #[user_data_impl] + impl AsyncWorker { + #[method] + async fn process(&self, input: String) -> mlua::Result { + Ok(format!("{}: {input}", self.prefix)) + } + + #[method] + async fn with_lua(&self, lua: Lua, key: String) -> mlua::Result { + lua.globals().get(key) + } + } + + #[tokio::test] + async fn test_async_methods() { + let lua = Lua::new(); + lua.globals() + .set( + "worker", + AsyncWorker { + prefix: "test".into(), + }, + ) + .unwrap(); + + let result: String = lua + .load("return worker:process('hello, world')") + .eval_async() + .await + .unwrap(); + assert_eq!(result, "test: hello, world"); + + lua.globals().set("test_val", 42).unwrap(); + let result: i64 = lua + .load("return worker:with_lua('test_val')") + .eval_async() + .await + .unwrap(); + assert_eq!(result, 42) + } +}