From 29af448ad9799b1791a7660f6b0446a3ecd9a4c2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Thu, 12 Feb 2026 15:27:30 +0000 Subject: [PATCH 001/138] Update README to indicate dev status --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c2e2eba2..fa462e10 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ [Benchmarks]: https://github.com/khvzak/script-bench-rs [FAQ]: FAQ.md +## The main branch is the development version of `mlua`. Please see the [v0.11](https://github.com/mlua-rs/mlua/tree/v0.11) branch for the stable versions of `mlua`. + `mlua` is a set of bindings to the [Lua](https://www.lua.org) programming language for Rust with a goal of providing a _safe_ (as much as possible), high level, easy to use, practical and flexible API. From f19c6aac3b04889ea0b52e84183cc11a3b15d933 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Thu, 12 Feb 2026 15:59:55 +0000 Subject: [PATCH 002/138] Fix tests --- tests/hooks.rs | 3 ++- tests/tests.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/hooks.rs b/tests/hooks.rs index c5d7da32..f1b72445 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -3,7 +3,8 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex}; -use mlua::{DebugEvent, Error, HookTriggers, Lua, Result, ThreadStatus, Value, VmState}; +use mlua::debug::DebugEvent; +use mlua::{Error, HookTriggers, Lua, Result, ThreadStatus, Value, VmState}; #[test] fn test_hook_triggers() { diff --git a/tests/tests.rs b/tests/tests.rs index 47151387..99716f63 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1374,7 +1374,7 @@ fn test_inspect_stack() -> Result<()> { local function baz(a, b, c, ...) return stack_info() end - assert(baz() == 'DebugStack { num_ups: 1, num_params: 3, is_vararg: true }') + assert(baz() == 'DebugStack { num_upvalues: 1, num_params: 3, is_vararg: true }') "#, ) .exec()?; @@ -1387,7 +1387,7 @@ fn test_inspect_stack() -> Result<()> { local function baz(a, b, c, ...) return stack_info() end - assert(baz() == 'DebugStack { num_ups: 1 }') + assert(baz() == 'DebugStack { num_upvalues: 1 }') "#, ) .exec()?; From 7f3ec63ab5898bf3a8145a065bc24393dce916a4 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 20 Feb 2026 18:59:53 +0000 Subject: [PATCH 003/138] Support `__todebugstring` for pretty userdata debug output Close #681 --- src/userdata.rs | 6 ++++++ src/value.rs | 44 ++++++++++++++++++++++++++++++++------------ tests/value.rs | 24 +++++++++++++++++++++++- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/userdata.rs b/src/userdata.rs index c8d8c19b..4719533e 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -123,6 +123,11 @@ pub enum MetaMethod { /// /// This is not an operator, but will be called by methods such as `tostring` and `print`. ToString, + /// The `__todebugstring` metamethod for debug purposes. + /// + /// This is an mlua-specific metamethod that can be used to provide debug representation for + /// userdata. + ToDebugString, /// The `__pairs` metamethod. /// /// This is not an operator, but it will be called by the built-in `pairs` function. @@ -232,6 +237,7 @@ impl MetaMethod { MetaMethod::NewIndex => "__newindex", MetaMethod::Call => "__call", MetaMethod::ToString => "__tostring", + MetaMethod::ToDebugString => "__todebugstring", #[cfg(any( feature = "lua55", diff --git a/src/value.rs b/src/value.rs index 1250a3f7..623fe001 100644 --- a/src/value.rs +++ b/src/value.rs @@ -151,7 +151,7 @@ impl Value { /// This might invoke the `__tostring` metamethod for non-primitive types (eg. tables, /// functions). pub fn to_string(&self) -> Result { - unsafe fn invoke_to_string(vref: &ValueRef) -> Result { + unsafe fn invoke_tostring(vref: &ValueRef) -> Result { let lua = vref.lua.lock(); let state = lua.state(); let _guard = StackGuard::new(state); @@ -178,9 +178,9 @@ impl Value { | Value::Function(Function(vref)) | Value::Thread(Thread(vref, ..)) | Value::UserData(AnyUserData(vref)) - | Value::Other(vref) => unsafe { invoke_to_string(vref) }, + | Value::Other(vref) => unsafe { invoke_tostring(vref) }, #[cfg(feature = "luau")] - Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_to_string(vref) }, + Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_tostring(vref) }, Value::Error(err) => Ok(err.to_string()), } } @@ -545,6 +545,24 @@ impl Value { ident: usize, visited: &mut HashSet<*const c_void>, ) -> fmt::Result { + unsafe fn invoke_tostring_dbg(vref: &ValueRef) -> Result> { + let lua = vref.lua.lock(); + let state = lua.state(); + let _guard = StackGuard::new(state); + check_stack(state, 3)?; + + lua.push_ref(vref); + protect_lua!(state, 1, 1, fn(state) { + // Try `__todebugstring` metamethod first, then `__tostring` + if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 { + if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 { + ffi::lua_pushnil(state); + } + } + })?; + Ok(lua.pop_value().as_string().map(|s| s.to_string_lossy())) + } + match self { Value::Nil => write!(fmt, "nil"), Value::Boolean(b) => write!(fmt, "{b}"), @@ -561,15 +579,17 @@ impl Value { t @ Value::Table(_) => write!(fmt, "table: {:?}", t.to_pointer()), f @ Value::Function(_) => write!(fmt, "function: {:?}", f.to_pointer()), t @ Value::Thread(_) => write!(fmt, "thread: {:?}", t.to_pointer()), - u @ Value::UserData(ud) => { - // Try `__name/__type` first then `__tostring` - let name = ud.type_name().ok().flatten(); - let s = name - .map(|name| format!("{name}: {:?}", u.to_pointer())) - .or_else(|| u.to_string().ok()) - .unwrap_or_else(|| format!("userdata: {:?}", u.to_pointer())); - write!(fmt, "{s}") - } + u @ Value::UserData(ud) => unsafe { + // Try converting to a (debug) string first, with fallback to `__name/__type` + match invoke_tostring_dbg(&ud.0) { + Ok(Some(s)) => write!(fmt, "{s}"), + _ => { + let name = ud.type_name().ok().flatten(); + let name = name.as_deref().unwrap_or("userdata"); + write!(fmt, "{name}: {:?}", u.to_pointer()) + } + } + }, #[cfg(feature = "luau")] buf @ Value::Buffer(_) => write!(fmt, "buffer: {:?}", buf.to_pointer()), Value::Error(e) if recursive => write!(fmt, "{e:?}"), diff --git a/tests/value.rs b/tests/value.rs index 43ec708f..31bc4e9d 100644 --- a/tests/value.rs +++ b/tests/value.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::os::raw::c_void; use std::ptr; -use mlua::{Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, Value}; +use mlua::{ + Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, UserDataRegistry, Value, +}; #[test] fn test_value_eq() -> Result<()> { @@ -218,6 +220,26 @@ fn test_debug_format() -> Result<()> { .map(Value::UserData)?; assert!(format!("{ud:#?}").starts_with("HashMap:")); + struct ToDebugUserData; + impl UserData for ToDebugUserData { + fn register(registry: &mut UserDataRegistry) { + registry.add_meta_method("__tostring", |_, _, ()| Ok("regular-string")); + registry.add_meta_method("__todebugstring", |_, _, ()| Ok("debug-string")); + } + } + let debug_ud = Value::UserData(lua.create_userdata(ToDebugUserData)?); + assert_eq!(debug_ud.to_string()?, "regular-string"); + assert_eq!(format!("{debug_ud:#?}"), "debug-string"); + + struct ToStringUserData; + impl UserData for ToStringUserData { + fn register(registry: &mut UserDataRegistry) { + registry.add_meta_method("__tostring", |_, _, ()| Ok("to-string-only")); + } + } + let tostring_only_ud = Value::UserData(lua.create_userdata(ToStringUserData)?); + assert_eq!(format!("{tostring_only_ud:#?}"), "tostring-only"); + Ok(()) } From 151adc0e8725f97d2bdaab2e1c24deaefd6dcf4e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 20 Feb 2026 20:31:29 +0000 Subject: [PATCH 004/138] Implement pretty debug format for `AnyUserData` similar to `Value::UserData`. --- src/userdata.rs | 41 ++++++++++++++++++++++++++++++++++++++++- src/value.rs | 30 +----------------------------- tests/value.rs | 12 +++++++++--- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/userdata.rs b/src/userdata.rs index 4719533e..9a507d70 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -711,7 +711,7 @@ pub trait UserData: Sized { /// /// [`is`]: crate::AnyUserData::is /// [`borrow`]: crate::AnyUserData::borrow -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct AnyUserData(pub(crate) ValueRef); impl AnyUserData { @@ -1081,6 +1081,45 @@ impl AnyUserData { }; is_serializable().unwrap_or(false) } + + unsafe fn invoke_tostring_dbg(&self) -> Result> { + let lua = self.0.lua.lock(); + let state = lua.state(); + let _guard = StackGuard::new(state); + check_stack(state, 3)?; + + lua.push_ref(&self.0); + protect_lua!(state, 1, 1, fn(state) { + // Try `__todebugstring` metamethod first, then `__tostring` + if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 { + if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 { + ffi::lua_pushnil(state); + } + } + })?; + Ok(lua.pop_value().as_string().map(|s| s.to_string_lossy())) + } + + pub(crate) fn fmt_pretty(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // Try converting to a (debug) string first, with fallback to `__name/__type` + match unsafe { self.invoke_tostring_dbg() } { + Ok(Some(s)) => write!(fmt, "{s}"), + _ => { + let name = self.type_name().ok().flatten(); + let name = name.as_deref().unwrap_or("userdata"); + write!(fmt, "{name}: {:?}", self.to_pointer()) + } + } + } +} + +impl fmt::Debug for AnyUserData { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if fmt.alternate() { + return self.fmt_pretty(fmt); + } + fmt.debug_tuple("AnyUserData").field(&self.0).finish() + } } /// Handle to a [`AnyUserData`] metatable. diff --git a/src/value.rs b/src/value.rs index 623fe001..8d178e04 100644 --- a/src/value.rs +++ b/src/value.rs @@ -545,24 +545,6 @@ impl Value { ident: usize, visited: &mut HashSet<*const c_void>, ) -> fmt::Result { - unsafe fn invoke_tostring_dbg(vref: &ValueRef) -> Result> { - let lua = vref.lua.lock(); - let state = lua.state(); - let _guard = StackGuard::new(state); - check_stack(state, 3)?; - - lua.push_ref(vref); - protect_lua!(state, 1, 1, fn(state) { - // Try `__todebugstring` metamethod first, then `__tostring` - if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 { - if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 { - ffi::lua_pushnil(state); - } - } - })?; - Ok(lua.pop_value().as_string().map(|s| s.to_string_lossy())) - } - match self { Value::Nil => write!(fmt, "nil"), Value::Boolean(b) => write!(fmt, "{b}"), @@ -579,17 +561,7 @@ impl Value { t @ Value::Table(_) => write!(fmt, "table: {:?}", t.to_pointer()), f @ Value::Function(_) => write!(fmt, "function: {:?}", f.to_pointer()), t @ Value::Thread(_) => write!(fmt, "thread: {:?}", t.to_pointer()), - u @ Value::UserData(ud) => unsafe { - // Try converting to a (debug) string first, with fallback to `__name/__type` - match invoke_tostring_dbg(&ud.0) { - Ok(Some(s)) => write!(fmt, "{s}"), - _ => { - let name = ud.type_name().ok().flatten(); - let name = name.as_deref().unwrap_or("userdata"); - write!(fmt, "{name}: {:?}", u.to_pointer()) - } - } - }, + Value::UserData(ud) => ud.fmt_pretty(fmt), #[cfg(feature = "luau")] buf @ Value::Buffer(_) => write!(fmt, "buffer: {:?}", buf.to_pointer()), Value::Error(e) if recursive => write!(fmt, "{e:?}"), diff --git a/tests/value.rs b/tests/value.rs index 31bc4e9d..506aaa0e 100644 --- a/tests/value.rs +++ b/tests/value.rs @@ -3,7 +3,8 @@ use std::os::raw::c_void; use std::ptr; use mlua::{ - Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, UserDataRegistry, Value, + AnyUserData, Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, UserDataRegistry, + Value, }; #[test] @@ -234,11 +235,16 @@ fn test_debug_format() -> Result<()> { struct ToStringUserData; impl UserData for ToStringUserData { fn register(registry: &mut UserDataRegistry) { - registry.add_meta_method("__tostring", |_, _, ()| Ok("to-string-only")); + registry.add_meta_method("__tostring", |_, _, ()| Ok("regular-string")); } } let tostring_only_ud = Value::UserData(lua.create_userdata(ToStringUserData)?); - assert_eq!(format!("{tostring_only_ud:#?}"), "tostring-only"); + assert_eq!(format!("{tostring_only_ud:#?}"), "regular-string"); + + // Check that `AnyUsedata` pretty debug format is same as for `Value::UserData` + let any_ud: AnyUserData = lua.create_userdata(ToDebugUserData)?; + let value_ud = Value::UserData(any_ud.clone()); + assert_eq!(format!("{any_ud:#?}"), format!("{value_ud:#?}")); Ok(()) } From 63a255bbc918bf0925650313872608fdc838a57e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 21 Feb 2026 15:05:55 +0000 Subject: [PATCH 005/138] Replace `is_sync` specialization trick with `MaybeSync` trait bound. The `is_sync::()` runtime check relied on implicit specialization via `Copy`/`Clone` array behavior, which has changed in Rust 1.86+. `UserDataRef` always taking an exclusive lock even for `Sync` userdata, preventing concurrent shared borrows. With the `send` feature flag enabled, userdata types must now be `Send + Sync`. This is a breaking change, `T: Send + !Sync` userdata types can be wrapped in a `Mutex` or used inside a `Scope` where this restriction is lifted. --- src/conversion.rs | 4 +-- src/lib.rs | 3 ++- src/state.rs | 12 ++++----- src/types.rs | 12 +++++++++ src/userdata.rs | 6 ++--- src/userdata/cell.rs | 49 ++++++++++++++----------------------- src/userdata/ref.rs | 10 +++----- src/userdata/registry.rs | 6 +++++ src/userdata/util.rs | 31 ----------------------- tests/send.rs | 53 +++------------------------------------- 10 files changed, 57 insertions(+), 129 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index b74343db..8de53467 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -16,7 +16,7 @@ use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; use crate::table::Table; use crate::thread::Thread; use crate::traits::{FromLua, IntoLua, ShortTypeName as _}; -use crate::types::{Either, LightUserData, MaybeSend, RegistryKey}; +use crate::types::{Either, LightUserData, MaybeSend, MaybeSync, RegistryKey}; use crate::userdata::{AnyUserData, UserData}; use crate::value::{Nil, Value}; @@ -294,7 +294,7 @@ impl FromLua for AnyUserData { } } -impl IntoLua for T { +impl IntoLua for T { #[inline] fn into_lua(self, lua: &Lua) -> Result { Ok(Value::UserData(lua.create_userdata(self)?)) diff --git a/src/lib.rs b/src/lib.rs index fa3bd1d0..787baa3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,8 @@ pub use crate::traits::{ FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut, ObjectLike, }; pub use crate::types::{ - AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, Number, RegistryKey, VmState, + AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, MaybeSync, Number, RegistryKey, + VmState, }; pub use crate::userdata::{ AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, diff --git a/src/state.rs b/src/state.rs index 9c364a2e..543c0063 100644 --- a/src/state.rs +++ b/src/state.rs @@ -20,8 +20,8 @@ use crate::table::Table; use crate::thread::Thread; use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti}; use crate::types::{ - AppDataRef, AppDataRefMut, ArcReentrantMutexGuard, Integer, LuaType, MaybeSend, Number, ReentrantMutex, - ReentrantMutexGuard, RegistryKey, VmState, XRc, XWeak, + AppDataRef, AppDataRefMut, ArcReentrantMutexGuard, Integer, LuaType, MaybeSend, MaybeSync, Number, + ReentrantMutex, ReentrantMutexGuard, RegistryKey, VmState, XRc, XWeak, }; use crate::userdata::{AnyUserData, UserData, UserDataProxy, UserDataRegistry, UserDataStorage}; use crate::util::{StackGuard, assert_stack, check_stack, protect_lua_closure, push_string, rawset_field}; @@ -1492,7 +1492,7 @@ impl Lua { #[inline] pub fn create_userdata(&self, data: T) -> Result where - T: UserData + MaybeSend + 'static, + T: UserData + MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_userdata(UserDataStorage::new(data)) } } @@ -1503,7 +1503,7 @@ impl Lua { #[inline] pub fn create_ser_userdata(&self, data: T) -> Result where - T: UserData + Serialize + MaybeSend + 'static, + T: UserData + Serialize + MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_userdata(UserDataStorage::new_ser(data)) } } @@ -1518,7 +1518,7 @@ impl Lua { #[inline] pub fn create_any_userdata(&self, data: T) -> Result where - T: MaybeSend + 'static, + T: MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_any_userdata(UserDataStorage::new(data)) } } @@ -1531,7 +1531,7 @@ impl Lua { #[inline] pub fn create_ser_any_userdata(&self, data: T) -> Result where - T: Serialize + MaybeSend + 'static, + T: Serialize + MaybeSend + MaybeSync + 'static, { unsafe { (self.lock()).make_any_userdata(UserDataStorage::new_ser(data)) } } diff --git a/src/types.rs b/src/types.rs index d05d35c2..a6a91030 100644 --- a/src/types.rs +++ b/src/types.rs @@ -128,6 +128,18 @@ pub trait MaybeSend {} #[cfg(not(feature = "send"))] impl MaybeSend for T {} +/// A trait that adds `Sync` requirement if `send` feature is enabled. +#[cfg(feature = "send")] +pub trait MaybeSync: Sync {} +#[cfg(feature = "send")] +impl MaybeSync for T {} + +/// A trait that adds `Sync` requirement if `send` feature is enabled. +#[cfg(not(feature = "send"))] +pub trait MaybeSync {} +#[cfg(not(feature = "send"))] +impl MaybeSync for T {} + pub(crate) struct DestructedUserdata; pub(crate) trait LuaType { diff --git a/src/userdata.rs b/src/userdata.rs index 9a507d70..51ad5178 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -10,7 +10,7 @@ use crate::state::Lua; use crate::string::LuaString; use crate::table::{Table, TablePairs}; use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti}; -use crate::types::{MaybeSend, ValueRef}; +use crate::types::{MaybeSend, MaybeSync, ValueRef}; use crate::util::{StackGuard, check_stack, get_userdata, push_string, short_type_name, take_userdata}; use crate::value::Value; @@ -1216,7 +1216,7 @@ impl AnyUserData { /// Wraps any Rust type, returning an opaque type that implements [`IntoLua`] trait. /// /// This function uses [`Lua::create_any_userdata`] under the hood. - pub fn wrap(data: T) -> impl IntoLua { + pub fn wrap(data: T) -> impl IntoLua { WrappedUserdata(move |lua| lua.create_any_userdata(data)) } @@ -1226,7 +1226,7 @@ impl AnyUserData { /// This function uses [`Lua::create_ser_any_userdata`] under the hood. #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] - pub fn wrap_ser(data: T) -> impl IntoLua { + pub fn wrap_ser(data: T) -> impl IntoLua { WrappedUserdata(move |lua| lua.create_ser_any_userdata(data)) } } diff --git a/src/userdata/cell.rs b/src/userdata/cell.rs index 58f72465..5bd382f8 100644 --- a/src/userdata/cell.rs +++ b/src/userdata/cell.rs @@ -25,7 +25,7 @@ pub(crate) enum UserDataStorage { pub(crate) enum UserDataVariant { Default(XRc>), #[cfg(feature = "serde")] - Serializable(XRc>>, bool), // bool is `is_sync` + Serializable(XRc>>), } impl Clone for UserDataVariant { @@ -34,7 +34,7 @@ impl Clone for UserDataVariant { match self { Self::Default(inner) => Self::Default(XRc::clone(inner)), #[cfg(feature = "serde")] - Self::Serializable(inner, is_sync) => Self::Serializable(XRc::clone(inner), *is_sync), + Self::Serializable(inner) => Self::Serializable(XRc::clone(inner)), } } } @@ -42,10 +42,12 @@ impl Clone for UserDataVariant { impl UserDataVariant { #[inline(always)] pub(super) fn try_borrow_scoped(&self, f: impl FnOnce(&T) -> R) -> Result { - // We don't need to check for `T: Sync` because when this method is used (internally), - // Lua mutex is already locked. - // If non-`Sync` userdata is already borrowed by another thread (via `UserDataRef`), it will be - // exclusively locked. + // Shared (read) lock is always correct for in-place borrows: + // - this method is called internally while the Lua mutex is held, ensuring exclusive Lua-level + // access per call frame + // - with `send` feature, all owned userdata satisfies `T: Sync`, so simultaneous shared references + // from multiple threads are sound + // - without `send` feature, single-threaded execution makes shared lock safe for any `T` let _guard = (self.raw_lock().try_lock_shared_guarded()).map_err(|_| Error::UserDataBorrowError)?; Ok(f(unsafe { &*self.as_ptr() })) } @@ -80,7 +82,7 @@ impl UserDataVariant { Ok(match self { Self::Default(inner) => XRc::into_inner(inner).unwrap().value.into_inner(), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => unsafe { + Self::Serializable(inner) => unsafe { let raw = Box::into_raw(XRc::into_inner(inner).unwrap().value.into_inner()); *Box::from_raw(raw as *mut T) }, @@ -92,7 +94,7 @@ impl UserDataVariant { match self { Self::Default(inner) => XRc::strong_count(inner), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => XRc::strong_count(inner), + Self::Serializable(inner) => XRc::strong_count(inner), } } @@ -101,7 +103,7 @@ impl UserDataVariant { match self { Self::Default(inner) => &inner.raw_lock, #[cfg(feature = "serde")] - Self::Serializable(inner, _) => &inner.raw_lock, + Self::Serializable(inner) => &inner.raw_lock, } } @@ -110,7 +112,7 @@ impl UserDataVariant { match self { Self::Default(inner) => inner.value.get(), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => unsafe { &mut **(inner.value.get() as *mut Box) }, + Self::Serializable(inner) => unsafe { &mut **(inner.value.get() as *mut Box) }, } } } @@ -119,24 +121,10 @@ impl UserDataVariant { impl Serialize for UserDataStorage<()> { fn serialize(&self, serializer: S) -> std::result::Result { match self { - Self::Owned(variant @ UserDataVariant::Serializable(inner, is_sync)) => unsafe { - #[cfg(feature = "send")] - if *is_sync { - let _guard = (variant.raw_lock().try_lock_shared_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } else { - let _guard = (variant.raw_lock().try_lock_exclusive_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } - #[cfg(not(feature = "send"))] - { - let _ = is_sync; - let _guard = (variant.raw_lock().try_lock_shared_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } + Self::Owned(variant @ UserDataVariant::Serializable(inner)) => unsafe { + let _guard = (variant.raw_lock().try_lock_shared_guarded()) + .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; + (*inner.value.get()).serialize(serializer) }, _ => Err(serde::ser::Error::custom("cannot serialize ")), } @@ -201,11 +189,10 @@ impl UserDataStorage { #[inline(always)] pub(crate) fn new_ser(data: T) -> Self where - T: Serialize + crate::types::MaybeSend, + T: Serialize + crate::types::MaybeSend + crate::types::MaybeSync, { let data = Box::new(data) as Box; - let is_sync = super::util::is_sync::(); - let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data)), is_sync); + let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data))); Self::Owned(variant) } diff --git a/src/userdata/ref.rs b/src/userdata/ref.rs index 48f67c28..3cc59ef4 100644 --- a/src/userdata/ref.rs +++ b/src/userdata/ref.rs @@ -12,7 +12,6 @@ use crate::value::Value; use super::cell::{UserDataStorage, UserDataVariant}; use super::lock::{LockGuard, RawLock, UserDataLock}; -use super::util::is_sync; #[cfg(feature = "userdata-wrappers")] use { @@ -63,11 +62,10 @@ impl TryFrom> for UserDataRef { #[inline] fn try_from(variant: UserDataVariant) -> Result { - let guard = if cfg!(not(feature = "send")) || is_sync::() { - variant.raw_lock().try_lock_shared_guarded() - } else { - variant.raw_lock().try_lock_exclusive_guarded() - }; + // Shared (read) lock is always correct: + // - with `send` feature, `T: Sync` is guaranteed by the `MaybeSync` bound on userdata creation + // - without `send` feature, single-threaded access makes shared lock safe for any `T` + let guard = variant.raw_lock().try_lock_shared_guarded(); let guard = guard.map_err(|_| Error::UserDataBorrowError)?; let guard = unsafe { mem::transmute::, LockGuard<'static, _>>(guard) }; Ok(UserDataRef::from_parts(UserDataRefInner::Default(variant), guard)) diff --git a/src/userdata/registry.rs b/src/userdata/registry.rs index e16bec6d..c5138140 100644 --- a/src/userdata/registry.rs +++ b/src/userdata/registry.rs @@ -654,6 +654,12 @@ macro_rules! lua_userdata_impl { // A special proxy object for UserData pub(crate) struct UserDataProxy(pub(crate) PhantomData); +// `UserDataProxy` holds no real `T` value, only a type marker, so it is always safe to send/share. +#[cfg(feature = "send")] +unsafe impl Send for UserDataProxy {} +#[cfg(feature = "send")] +unsafe impl Sync for UserDataProxy {} + lua_userdata_impl!(UserDataProxy); #[cfg(all(feature = "userdata-wrappers", not(feature = "send")))] diff --git a/src/userdata/util.rs b/src/userdata/util.rs index d42eaaed..6c5f0f8f 100644 --- a/src/userdata/util.rs +++ b/src/userdata/util.rs @@ -1,6 +1,4 @@ use std::any::TypeId; -use std::cell::Cell; -use std::marker::PhantomData; use std::os::raw::c_int; use std::ptr; @@ -11,35 +9,6 @@ use crate::error::{Error, Result}; use crate::types::CallbackPtr; use crate::util::{get_userdata, rawget_field, rawset_field, take_userdata}; -// This is a trick to check if a type is `Sync` or not. -// It uses leaked specialization feature from stdlib. -struct IsSync<'a, T> { - is_sync: &'a Cell, - _marker: PhantomData, -} - -impl Clone for IsSync<'_, T> { - fn clone(&self) -> Self { - self.is_sync.set(false); - IsSync { - is_sync: self.is_sync, - _marker: PhantomData, - } - } -} - -impl Copy for IsSync<'_, T> {} - -pub(crate) fn is_sync() -> bool { - let is_sync = Cell::new(true); - let _ = [IsSync:: { - is_sync: &is_sync, - _marker: PhantomData, - }] - .clone(); - is_sync.get() -} - // Userdata type hints, used to match types of wrapped userdata #[derive(Clone, Copy)] pub(crate) struct TypeIdHints { diff --git a/tests/send.rs b/tests/send.rs index f9803f5b..2f10466a 100644 --- a/tests/send.rs +++ b/tests/send.rs @@ -1,50 +1,7 @@ #![cfg(feature = "send")] -use std::cell::UnsafeCell; -use std::marker::PhantomData; - -use mlua::{AnyUserData, Error, Lua, ObjectLike, Result, UserData, UserDataMethods, UserDataRef}; -use static_assertions::{assert_impl_all, assert_not_impl_all}; - -#[test] -fn test_userdata_multithread_access_send_only() -> Result<()> { - let lua = Lua::new(); - - // This type is `Send` but not `Sync`. - struct MyUserData(String, PhantomData>); - assert_impl_all!(MyUserData: Send); - assert_not_impl_all!(MyUserData: Sync); - - impl UserData for MyUserData { - fn add_methods>(methods: &mut M) { - methods.add_method("method", |lua, this, ()| { - let ud = lua.globals().get::("ud")?; - assert_eq!(ud.call_method::("method2", ())?, "method2"); - Ok(this.0.clone()) - }); - - methods.add_method("method2", |_, _, ()| Ok("method2")); - } - } - - lua.globals() - .set("ud", MyUserData("hello".to_string(), PhantomData))?; - - // We acquired the exclusive reference. - let ud = lua.globals().get::>("ud")?; - - std::thread::scope(|s| { - s.spawn(|| { - let res = lua.globals().get::>("ud"); - assert!(matches!(res, Err(Error::UserDataBorrowError))); - }); - }); - - drop(ud); - lua.load("ud:method()").exec().unwrap(); - - Ok(()) -} +use mlua::{AnyUserData, Lua, ObjectLike, Result, UserData, UserDataMethods, UserDataRef}; +use static_assertions::assert_impl_all; #[test] fn test_userdata_multithread_access_sync() -> Result<()> { @@ -74,13 +31,11 @@ fn test_userdata_multithread_access_sync() -> Result<()> { std::thread::scope(|s| { s.spawn(|| { // Getting another shared reference for `Sync` type is allowed. - // FIXME: does not work due to https://github.com/rust-lang/rust/pull/135634 - // let _ = lua.globals().get::>("ud").unwrap(); + let _ = lua.globals().get::>("ud").unwrap(); }); }); - // FIXME: does not work due to https://github.com/rust-lang/rust/pull/135634 - // lua.load("ud:method()").exec().unwrap(); + lua.load("ud:method()").exec().unwrap(); Ok(()) } From 452dc8be888cbb99e04c3773d0f1ff9d1bf9c07c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 21 Feb 2026 15:31:48 +0000 Subject: [PATCH 006/138] clippy --- src/userdata.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/userdata.rs b/src/userdata.rs index 51ad5178..acea3108 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -1091,6 +1091,7 @@ impl AnyUserData { lua.push_ref(&self.0); protect_lua!(state, 1, 1, fn(state) { // Try `__todebugstring` metamethod first, then `__tostring` + #[allow(clippy::collapsible_if)] if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 { if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 { ffi::lua_pushnil(state); From 8fcb6a841673a1820645c1b80d8fcf8d99e97d9a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 21 Feb 2026 15:38:00 +0000 Subject: [PATCH 007/138] Update dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7eceb7f4..29c4b8f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,9 +77,9 @@ static_assertions = "1.0" hyper = { version = "1.2", features = ["full"] } hyper-util = { version = "0.1.3", features = ["full"] } http-body-util = "0.1.1" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.13", features = ["json"] } tempfile = "3" -criterion = { version = "0.7", features = ["async_tokio"] } +criterion = { version = "0.8", features = ["async_tokio"] } rustyline = "17.0" tokio = { version = "1.0", features = ["full"] } From 943c3aed5811c61cd78f57ea6d6141dd2f38a89f Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 21 Feb 2026 21:19:42 +0000 Subject: [PATCH 008/138] Some minor fixes in userdata cell --- src/userdata/cell.rs | 2 +- src/userdata/lock.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/userdata/cell.rs b/src/userdata/cell.rs index 5bd382f8..40025fa4 100644 --- a/src/userdata/cell.rs +++ b/src/userdata/cell.rs @@ -13,7 +13,7 @@ use super::r#ref::{UserDataRef, UserDataRefMut}; type DynSerialize = dyn erased_serde::Serialize; #[cfg(all(feature = "serde", feature = "send"))] -type DynSerialize = dyn erased_serde::Serialize + Send; +type DynSerialize = dyn erased_serde::Serialize + Send + Sync; pub(crate) enum UserDataStorage { Owned(UserDataVariant), diff --git a/src/userdata/lock.rs b/src/userdata/lock.rs index e0e5d1af..b749ed71 100644 --- a/src/userdata/lock.rs +++ b/src/userdata/lock.rs @@ -72,7 +72,7 @@ mod lock_impl { #[inline(always)] fn try_lock_shared(&self) -> bool { - let flag = self.get().wrapping_add(1); + let flag = self.get().checked_add(1).expect("userdata lock count overflow"); if flag <= UNUSED { return false; } From 5776c722089745a72afedc591d9bd9599b2b9155 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 21 Feb 2026 23:15:48 +0000 Subject: [PATCH 009/138] Use `parking_lot::RwLock` in `UserDataCell` container in "send" mode. In non-send mode, mimic the `RwLock` API (using `Cell` counter). We're continue manually operating the underlying `RawRwLock` for flexibility. --- src/userdata/cell.rs | 55 +++++++++++++++++++++++---------------- src/userdata/lock.rs | 61 +++++++++++++++++++++++++++++++------------- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/src/userdata/cell.rs b/src/userdata/cell.rs index 40025fa4..e954b613 100644 --- a/src/userdata/cell.rs +++ b/src/userdata/cell.rs @@ -1,4 +1,4 @@ -use std::cell::{RefCell, UnsafeCell}; +use std::cell::RefCell; #[cfg(feature = "serde")] use serde::ser::{Serialize, Serializer}; @@ -6,7 +6,7 @@ use serde::ser::{Serialize, Serializer}; use crate::error::{Error, Result}; use crate::types::XRc; -use super::lock::{RawLock, UserDataLock}; +use super::lock::{RawLock, RwLock, UserDataLock}; use super::r#ref::{UserDataRef, UserDataRefMut}; #[cfg(all(feature = "serde", not(feature = "send")))] @@ -80,10 +80,12 @@ impl UserDataVariant { return Err(Error::UserDataBorrowMutError); } Ok(match self { - Self::Default(inner) => XRc::into_inner(inner).unwrap().value.into_inner(), + Self::Default(inner) => XRc::into_inner(inner).unwrap().into_value(), #[cfg(feature = "serde")] Self::Serializable(inner) => unsafe { - let raw = Box::into_raw(XRc::into_inner(inner).unwrap().value.into_inner()); + // The serde variant erases `T` to `Box`, so we + // must cast the raw pointer back to recover the concrete type. + let raw = Box::into_raw(XRc::into_inner(inner).unwrap().into_value()); *Box::from_raw(raw as *mut T) }, }) @@ -101,18 +103,18 @@ impl UserDataVariant { #[inline(always)] pub(super) fn raw_lock(&self) -> &RawLock { match self { - Self::Default(inner) => &inner.raw_lock, + Self::Default(inner) => unsafe { inner.raw_lock() }, #[cfg(feature = "serde")] - Self::Serializable(inner) => &inner.raw_lock, + Self::Serializable(inner) => unsafe { inner.raw_lock() }, } } #[inline(always)] pub(super) fn as_ptr(&self) -> *mut T { match self { - Self::Default(inner) => inner.value.get(), + Self::Default(inner) => inner.as_ptr(), #[cfg(feature = "serde")] - Self::Serializable(inner) => unsafe { &mut **(inner.value.get() as *mut Box) }, + Self::Serializable(inner) => unsafe { (&mut **inner.as_ptr()) as *mut DynSerialize as *mut T }, } } } @@ -124,7 +126,7 @@ impl Serialize for UserDataStorage<()> { Self::Owned(variant @ UserDataVariant::Serializable(inner)) => unsafe { let _guard = (variant.raw_lock().try_lock_shared_guarded()) .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) + (*inner.as_ptr()).serialize(serializer) }, _ => Err(serde::ser::Error::custom("cannot serialize ")), } @@ -132,23 +134,32 @@ impl Serialize for UserDataStorage<()> { } /// A type that provides interior mutability for a userdata value (thread-safe). -pub(crate) struct UserDataCell { - raw_lock: RawLock, - value: UnsafeCell, -} - -#[cfg(feature = "send")] -unsafe impl Send for UserDataCell {} -#[cfg(feature = "send")] -unsafe impl Sync for UserDataCell {} +pub(crate) struct UserDataCell(RwLock); impl UserDataCell { #[inline(always)] fn new(value: T) -> Self { - UserDataCell { - raw_lock: RawLock::INIT, - value: UnsafeCell::new(value), - } + UserDataCell(RwLock::new(value)) + } + + /// Returns a reference to the underlying raw lock. + #[inline(always)] + pub(super) unsafe fn raw_lock(&self) -> &RawLock { + self.0.raw() + } + + /// Returns a raw pointer to the wrapped value. + /// + /// The caller is responsible for ensuring the appropriate lock is held. + #[inline(always)] + pub(super) fn as_ptr(&self) -> *mut T { + self.0.data_ptr() + } + + /// Consumes the cell and returns the inner value. + #[inline(always)] + pub(super) fn into_value(self) -> T { + self.0.into_inner() } } diff --git a/src/userdata/lock.rs b/src/userdata/lock.rs index b749ed71..901a557b 100644 --- a/src/userdata/lock.rs +++ b/src/userdata/lock.rs @@ -1,6 +1,4 @@ pub(crate) trait UserDataLock { - const INIT: Self; - fn is_locked(&self) -> bool; fn try_lock_shared(&self) -> bool; fn try_lock_exclusive(&self) -> bool; @@ -48,12 +46,12 @@ impl Drop for LockGuard<'_, L> { } } -pub(crate) use lock_impl::RawLock; +pub(crate) use lock_impl::{RawLock, RwLock}; #[cfg(not(feature = "send"))] #[cfg(not(tarpaulin_include))] mod lock_impl { - use std::cell::Cell; + use std::cell::{Cell, UnsafeCell}; // Positive values represent the number of read references. // Negative values represent the number of write references (only one allowed). @@ -62,9 +60,6 @@ mod lock_impl { const UNUSED: isize = 0; impl super::UserDataLock for RawLock { - #[allow(clippy::declare_interior_mutable_const)] - const INIT: Self = Cell::new(UNUSED); - #[inline(always)] fn is_locked(&self) -> bool { self.get() != UNUSED @@ -104,41 +99,71 @@ mod lock_impl { self.set(flag + 1); } } + + /// A cheap single-threaded read-write lock pairing a `parking_lot::RwLock` type. + pub(crate) struct RwLock { + lock: RawLock, + data: UnsafeCell, + } + + impl RwLock { + /// Creates a new `RwLock` containing the given value. + #[inline(always)] + pub(crate) fn new(value: T) -> Self { + RwLock { + lock: RawLock::new(UNUSED), + data: UnsafeCell::new(value), + } + } + + /// Returns a reference to the underlying raw lock. + #[inline(always)] + pub(crate) unsafe fn raw(&self) -> &RawLock { + &self.lock + } + + /// Returns a raw pointer to the underlying data. + #[inline(always)] + pub(crate) fn data_ptr(&self) -> *mut T { + self.data.get() + } + + /// Consumes this `RwLock`, returning the underlying data. + #[inline(always)] + pub(crate) fn into_inner(self) -> T { + self.data.into_inner() + } + } } #[cfg(feature = "send")] mod lock_impl { - use parking_lot::lock_api::RawRwLock; - - pub(crate) type RawLock = parking_lot::RawRwLock; + pub(crate) use parking_lot::{RawRwLock as RawLock, RwLock}; impl super::UserDataLock for RawLock { - #[allow(clippy::declare_interior_mutable_const)] - const INIT: Self = ::INIT; - #[inline(always)] fn is_locked(&self) -> bool { - RawRwLock::is_locked(self) + parking_lot::lock_api::RawRwLock::is_locked(self) } #[inline(always)] fn try_lock_shared(&self) -> bool { - RawRwLock::try_lock_shared(self) + parking_lot::lock_api::RawRwLock::try_lock_shared(self) } #[inline(always)] fn try_lock_exclusive(&self) -> bool { - RawRwLock::try_lock_exclusive(self) + parking_lot::lock_api::RawRwLock::try_lock_exclusive(self) } #[inline(always)] unsafe fn unlock_shared(&self) { - RawRwLock::unlock_shared(self) + parking_lot::lock_api::RawRwLock::unlock_shared(self) } #[inline(always)] unsafe fn unlock_exclusive(&self) { - RawRwLock::unlock_exclusive(self) + parking_lot::lock_api::RawRwLock::unlock_exclusive(self) } } } From 30cf4bef5817221b7ace93e5ddcff53c4a2f4052 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 00:13:19 +0000 Subject: [PATCH 010/138] Use RwLock directly instead of UserDataCell --- src/userdata/cell.rs | 52 ++++++++++---------------------------------- 1 file changed, 11 insertions(+), 41 deletions(-) diff --git a/src/userdata/cell.rs b/src/userdata/cell.rs index e954b613..f0058fd8 100644 --- a/src/userdata/cell.rs +++ b/src/userdata/cell.rs @@ -23,9 +23,9 @@ pub(crate) enum UserDataStorage { // A enum for storing userdata values. // It's stored inside a Lua VM and protected by the outer `ReentrantMutex`. pub(crate) enum UserDataVariant { - Default(XRc>), + Default(XRc>), #[cfg(feature = "serde")] - Serializable(XRc>>), + Serializable(XRc>>), } impl Clone for UserDataVariant { @@ -80,12 +80,12 @@ impl UserDataVariant { return Err(Error::UserDataBorrowMutError); } Ok(match self { - Self::Default(inner) => XRc::into_inner(inner).unwrap().into_value(), + Self::Default(inner) => XRc::into_inner(inner).unwrap().into_inner(), #[cfg(feature = "serde")] Self::Serializable(inner) => unsafe { // The serde variant erases `T` to `Box`, so we // must cast the raw pointer back to recover the concrete type. - let raw = Box::into_raw(XRc::into_inner(inner).unwrap().into_value()); + let raw = Box::into_raw(XRc::into_inner(inner).unwrap().into_inner()); *Box::from_raw(raw as *mut T) }, }) @@ -103,18 +103,18 @@ impl UserDataVariant { #[inline(always)] pub(super) fn raw_lock(&self) -> &RawLock { match self { - Self::Default(inner) => unsafe { inner.raw_lock() }, + Self::Default(inner) => unsafe { inner.raw() }, #[cfg(feature = "serde")] - Self::Serializable(inner) => unsafe { inner.raw_lock() }, + Self::Serializable(inner) => unsafe { inner.raw() }, } } #[inline(always)] pub(super) fn as_ptr(&self) -> *mut T { match self { - Self::Default(inner) => inner.as_ptr(), + Self::Default(inner) => inner.data_ptr(), #[cfg(feature = "serde")] - Self::Serializable(inner) => unsafe { (&mut **inner.as_ptr()) as *mut DynSerialize as *mut T }, + Self::Serializable(inner) => unsafe { (&mut **inner.data_ptr()) as *mut DynSerialize as *mut T }, } } } @@ -126,43 +126,13 @@ impl Serialize for UserDataStorage<()> { Self::Owned(variant @ UserDataVariant::Serializable(inner)) => unsafe { let _guard = (variant.raw_lock().try_lock_shared_guarded()) .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.as_ptr()).serialize(serializer) + (*inner.data_ptr()).serialize(serializer) }, _ => Err(serde::ser::Error::custom("cannot serialize ")), } } } -/// A type that provides interior mutability for a userdata value (thread-safe). -pub(crate) struct UserDataCell(RwLock); - -impl UserDataCell { - #[inline(always)] - fn new(value: T) -> Self { - UserDataCell(RwLock::new(value)) - } - - /// Returns a reference to the underlying raw lock. - #[inline(always)] - pub(super) unsafe fn raw_lock(&self) -> &RawLock { - self.0.raw() - } - - /// Returns a raw pointer to the wrapped value. - /// - /// The caller is responsible for ensuring the appropriate lock is held. - #[inline(always)] - pub(super) fn as_ptr(&self) -> *mut T { - self.0.data_ptr() - } - - /// Consumes the cell and returns the inner value. - #[inline(always)] - pub(super) fn into_value(self) -> T { - self.0.into_inner() - } -} - pub(crate) enum ScopedUserDataVariant { Ref(*const T), RefMut(RefCell<*mut T>), @@ -183,7 +153,7 @@ impl Drop for ScopedUserDataVariant { impl UserDataStorage { #[inline(always)] pub(crate) fn new(data: T) -> Self { - Self::Owned(UserDataVariant::Default(XRc::new(UserDataCell::new(data)))) + Self::Owned(UserDataVariant::Default(XRc::new(RwLock::new(data)))) } #[inline(always)] @@ -203,7 +173,7 @@ impl UserDataStorage { T: Serialize + crate::types::MaybeSend + crate::types::MaybeSync, { let data = Box::new(data) as Box; - let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data))); + let variant = UserDataVariant::Serializable(XRc::new(RwLock::new(data))); Self::Owned(variant) } From 79d438aaad9593ff8e5eebb83b87e7ca45ddb5f3 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 13:28:48 +0000 Subject: [PATCH 011/138] Build CI docs on main branch --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3a2ad77..662a04a3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,8 @@ -name: Documentation (dev) +name: Documentation (main) on: push: - branches: [dev] + branches: [main] workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages From 0f3fdb05394e104da14cfc6f4d55e7d0759b0bf0 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 14:33:38 +0000 Subject: [PATCH 012/138] Make `string` module public --- src/lib.rs | 7 +++++-- src/string.rs | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 787baa3d..19792ebd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,7 +82,6 @@ mod multi; mod scope; mod state; mod stdlib; -mod string; mod thread; mod traits; mod types; @@ -94,6 +93,7 @@ mod vector; pub mod debug; pub mod function; pub mod prelude; +pub mod string; pub mod table; pub use bstr::BString; @@ -106,7 +106,7 @@ pub use crate::multi::{MultiValue, Variadic}; pub use crate::scope::Scope; pub use crate::state::{GCMode, Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; -pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString, LuaString as String}; +pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; pub use crate::table::Table; pub use crate::thread::{Thread, ThreadStatus}; pub use crate::traits::{ @@ -122,6 +122,9 @@ pub use crate::userdata::{ }; pub use crate::value::{Nil, Value}; +#[doc(hidden)] +pub use crate::string::LuaString as String; + #[cfg(not(feature = "luau"))] pub use crate::debug::HookTriggers; diff --git a/src/string.rs b/src/string.rs index 0a1eff91..711685bf 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,3 +1,13 @@ +//! Lua string handling. +//! +//! This module provides types for working with Lua strings from Rust. +//! +//! # Main Types +//! +//! - [`LuaString`] - A handle to an internal Lua string (may not be valid UTF-8). +//! - [`BorrowedStr`] - A borrowed `&str` view of a Lua string that holds a strong reference to the Lua state. +//! - [`BorrowedBytes`] - A borrowed `&[u8]` view of a Lua string that holds a strong reference to the Lua state. + use std::borrow::{Borrow, Cow}; use std::hash::{Hash, Hasher}; use std::ops::Deref; From 33bf3ffde7c0fc363c09f9bc85e1c59b1e61ab6b Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 14:37:23 +0000 Subject: [PATCH 013/138] Fix doc warnings --- src/debug.rs | 4 ++-- src/state.rs | 4 ++-- src/string.rs | 6 ++++-- src/thread.rs | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/debug.rs b/src/debug.rs index a8527e7b..89d8c501 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,8 +1,8 @@ //! Lua debugging interface. //! //! This module provides access to the Lua debug interface, allowing inspection of the call stack, -//! and function information. The main types are [`Debug`] for accessing debug information and -//! [`HookTriggers`] for configuring debug hooks. +//! and function information. The main types are [`struct@Debug`] for accessing debug information +//! and [`HookTriggers`] for configuring debug hooks. use std::borrow::Cow; use std::os::raw::c_int; diff --git a/src/state.rs b/src/state.rs index 543c0063..6da2eefb 100644 --- a/src/state.rs +++ b/src/state.rs @@ -909,8 +909,8 @@ impl Lua { /// Gets information about the interpreter runtime stack at the given level. /// - /// This function calls callback `f`, passing the [`Debug`] structure that can be used to get - /// information about the function executing at a given level. + /// This function calls callback `f`, passing the [`struct@Debug`] structure that can be used to + /// get information about the function executing at a given level. /// Level `0` is the current running function, whereas level `n+1` is the function that has /// called level `n` (except for tail calls, which do not count in the stack). pub fn inspect_stack(&self, level: usize, f: impl FnOnce(&Debug) -> R) -> Option { diff --git a/src/string.rs b/src/string.rs index 711685bf..3428ce69 100644 --- a/src/string.rs +++ b/src/string.rs @@ -5,8 +5,10 @@ //! # Main Types //! //! - [`LuaString`] - A handle to an internal Lua string (may not be valid UTF-8). -//! - [`BorrowedStr`] - A borrowed `&str` view of a Lua string that holds a strong reference to the Lua state. -//! - [`BorrowedBytes`] - A borrowed `&[u8]` view of a Lua string that holds a strong reference to the Lua state. +//! - [`BorrowedStr`] - A borrowed `&str` view of a Lua string that holds a strong reference to the +//! Lua state. +//! - [`BorrowedBytes`] - A borrowed `&[u8]` view of a Lua string that holds a strong reference to +//! the Lua state. use std::borrow::{Borrow, Cow}; use std::hash::{Hash, Hasher}; diff --git a/src/thread.rs b/src/thread.rs index 6941dc65..03ac4137 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -449,6 +449,8 @@ impl Thread { /// Please note that Luau links environment table with chunk when loading it into Lua state. /// Therefore you need to load chunks into a thread to link with the thread environment. /// + /// [`Lua::sandbox`]: crate::Lua::sandbox + /// /// # Examples /// /// ``` From eb76db59da141eea6ba4262ff9633bb8f5b7e8d2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 19:52:57 +0000 Subject: [PATCH 014/138] Make `userdata` module public --- src/lib.rs | 13 ++++++++----- src/userdata.rs | 16 ++++++++++++++++ tests/userdata.rs | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 19792ebd..d34b78b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,7 +85,6 @@ mod stdlib; mod thread; mod traits; mod types; -mod userdata; mod util; mod value; mod vector; @@ -95,6 +94,7 @@ pub mod function; pub mod prelude; pub mod string; pub mod table; +pub mod userdata; pub use bstr::BString; pub use ffi::{self, lua_CFunction, lua_State}; @@ -116,14 +116,17 @@ pub use crate::types::{ AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, MaybeSync, Number, RegistryKey, VmState, }; -pub use crate::userdata::{ - AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, - UserDataRefMut, UserDataRegistry, -}; +pub use crate::userdata::AnyUserData; pub use crate::value::{Nil, Value}; +// Re-export some types to keep backward compatibility and avoid breaking changes in the public API. #[doc(hidden)] pub use crate::string::LuaString as String; +#[doc(hidden)] +pub use crate::userdata::{ + MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, UserDataRefMut, + UserDataRegistry, +}; #[cfg(not(feature = "luau"))] pub use crate::debug::HookTriggers; diff --git a/src/userdata.rs b/src/userdata.rs index acea3108..6e5211eb 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -1,3 +1,19 @@ +//! Lua userdata handling. +//! +//! This module provides types for creating and working with Lua userdata from Rust. +//! +//! # Main Types +//! +//! - [`AnyUserData`] - A handle to a Lua userdata value of any Rust type. +//! - [`UserData`] - Trait to implement for types that should be exposed to Lua as userdata. +//! - [`UserDataFields`] - Trait for registering fields on userdata types. +//! - [`UserDataMethods`] - Trait for registering methods on userdata types. +//! - [`UserDataRegistry`] - Registry for userdata methods and fields. +//! - [`UserDataMetatable`] - A handle to the metatable of a userdata type. +//! - [`UserDataRef`] - A borrowed reference to a userdata value. +//! - [`UserDataRefMut`] - A mutably borrowed reference to a userdata value. +//! - [`MetaMethod`] - Metamethod names for customizing Lua operators. + use std::any::TypeId; use std::ffi::CStr; use std::fmt; diff --git a/tests/userdata.rs b/tests/userdata.rs index df5ed1f4..4af59814 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -1376,7 +1376,7 @@ fn test_userdata_namecall() -> Result<()> { struct MyUserData; impl UserData for MyUserData { - fn register(registry: &mut mlua::UserDataRegistry) { + fn register(registry: &mut UserDataRegistry) { registry.add_method("method", |_, _, ()| Ok("method called")); registry.add_field_method_get("field", |_, _| Ok("field value")); From 35294359adb4d5a4a6d854662663899d5c1c2946 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 22 Feb 2026 19:58:28 +0000 Subject: [PATCH 015/138] Inline doc for some types --- src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index d34b78b1..10a6d3a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,12 +101,15 @@ pub use ffi::{self, lua_CFunction, lua_State}; pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; pub use crate::error::{Error, ErrorContext, ExternalError, ExternalResult, Result}; +#[doc(inline)] pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; pub use crate::scope::Scope; pub use crate::state::{GCMode, Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; +#[doc(inline)] pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; +#[doc(inline)] pub use crate::table::Table; pub use crate::thread::{Thread, ThreadStatus}; pub use crate::traits::{ @@ -116,6 +119,7 @@ pub use crate::types::{ AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, MaybeSync, Number, RegistryKey, VmState, }; +#[doc(inline)] pub use crate::userdata::AnyUserData; pub use crate::value::{Nil, Value}; @@ -129,6 +133,7 @@ pub use crate::userdata::{ }; #[cfg(not(feature = "luau"))] +#[doc(inline)] pub use crate::debug::HookTriggers; #[cfg(any(feature = "luau", doc))] From 88177203620db5e75a1d131dba4329db97a0b191 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 23 Feb 2026 09:51:24 +0000 Subject: [PATCH 016/138] Re-export (hidden) `TablePairs` and `TableSequence` --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 10a6d3a8..d12101bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,8 @@ pub use crate::value::{Nil, Value}; #[doc(hidden)] pub use crate::string::LuaString as String; #[doc(hidden)] +pub use crate::table::{TablePairs, TableSequence}; +#[doc(hidden)] pub use crate::userdata::{ MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, UserDataRefMut, UserDataRegistry, From bf0c96908f06ce7491497b7e392835054f8dc0ce Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 23 Feb 2026 10:24:21 +0000 Subject: [PATCH 017/138] Remove lifetime from `BorrowedStr` and `BorrowedBytes` The underlying `ValueRef` is cheap to clone as only increases reference count, instead of allocating a new Lua stack slot. --- src/conversion.rs | 50 ++++++++++----------------- src/string.rs | 88 +++++++++++++++++++++++++---------------------- src/value.rs | 2 +- 3 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 8de53467..f2f7ca5a 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -4,7 +4,7 @@ use std::ffi::{CStr, CString, OsStr, OsString}; use std::hash::{BuildHasher, Hash}; use std::os::raw::c_int; use std::path::{Path, PathBuf}; -use std::{mem, slice, str}; +use std::{slice, str}; use bstr::{BStr, BString, ByteVec}; use num_traits::cast; @@ -86,91 +86,79 @@ impl FromLua for LuaString { } } -impl IntoLua for BorrowedStr<'_> { +impl IntoLua for BorrowedStr { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.into_owned())) + Ok(Value::String(LuaString(self.vref))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl IntoLua for &BorrowedStr<'_> { +impl IntoLua for &BorrowedStr { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.clone().into_owned())) + Ok(Value::String(LuaString(self.vref.clone()))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl FromLua for BorrowedStr<'_> { +impl FromLua for BorrowedStr { fn from_lua(value: Value, lua: &Lua) -> Result { let s = LuaString::from_lua(value, lua)?; - let BorrowedStr { buf, _lua, .. } = BorrowedStr::try_from(&s)?; - let buf = unsafe { mem::transmute::<&str, &'static str>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + BorrowedStr::try_from(&s) } unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { let s = LuaString::from_stack(idx, lua)?; - let BorrowedStr { buf, _lua, .. } = BorrowedStr::try_from(&s)?; - let buf = unsafe { mem::transmute::<&str, &'static str>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + BorrowedStr::try_from(&s) } } -impl IntoLua for BorrowedBytes<'_> { +impl IntoLua for BorrowedBytes { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.into_owned())) + Ok(Value::String(LuaString(self.vref))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl IntoLua for &BorrowedBytes<'_> { +impl IntoLua for &BorrowedBytes { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.clone().into_owned())) + Ok(Value::String(LuaString(self.vref.clone()))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl FromLua for BorrowedBytes<'_> { +impl FromLua for BorrowedBytes { fn from_lua(value: Value, lua: &Lua) -> Result { let s = LuaString::from_lua(value, lua)?; - let BorrowedBytes { buf, _lua, .. } = BorrowedBytes::from(&s); - let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + Ok(BorrowedBytes::from(&s)) } unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { let s = LuaString::from_stack(idx, lua)?; - let BorrowedBytes { buf, _lua, .. } = BorrowedBytes::from(&s); - let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + Ok(BorrowedBytes::from(&s)) } } diff --git a/src/string.rs b/src/string.rs index 3428ce69..3093c99c 100644 --- a/src/string.rs +++ b/src/string.rs @@ -10,11 +10,11 @@ //! - [`BorrowedBytes`] - A borrowed `&[u8]` view of a Lua string that holds a strong reference to //! the Lua state. -use std::borrow::{Borrow, Cow}; +use std::borrow::Borrow; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::os::raw::{c_int, c_void}; -use std::{cmp, fmt, slice, str}; +use std::{cmp, fmt, mem, slice, str}; use crate::error::{Error, Result}; use crate::state::Lua; @@ -37,6 +37,9 @@ pub struct LuaString(pub(crate) ValueRef); impl LuaString { /// Get a [`BorrowedStr`] if the Lua string is valid UTF-8. /// + /// The returned `BorrowedStr` holds a strong reference to the Lua state to guarantee the + /// validity of the underlying data. + /// /// # Examples /// /// ``` @@ -54,7 +57,7 @@ impl LuaString { /// # } /// ``` #[inline] - pub fn to_str(&self) -> Result> { + pub fn to_str(&self) -> Result { BorrowedStr::try_from(self) } @@ -97,8 +100,9 @@ impl LuaString { /// Get the bytes that make up this string. /// - /// The returned slice will not contain the terminating null byte, but will contain any null - /// bytes embedded into the Lua string. + /// The returned `BorrowedStr` holds a strong reference to the Lua state to guarantee the + /// validity of the underlying data. The data will not contain the terminating null byte, but + /// will contain any null bytes embedded into the Lua string. /// /// # Examples /// @@ -113,16 +117,16 @@ impl LuaString { /// # } /// ``` #[inline] - pub fn as_bytes(&self) -> BorrowedBytes<'_> { + pub fn as_bytes(&self) -> BorrowedBytes { BorrowedBytes::from(self) } /// Get the bytes that make up this string, including the trailing null byte. - pub fn as_bytes_with_nul(&self) -> BorrowedBytes<'_> { - let BorrowedBytes { buf, borrow, _lua } = BorrowedBytes::from(self); + pub fn as_bytes_with_nul(&self) -> BorrowedBytes { + let BorrowedBytes { buf, vref, _lua } = BorrowedBytes::from(self); // Include the trailing null byte (it's always present but excluded by default) let buf = unsafe { slice::from_raw_parts((*buf).as_ptr(), (*buf).len() + 1) }; - BorrowedBytes { buf, borrow, _lua } + BorrowedBytes { buf, vref, _lua } } // Does not return the terminating null byte @@ -245,14 +249,14 @@ impl fmt::Display for Display<'_> { } /// A borrowed string (`&str`) that holds a strong reference to the Lua state. -pub struct BorrowedStr<'a> { +pub struct BorrowedStr { // `buf` points to a readonly memory managed by Lua - pub(crate) buf: &'a str, - pub(crate) borrow: Cow<'a, LuaString>, + pub(crate) buf: &'static str, + pub(crate) vref: ValueRef, pub(crate) _lua: Lua, } -impl Deref for BorrowedStr<'_> { +impl Deref for BorrowedStr { type Target = str; #[inline(always)] @@ -261,33 +265,33 @@ impl Deref for BorrowedStr<'_> { } } -impl Borrow for BorrowedStr<'_> { +impl Borrow for BorrowedStr { #[inline(always)] fn borrow(&self) -> &str { self.buf } } -impl AsRef for BorrowedStr<'_> { +impl AsRef for BorrowedStr { #[inline(always)] fn as_ref(&self) -> &str { self.buf } } -impl fmt::Display for BorrowedStr<'_> { +impl fmt::Display for BorrowedStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl fmt::Debug for BorrowedStr<'_> { +impl fmt::Debug for BorrowedStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl PartialEq for BorrowedStr<'_> +impl PartialEq for BorrowedStr where T: AsRef, { @@ -296,9 +300,9 @@ where } } -impl Eq for BorrowedStr<'_> {} +impl Eq for BorrowedStr {} -impl PartialOrd for BorrowedStr<'_> +impl PartialOrd for BorrowedStr where T: AsRef, { @@ -307,33 +311,33 @@ where } } -impl Ord for BorrowedStr<'_> { +impl Ord for BorrowedStr { fn cmp(&self, other: &Self) -> cmp::Ordering { self.buf.cmp(other.buf) } } -impl<'a> TryFrom<&'a LuaString> for BorrowedStr<'a> { +impl TryFrom<&LuaString> for BorrowedStr { type Error = Error; #[inline] - fn try_from(value: &'a LuaString) -> Result { - let BorrowedBytes { buf, borrow, _lua } = BorrowedBytes::from(value); + fn try_from(value: &LuaString) -> Result { + let BorrowedBytes { buf, vref, _lua } = BorrowedBytes::from(value); let buf = str::from_utf8(buf).map_err(|e| Error::from_lua_conversion("string", "&str", e.to_string()))?; - Ok(Self { buf, borrow, _lua }) + Ok(Self { buf, vref, _lua }) } } /// A borrowed byte slice (`&[u8]`) that holds a strong reference to the Lua state. -pub struct BorrowedBytes<'a> { +pub struct BorrowedBytes { // `buf` points to a readonly memory managed by Lua - pub(crate) buf: &'a [u8], - pub(crate) borrow: Cow<'a, LuaString>, + pub(crate) buf: &'static [u8], + pub(crate) vref: ValueRef, pub(crate) _lua: Lua, } -impl Deref for BorrowedBytes<'_> { +impl Deref for BorrowedBytes { type Target = [u8]; #[inline(always)] @@ -342,27 +346,27 @@ impl Deref for BorrowedBytes<'_> { } } -impl Borrow<[u8]> for BorrowedBytes<'_> { +impl Borrow<[u8]> for BorrowedBytes { #[inline(always)] fn borrow(&self) -> &[u8] { self.buf } } -impl AsRef<[u8]> for BorrowedBytes<'_> { +impl AsRef<[u8]> for BorrowedBytes { #[inline(always)] fn as_ref(&self) -> &[u8] { self.buf } } -impl fmt::Debug for BorrowedBytes<'_> { +impl fmt::Debug for BorrowedBytes { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl PartialEq for BorrowedBytes<'_> +impl PartialEq for BorrowedBytes where T: AsRef<[u8]>, { @@ -371,9 +375,9 @@ where } } -impl Eq for BorrowedBytes<'_> {} +impl Eq for BorrowedBytes {} -impl PartialOrd for BorrowedBytes<'_> +impl PartialOrd for BorrowedBytes where T: AsRef<[u8]>, { @@ -382,13 +386,13 @@ where } } -impl Ord for BorrowedBytes<'_> { +impl Ord for BorrowedBytes { fn cmp(&self, other: &Self) -> cmp::Ordering { self.buf.cmp(other.buf) } } -impl<'a> IntoIterator for &'a BorrowedBytes<'_> { +impl<'a> IntoIterator for &'a BorrowedBytes { type Item = &'a u8; type IntoIter = slice::Iter<'a, u8>; @@ -397,12 +401,14 @@ impl<'a> IntoIterator for &'a BorrowedBytes<'_> { } } -impl<'a> From<&'a LuaString> for BorrowedBytes<'a> { +impl From<&LuaString> for BorrowedBytes { #[inline] - fn from(value: &'a LuaString) -> Self { + fn from(value: &LuaString) -> Self { let (buf, _lua) = unsafe { value.to_slice() }; - let borrow = Cow::Borrowed(value); - Self { buf, borrow, _lua } + let vref = value.0.clone(); + // SAFETY: The `buf` is valid for the lifetime of the Lua state and occupied slot index + let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; + Self { buf, vref, _lua } } } diff --git a/src/value.rs b/src/value.rs index 8d178e04..10d4c4d0 100644 --- a/src/value.rs +++ b/src/value.rs @@ -361,7 +361,7 @@ impl Value { note = "This method does not follow Rust naming convention. Use `as_string().and_then(|s| s.to_str().ok())` instead." )] #[inline] - pub fn as_str(&self) -> Option> { + pub fn as_str(&self) -> Option { self.as_string().and_then(|s| s.to_str().ok()) } From 47e6a37323cd7df96dcdc869d773c79b81cf9e9d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Feb 2026 11:46:05 +0000 Subject: [PATCH 018/138] Add shortcuts to check thread status (`Thread::is_resumable()`, `Thread::is_finished()` etc) --- src/thread.rs | 25 +++++++++++++++++++++++++ tests/async.rs | 4 ++-- tests/hooks.rs | 8 ++++---- tests/luau.rs | 8 +++----- tests/thread.rs | 36 ++++++++++++++++++------------------ 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/thread.rs b/src/thread.rs index 03ac4137..74bced65 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -259,6 +259,31 @@ impl Thread { } } + /// Returns `true` if this thread is resumable (meaning it can be resumed by calling + /// [`Thread::resume`]). + #[inline(always)] + pub fn is_resumable(&self) -> bool { + self.status() == ThreadStatus::Resumable + } + + /// Returns `true` if this thread is currently running. + #[inline(always)] + pub fn is_running(&self) -> bool { + self.status() == ThreadStatus::Running + } + + /// Returns `true` if this thread has finished executing. + #[inline(always)] + pub fn is_finished(&self) -> bool { + self.status() == ThreadStatus::Finished + } + + /// Returns `true` if this thread has raised a Lua error during execution. + #[inline(always)] + pub fn is_error(&self) -> bool { + self.status() == ThreadStatus::Error + } + /// Sets a hook function that will periodically be called as Lua code executes. /// /// This function is similar or [`Lua::set_hook`] except that it sets for the thread. diff --git a/tests/async.rs b/tests/async.rs index 22df2ab3..16ddd9ee 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -7,7 +7,7 @@ use futures_util::stream::TryStreamExt; use tokio::sync::Mutex; use mlua::{ - Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, ThreadStatus, UserData, + Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, UserData, UserDataMethods, UserDataRef, Value, }; @@ -714,7 +714,7 @@ fn test_async_yield_with() -> Result<()> { assert_eq!(thread.resume::<(i32, i32)>((10, 11))?, (21, 110)); assert_eq!(thread.resume::<(i32, i32)>((11, 12))?, (23, 132)); assert_eq!(thread.resume::<(i32, i32)>((12, 13))?, (0, 0)); - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); Ok(()) } diff --git a/tests/hooks.rs b/tests/hooks.rs index f1b72445..9d68c84b 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex}; use mlua::debug::DebugEvent; -use mlua::{Error, HookTriggers, Lua, Result, ThreadStatus, Value, VmState}; +use mlua::{Error, HookTriggers, Lua, Result, Value, VmState}; #[test] fn test_hook_triggers() { @@ -281,14 +281,14 @@ fn test_hook_yield() -> Result<()> { assert!(co.resume::<()>(()).is_ok()); assert!(co.resume::<()>(()).is_ok()); assert!(co.resume::<()>(()).is_ok()); - assert!(co.status() == ThreadStatus::Finished); + assert!(co.is_finished()); } #[cfg(any(feature = "lua51", feature = "lua52", feature = "luajit"))] { assert!( matches!(co.resume::<()>(()), Err(Error::RuntimeError(err)) if err.contains("attempt to yield from a hook")) ); - assert!(co.status() == ThreadStatus::Error); + assert!(co.is_error()); } Ok(()) @@ -321,7 +321,7 @@ fn test_global_hook() -> Result<()> { thread.resume::<()>(()).unwrap(); lua.remove_global_hook(); thread.resume::<()>(()).unwrap(); - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); assert_eq!(counter.load(Ordering::Relaxed), 3); Ok(()) diff --git a/tests/luau.rs b/tests/luau.rs index 8f745768..5143e7ac 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -6,9 +6,7 @@ use std::os::raw::c_void; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; -use mlua::{ - Compiler, Error, Function, Lua, LuaOptions, Result, StdLib, Table, ThreadStatus, Value, Vector, VmState, -}; +use mlua::{Compiler, Error, Function, Lua, LuaOptions, Result, StdLib, Table, Value, Vector, VmState}; #[test] fn test_version() -> Result<()> { @@ -324,11 +322,11 @@ fn test_interrupts() -> Result<()> { .into_function()?, )?; co.resume::<()>(())?; - assert_eq!(co.status(), ThreadStatus::Resumable); + assert!(co.is_resumable()); let result: i32 = co.resume(())?; assert_eq!(result, 6); assert_eq!(yield_count.load(Ordering::Relaxed), 7); - assert_eq!(co.status(), ThreadStatus::Finished); + assert!(co.is_finished()); // Test no yielding at non-yieldable points yield_count.store(0, Ordering::Relaxed); diff --git a/tests/thread.rs b/tests/thread.rs index 71eb24c9..98b861f8 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -1,6 +1,6 @@ use std::panic::catch_unwind; -use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadStatus, Value}; +use mlua::{Error, Function, IntoLua, Lua, Result, Thread, Value}; #[test] fn test_thread() -> Result<()> { @@ -21,17 +21,17 @@ fn test_thread() -> Result<()> { .eval()?, )?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(0)?, 0); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(1)?, 1); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(2)?, 3); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(3)?, 6); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(4)?, 10); - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); let accumulate = lua.create_thread( lua.load( @@ -50,9 +50,9 @@ fn test_thread() -> Result<()> { accumulate.resume::<()>(i)?; } assert_eq!(accumulate.resume::(4)?, 10); - assert_eq!(accumulate.status(), ThreadStatus::Resumable); + assert!(accumulate.is_resumable()); assert!(accumulate.resume::<()>("error").is_err()); - assert_eq!(accumulate.status(), ThreadStatus::Error); + assert!(accumulate.is_error()); let thread = lua .load( @@ -65,7 +65,7 @@ fn test_thread() -> Result<()> { "#, ) .eval::()?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(())?, 42); let thread: Thread = lua @@ -92,7 +92,7 @@ fn test_thread() -> Result<()> { // Already running thread must be unresumable let thread = lua.create_thread(lua.create_function(|lua, ()| { - assert_eq!(lua.current_thread().status(), ThreadStatus::Running); + assert!(lua.current_thread().is_running()); let result = lua.current_thread().resume::<()>(()); assert!( matches!(result, Err(Error::CoroutineUnresumable)), @@ -123,12 +123,12 @@ fn test_thread_reset() -> Result<()> { assert!(thread.reset(func.clone()).is_ok()); for _ in 0..2 { - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); let _ = thread.resume::(MyUserData(arc.clone()))?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(Arc::strong_count(&arc), 2); thread.resume::<()>(())?; - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); thread.reset(func.clone())?; lua.gc_collect()?; assert_eq!(Arc::strong_count(&arc), 1); @@ -138,21 +138,21 @@ fn test_thread_reset() -> Result<()> { let func: Function = lua.load(r#"function(ud) error("test error") end"#).eval()?; let thread = lua.create_thread(func.clone())?; let _ = thread.resume::(MyUserData(arc.clone())); - assert_eq!(thread.status(), ThreadStatus::Error); + assert!(thread.is_error()); assert_eq!(Arc::strong_count(&arc), 2); #[cfg(any(feature = "lua55", feature = "lua54"))] { assert!(thread.reset(func.clone()).is_err()); // Reset behavior has changed in Lua v5.4.4 // It's became possible to force reset thread by popping error object - assert!(matches!(thread.status(), ThreadStatus::Finished)); + assert!(thread.is_finished()); assert!(thread.reset(func.clone()).is_ok()); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); } #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] { assert!(thread.reset(func.clone()).is_ok()); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); } // Try reset running thread From f1a97e4193e3d617ac743da176ab61fd72b3212c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Feb 2026 11:48:59 +0000 Subject: [PATCH 019/138] Open `Thread::state()` that returns `*mut lua_State` pointer. --- src/thread.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/thread.rs b/src/thread.rs index 74bced65..86e8223b 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -92,7 +92,6 @@ pub struct AsyncThread { impl Thread { /// Returns reference to the Lua state that this thread is associated with. - #[doc(hidden)] #[inline(always)] pub fn state(&self) -> *mut ffi::lua_State { self.1 From a45fe9bb936909adf5e2a25b2bef80640de835cb Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Feb 2026 12:03:37 +0000 Subject: [PATCH 020/138] Bump luau-src to 0.19 (Luau 0.710) --- mlua-sys/Cargo.toml | 2 +- mlua-sys/src/luau/lua.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index 2230af75..c6804892 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -43,7 +43,7 @@ cfg-if = "1.0" pkg-config = "0.3.17" lua-src = { version = ">= 550.0.0, < 550.1.0", optional = true } luajit-src = { version = ">= 210.6.0, < 210.7.0", optional = true } -luau0-src = { version = "0.18.0", optional = true } +luau0-src = { version = "0.19.0", optional = true } [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/src/luau/lua.rs b/mlua-sys/src/luau/lua.rs index 98ee5bc7..15862331 100644 --- a/mlua-sys/src/luau/lua.rs +++ b/mlua-sys/src/luau/lua.rs @@ -501,6 +501,12 @@ pub type lua_Coverage = unsafe extern "C-unwind" fn( size: usize, ); +pub type lua_CounterFunction = + unsafe extern "C-unwind" fn(context: *mut c_void, function: *const c_char, linedefined: c_int); + +pub type lua_CounterValue = + unsafe extern "C-unwind" fn(context: *mut c_void, kind: c_int, line: c_int, hits: u64); + unsafe extern "C-unwind" { pub fn lua_stackdepth(L: *mut lua_State) -> c_int; pub fn lua_getinfo(L: *mut lua_State, level: c_int, what: *const c_char, ar: *mut lua_Debug) -> c_int; @@ -515,6 +521,14 @@ unsafe extern "C-unwind" { pub fn lua_getcoverage(L: *mut lua_State, funcindex: c_int, context: *mut c_void, callback: lua_Coverage); + pub fn lua_getcounters( + L: *mut lua_State, + funcindex: c_int, + context: *mut c_void, + functionvisit: lua_CounterFunction, + countervisit: lua_CounterValue, + ); + pub fn lua_debugtrace(L: *mut lua_State) -> *const c_char; } @@ -552,7 +566,7 @@ pub struct lua_Callbacks { /// gets called when L is created (LP == parent) or destroyed (LP == NULL) pub userthread: Option, /// gets called when a string is created; returned atom can be retrieved via tostringatom - pub useratom: Option i16>, + pub useratom: Option i16>, /// gets called when BREAK instruction is encountered pub debugbreak: Option, From c91066006f5796ab644e04cc6852237935391c99 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Feb 2026 14:44:50 +0000 Subject: [PATCH 021/138] Make `thread` module public --- src/lib.rs | 10 +++++++--- src/prelude.rs | 13 +++++++------ src/thread.rs | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d12101bf..5573db90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ //! [`Future`]: std::future::Future //! [`serde::Serialize`]: https://docs.serde.rs/serde/ser/trait.Serialize.html //! [`serde::Deserialize`]: https://docs.serde.rs/serde/de/trait.Deserialize.html +//! [`AsyncThread`]: crate::thread::AsyncThread // Deny warnings inside doc tests / examples. When this isn't present, rustdoc doesn't show *any* // warnings at all. @@ -82,7 +83,6 @@ mod multi; mod scope; mod state; mod stdlib; -mod thread; mod traits; mod types; mod util; @@ -94,6 +94,7 @@ pub mod function; pub mod prelude; pub mod string; pub mod table; +pub mod thread; pub mod userdata; pub use bstr::BString; @@ -111,7 +112,8 @@ pub use crate::stdlib::StdLib; pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; #[doc(inline)] pub use crate::table::Table; -pub use crate::thread::{Thread, ThreadStatus}; +#[doc(inline)] +pub use crate::thread::Thread; pub use crate::traits::{ FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut, ObjectLike, }; @@ -133,6 +135,8 @@ pub use crate::userdata::{ MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, UserDataRefMut, UserDataRegistry, }; +#[doc(hidden)] +pub use thread::ThreadStatus; #[cfg(not(feature = "luau"))] #[doc(inline)] @@ -149,7 +153,7 @@ pub use crate::{ #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] -pub use crate::{thread::AsyncThread, traits::LuaNativeAsyncFn}; +pub use crate::traits::LuaNativeAsyncFn; #[cfg(feature = "serde")] #[doc(inline)] diff --git a/src/prelude.rs b/src/prelude.rs index 23cdf85d..f5d670a2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,12 +9,13 @@ pub use crate::{ LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, - Table as LuaTable, Thread as LuaThread, ThreadStatus as LuaThreadStatus, UserData as LuaUserData, - UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, - UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, - UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, - Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, + Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, + UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, + UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, + UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, + VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, + thread::ThreadStatus as LuaThreadStatus, }; #[cfg(not(feature = "luau"))] @@ -30,7 +31,7 @@ pub use crate::{ #[cfg(feature = "async")] #[doc(no_inline)] -pub use crate::{AsyncThread as LuaAsyncThread, LuaNativeAsyncFn}; +pub use crate::{LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThread}; #[cfg(feature = "serde")] #[doc(no_inline)] diff --git a/src/thread.rs b/src/thread.rs index 86e8223b..561c0bb8 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -1,3 +1,40 @@ +//! Lua thread (coroutine) handling. +//! +//! This module provides types for creating and working with Lua coroutines from Rust. +//! Coroutines allow cooperative multitasking within a single Lua state by suspending and +//! resuming execution at well-defined yield points. +//! +//! # Basic Usage +//! +//! Threads are created via [`Lua::create_thread`] and driven by calling [`Thread::resume`]: +//! +//! ```rust +//! # use mlua::{Lua, Result, Thread}; +//! # fn main() -> Result<()> { +//! let lua = Lua::new(); +//! let thread: Thread = lua.load(r#" +//! coroutine.create(function(a, b) +//! coroutine.yield(a + b) +//! return a * b +//! end) +//! "#).eval()?; +//! +//! assert_eq!(thread.resume::((3, 4))?, 7); +//! assert_eq!(thread.resume::(())?, 12); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Async Support +//! +//! When the `async` feature is enabled, a [`Thread`] can be converted into an [`AsyncThread`] +//! via [`Thread::into_async`], which implements both [`Future`] and [`Stream`]. +//! This integrates Lua coroutines naturally with Rust async runtimes such as Tokio. +//! +//! [`Lua::create_thread`]: crate::Lua::create_thread +//! [`Future`]: std::future::Future +//! [`Stream`]: futures_util::stream::Stream + use std::fmt; use std::os::raw::{c_int, c_void}; From efd085603367ef969cb85b7b0ad37333226027e7 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Feb 2026 14:52:59 +0000 Subject: [PATCH 022/138] Derive `PartialEq` for `Thread` --- src/thread.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/thread.rs b/src/thread.rs index 561c0bb8..935f4546 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -106,7 +106,7 @@ impl ThreadStatusInner { } /// Handle to an internal Lua thread (coroutine). -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct Thread(pub(crate) ValueRef, pub(crate) *mut ffi::lua_State); #[cfg(feature = "send")] @@ -565,12 +565,6 @@ impl fmt::Debug for Thread { } } -impl PartialEq for Thread { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - impl LuaType for Thread { const TYPE_ID: c_int = ffi::LUA_TTHREAD; } From a959b98d3062856d1737e927f387eaf3df95c2fe Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 1 Mar 2026 16:11:38 +0000 Subject: [PATCH 023/138] Add chunk module doc and update prelude re-exports --- src/chunk.rs | 7 +++++++ src/lib.rs | 4 ++-- src/prelude.rs | 12 ++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index 3aedfb43..5426411b 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,3 +1,10 @@ +//! Lua chunk loading and execution. +//! +//! This module provides types for loading Lua source code or bytecode into a [`Chunk`], +//! configuring how it is compiled and executed, and converting it into a callable [`Function`]. +//! +//! Chunks can be loaded from strings, byte slices, or files via the [`AsChunk`] trait. + use std::borrow::Cow; use std::collections::HashMap; use std::ffi::CString; diff --git a/src/lib.rs b/src/lib.rs index 5573db90..97a2669b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,12 +131,12 @@ pub use crate::string::LuaString as String; #[doc(hidden)] pub use crate::table::{TablePairs, TableSequence}; #[doc(hidden)] +pub use crate::thread::ThreadStatus; +#[doc(hidden)] pub use crate::userdata::{ MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, UserDataRefMut, UserDataRegistry, }; -#[doc(hidden)] -pub use thread::ThreadStatus; #[cfg(not(feature = "luau"))] #[doc(inline)] diff --git a/src/prelude.rs b/src/prelude.rs index f5d670a2..a10ab987 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,10 +3,10 @@ #[doc(no_inline)] pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, - Chunk as LuaChunk, Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, - ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti, - Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger, IntoLua, IntoLuaMulti, - LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, + Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, + ErrorContext as LuaErrorContext, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, + FromLua, FromLuaMulti, Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger, IntoLua, + IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, @@ -25,8 +25,8 @@ pub use crate::HookTriggers as LuaHookTriggers; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, NavigateError as LuaNavigateError, Require as LuaRequire, - TextRequirer as LuaTextRequirer, Vector as LuaVector, + CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, NavigateError as LuaNavigateError, + Require as LuaRequire, TextRequirer as LuaTextRequirer, Vector as LuaVector, }; #[cfg(feature = "async")] From 81ae8e1393b1def3d6e9758d6131929b6638a60d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 1 Mar 2026 16:17:11 +0000 Subject: [PATCH 024/138] Make `Chunk::wrap` public --- src/chunk.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/chunk.rs b/src/chunk.rs index 5426411b..cd1b0597 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -786,7 +786,6 @@ impl Chunk<'_> { /// /// The resulted `IntoLua` implementation will convert the chunk into a Lua function without /// executing it. - #[doc(hidden)] #[track_caller] pub fn wrap(chunk: impl AsChunk) -> impl IntoLua { WrappedChunk { From a9604c4946532482920458e8026c8a586000ce38 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 1 Mar 2026 16:26:09 +0000 Subject: [PATCH 025/138] Rename Luau's TextRequirer to FsRequirer --- src/lib.rs | 2 +- src/luau/mod.rs | 4 ++-- src/luau/require.rs | 3 +-- src/luau/require/fs.rs | 12 ++++++------ src/prelude.rs | 4 ++-- tests/luau/require.rs | 8 +++----- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 97a2669b..14e247d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,7 +147,7 @@ pub use crate::debug::HookTriggers; pub use crate::{ buffer::Buffer, chunk::{CompileConstant, Compiler}, - luau::{HeapDump, NavigateError, Require, TextRequirer}, + luau::{FsRequirer, HeapDump, NavigateError, Require}, vector::Vector, }; diff --git a/src/luau/mod.rs b/src/luau/mod.rs index 4015a7c4..701d015c 100644 --- a/src/luau/mod.rs +++ b/src/luau/mod.rs @@ -10,7 +10,7 @@ use crate::traits::{FromLuaMulti, IntoLua}; use crate::types::MaybeSend; pub use heap_dump::HeapDump; -pub use require::{NavigateError, Require, TextRequirer}; +pub use require::{FsRequirer, NavigateError, Require}; // Since Luau has some missing standard functions, we re-implement them here @@ -86,7 +86,7 @@ impl Lua { } // Enable default `require` implementation - let require = self.create_require_function(require::TextRequirer::new())?; + let require = self.create_require_function(FsRequirer::new())?; self.globals().raw_set("require", require)?; Ok(()) diff --git a/src/luau/require.rs b/src/luau/require.rs index 86b23a12..3aee6f27 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -12,8 +12,7 @@ use crate::state::{Lua, callback_error_ext}; use crate::table::Table; use crate::types::MaybeSend; -// TODO: Rename to FsRequirer -pub use fs::TextRequirer; +pub use fs::FsRequirer; /// An error that can occur during navigation in the Luau `require-by-string` system. #[derive(Debug, Clone)] diff --git a/src/luau/require/fs.rs b/src/luau/require/fs.rs index 6588b02c..f6373434 100644 --- a/src/luau/require/fs.rs +++ b/src/luau/require/fs.rs @@ -12,7 +12,7 @@ use super::{NavigateError, Require}; /// The standard implementation of Luau `require-by-string` navigation. #[derive(Default, Debug)] -pub struct TextRequirer { +pub struct FsRequirer { /// An absolute path to the current Luau module (not mapped to a physical file) abs_path: PathBuf, /// A relative path to the current Luau module (not mapped to a physical file) @@ -22,7 +22,7 @@ pub struct TextRequirer { resolved_path: Option, } -impl TextRequirer { +impl FsRequirer { /// The prefix used for chunk names in the require system. /// Only chunk names starting with this prefix are allowed to be used in `require`. const CHUNK_PREFIX: &str = "@"; @@ -36,7 +36,7 @@ impl TextRequirer { /// The filename for the Luau configuration file. const LUAU_CONFIG_FILENAME: &str = ".config.luau"; - /// Creates a new `TextRequirer` instance. + /// Creates a new `FsRequirer` instance. pub fn new() -> Self { Self::default() } @@ -114,7 +114,7 @@ impl TextRequirer { } } -impl Require for TextRequirer { +impl Require for FsRequirer { fn is_require_allowed(&self, chunk_name: &str) -> bool { chunk_name.starts_with(Self::CHUNK_PREFIX) } @@ -231,7 +231,7 @@ impl Require for TextRequirer { mod tests { use std::path::Path; - use super::TextRequirer; + use super::FsRequirer; #[test] fn test_path_normalize() { @@ -267,7 +267,7 @@ mod tests { // '..' disappears if path is absolute and component is non-erasable ("/../", "/"), ] { - let path = TextRequirer::normalize_path(input.as_ref()); + let path = FsRequirer::normalize_path(input.as_ref()); assert_eq!( &path, expected.as_ref() as &Path, diff --git a/src/prelude.rs b/src/prelude.rs index a10ab987..f142ad40 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -25,8 +25,8 @@ pub use crate::HookTriggers as LuaHookTriggers; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, NavigateError as LuaNavigateError, - Require as LuaRequire, TextRequirer as LuaTextRequirer, Vector as LuaVector, + CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, FsRequirer as LuaFsRequirer, + NavigateError as LuaNavigateError, Require as LuaRequire, Vector as LuaVector, }; #[cfg(feature = "async")] diff --git a/tests/luau/require.rs b/tests/luau/require.rs index ba79fb6a..36649c9b 100644 --- a/tests/luau/require.rs +++ b/tests/luau/require.rs @@ -1,7 +1,7 @@ use std::io::Result as IoResult; use std::result::Result as StdResult; -use mlua::{Error, FromLua, IntoLua, Lua, MultiValue, NavigateError, Require, Result, TextRequirer, Value}; +use mlua::{Error, FromLua, FsRequirer, IntoLua, Lua, MultiValue, NavigateError, Require, Result, Value}; fn run_require(lua: &Lua, path: impl IntoLua) -> Result { lua.load(r#"return require(...)"#).call(path) @@ -65,7 +65,7 @@ fn test_require_errors() { assert!((res.unwrap_err().to_string()).contains("@ is not a valid alias")); // Test throwing mlua::Error - struct MyRequire(TextRequirer); + struct MyRequire(FsRequirer); impl Require for MyRequire { fn is_require_allowed(&self, chunk_name: &str) -> bool { @@ -109,9 +109,7 @@ fn test_require_errors() { } } - let require = lua - .create_require_function(MyRequire(TextRequirer::new())) - .unwrap(); + let require = lua.create_require_function(MyRequire(FsRequirer::new())).unwrap(); lua.globals().set("require", require).unwrap(); let res = lua.load(r#"return require('./a/relative/path')"#).exec(); assert!((res.unwrap_err().to_string()).contains("test error")); From a2d8b219643929d3d32271617308008767f1d2d3 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 1 Mar 2026 16:37:02 +0000 Subject: [PATCH 026/138] Make `luau` module public --- src/function.rs | 2 +- src/lib.rs | 6 +++--- src/luau/mod.rs | 16 ++++++++++++++++ src/prelude.rs | 7 +++++-- src/state.rs | 2 +- tests/luau/require.rs | 3 ++- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/function.rs b/src/function.rs index 9d1d7c9d..736b7ce4 100644 --- a/src/function.rs +++ b/src/function.rs @@ -246,7 +246,7 @@ impl Function { /// # } /// ``` /// - /// [`AsyncThread`]: crate::AsyncThread + /// [`AsyncThread`]: crate::thread::AsyncThread #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub fn call_async(&self, args: impl IntoLuaMulti) -> AsyncCallFuture diff --git a/src/lib.rs b/src/lib.rs index 14e247d3..32aede6b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,8 +76,6 @@ mod buffer; mod chunk; mod conversion; mod error; -#[cfg(any(feature = "luau", doc))] -mod luau; mod memory; mod multi; mod scope; @@ -91,6 +89,9 @@ mod vector; pub mod debug; pub mod function; +#[cfg(any(feature = "luau", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "luau")))] +pub mod luau; pub mod prelude; pub mod string; pub mod table; @@ -147,7 +148,6 @@ pub use crate::debug::HookTriggers; pub use crate::{ buffer::Buffer, chunk::{CompileConstant, Compiler}, - luau::{FsRequirer, HeapDump, NavigateError, Require}, vector::Vector, }; diff --git a/src/luau/mod.rs b/src/luau/mod.rs index 701d015c..bcda08c3 100644 --- a/src/luau/mod.rs +++ b/src/luau/mod.rs @@ -1,3 +1,19 @@ +//! Luau-specific extensions and types. +//! +//! This module provides Luau-specific functionality including custom `require` implementations, +//! heap memory analysis, and Luau VM integration utilities. +//! +//! # Overview +//! +//! - [`Require`] — trait for implementing custom module loaders used with +//! [`Lua::create_require_function`] +//! - [`FsRequirer`] — default filesystem-based [`Require`] implementation +//! - [`NavigateError`] — error type returned when navigating the module path +//! - [`HeapDump`] — snapshot of Luau heap memory usage, obtained via [`Lua::heap_dump`] +//! +//! [`Lua::create_require_function`]: crate::Lua::create_require_function +//! [`Lua::heap_dump`]: crate::Lua::heap_dump + use std::ffi::{CStr, CString}; use std::os::raw::c_int; use std::ptr; diff --git a/src/prelude.rs b/src/prelude.rs index f142ad40..1c65951a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -25,8 +25,11 @@ pub use crate::HookTriggers as LuaHookTriggers; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, FsRequirer as LuaFsRequirer, - NavigateError as LuaNavigateError, Require as LuaRequire, Vector as LuaVector, + CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, Vector as LuaVector, + luau::{ + FsRequirer as LuaFsRequirer, HeapDump as LuaHeapDump, NavigateError as LuaNavigateError, + Require as LuaRequire, + }, }; #[cfg(feature = "async")] diff --git a/src/state.rs b/src/state.rs index 6da2eefb..9566f48d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1456,7 +1456,7 @@ impl Lua { /// } /// ``` /// - /// [`AsyncThread`]: crate::AsyncThread + /// [`AsyncThread`]: crate::thread::AsyncThread #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub fn create_async_function(&self, func: F) -> Result diff --git a/tests/luau/require.rs b/tests/luau/require.rs index 36649c9b..eace354c 100644 --- a/tests/luau/require.rs +++ b/tests/luau/require.rs @@ -1,7 +1,8 @@ use std::io::Result as IoResult; use std::result::Result as StdResult; -use mlua::{Error, FromLua, FsRequirer, IntoLua, Lua, MultiValue, NavigateError, Require, Result, Value}; +use mlua::luau::{FsRequirer, NavigateError, Require}; +use mlua::{Error, FromLua, IntoLua, Lua, MultiValue, Result, Value}; fn run_require(lua: &Lua, path: impl IntoLua) -> Result { lua.load(r#"return require(...)"#).call(path) From d5d66abe424e5d27820f82115163f62133f2610a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 7 Mar 2026 23:33:52 +0000 Subject: [PATCH 027/138] Refactor GC control API - Replace `gc_inc/gc_gen` with `gc_set_mode` - Add `GcIncParams` and `GcGenParams` for GC tuning - Remove `gc_step_kbytes` (it's very rare needed and Lua 5.5 has changed the input param from kbytes to bytes) --- src/lib.rs | 5 +- src/prelude.rs | 22 +-- src/state.rs | 357 +++++++++++++++++++++++++++++------------------- tests/memory.rs | 26 +++- 4 files changed, 258 insertions(+), 152 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 32aede6b..180804b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,7 +107,10 @@ pub use crate::error::{Error, ErrorContext, ExternalError, ExternalResult, Resul pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; pub use crate::scope::Scope; -pub use crate::state::{GCMode, Lua, LuaOptions, WeakLua}; +#[cfg(any(feature = "lua54", feature = "lua55"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "lua54", feature = "lua55"))))] +pub use crate::state::GcGenParams; +pub use crate::state::{GcIncParams, GcMode, Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; #[doc(inline)] pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; diff --git a/src/prelude.rs b/src/prelude.rs index 1c65951a..ac3e6cdc 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,15 +5,15 @@ pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, - FromLua, FromLuaMulti, Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger, IntoLua, - IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, - MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, - ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, - Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, - UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, - UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, - UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, - VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, + FromLua, FromLuaMulti, Function as LuaFunction, GcIncParams as LuaGcIncParams, GcMode as LuaGcMode, + Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, + LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, + Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, + Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, + UserData as LuaUserData, UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, + UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, + UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, + Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, thread::ThreadStatus as LuaThreadStatus, }; @@ -22,6 +22,10 @@ pub use crate::{ #[doc(no_inline)] pub use crate::HookTriggers as LuaHookTriggers; +#[cfg(any(feature = "lua54", feature = "lua55"))] +#[doc(no_inline)] +pub use crate::GcGenParams as LuaGcGenParams; + #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ diff --git a/src/state.rs b/src/state.rs index 9566f48d..0bc4dc6a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -62,20 +62,126 @@ pub struct WeakLua(XWeak>); pub(crate) struct LuaGuard(ArcReentrantMutexGuard); -/// Mode of the Lua garbage collector (GC). +/// Tuning parameters for the incremental GC collector. /// -/// In Lua 5.4 GC can work in two modes: incremental and generational. -/// Previous Lua versions support only incremental GC. +/// More information can be found in the Lua [documentation]. +/// +/// [documentation]: https://www.lua.org/manual/5.5/manual.html#2.5.1 +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Default)] +pub struct GcIncParams { + /// Pause between successive GC cycles, expressed as a percentage of live memory. + #[cfg(not(feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] + pub pause: Option, + + /// Target heap size as a percentage of live data, controlling how aggressively + /// the GC reclaims memory (`LUA_GCSETGOAL`). + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub goal: Option, + + /// GC work performed per unit of memory allocated. + pub step_multiplier: Option, + + /// Granularity of each GC step in kilobytes. + #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] + pub step_size: Option, +} + +impl GcIncParams { + /// Sets the `pause` parameter. + #[cfg(not(feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] + pub fn pause(mut self, v: c_int) -> Self { + self.pause = Some(v); + self + } + + /// Sets the `goal` parameter. + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub fn goal(mut self, v: c_int) -> Self { + self.goal = Some(v); + self + } + + /// Sets the `step_multiplier` parameter. + pub fn step_multiplier(mut self, v: c_int) -> Self { + self.step_multiplier = Some(v); + self + } + + /// Sets the `step_size` parameter. + #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] + pub fn step_size(mut self, v: c_int) -> Self { + self.step_size = Some(v); + self + } +} + +/// Tuning parameters for the generational GC collector (Lua 5.4+). /// /// More information can be found in the Lua [documentation]. /// -/// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum GCMode { - Incremental, +/// [documentation]: https://www.lua.org/manual/5.5/manual.html#2.5.2 +#[cfg(any(feature = "lua55", feature = "lua54"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Default)] +pub struct GcGenParams { + /// Frequency of minor (young-generation) collection steps. + pub minor_multiplier: Option, + + /// Threshold controlling how large the young generation can grow before triggering + /// a shift from minor to major collection. + pub minor_to_major: Option, + + /// Threshold controlling how much the major collection must shrink the heap before + /// switching back to minor (young-generation) collection. + #[cfg(feature = "lua55")] + #[cfg_attr(docsrs, doc(cfg(feature = "lua55")))] + pub major_to_minor: Option, +} + +#[cfg(any(feature = "lua55", feature = "lua54"))] +impl GcGenParams { + /// Sets the `minor_multiplier` parameter. + pub fn minor_multiplier(mut self, v: c_int) -> Self { + self.minor_multiplier = Some(v); + self + } + + /// Sets the `minor_to_major` threshold. + pub fn minor_to_major(mut self, v: c_int) -> Self { + self.minor_to_major = Some(v); + self + } + + /// Sets the `major_to_minor` parameter. + #[cfg(feature = "lua55")] + #[cfg_attr(docsrs, doc(cfg(feature = "lua55")))] + pub fn major_to_minor(mut self, v: c_int) -> Self { + self.major_to_minor = Some(v); + self + } +} + +/// Lua garbage collector (GC) operating mode. +/// +/// Use [`Lua::gc_set_mode`] to switch the collector mode and/or tune its parameters. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum GcMode { + /// Incremental mark-and-sweep + Incremental(GcIncParams), + + /// Generational #[cfg(any(feature = "lua55", feature = "lua54"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] - Generational, + Generational(GcGenParams), } /// Controls Lua interpreter behavior such as Rust panics handling. @@ -998,19 +1104,19 @@ impl Lua { unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCISRUNNING, 0) != 0 } } - /// Stop the Lua GC from running + /// Stops the Lua GC from running. pub fn gc_stop(&self) { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCSTOP, 0) }; } - /// Restarts the Lua GC if it is not running + /// Restarts the Lua GC if it is not running. pub fn gc_restart(&self) { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCRESTART, 0) }; } - /// Perform a full garbage-collection cycle. + /// Performs a full garbage-collection cycle. /// /// It may be necessary to call this function twice to collect all currently unreachable /// objects. Once to finish the current gc cycle, and once to start and finish the next cycle. @@ -1023,153 +1129,128 @@ impl Lua { } } - /// Steps the garbage collector one indivisible step. + /// Performs a basic step of garbage collection. /// - /// Returns `true` if this has finished a collection cycle. - pub fn gc_step(&self) -> Result { - self.gc_step_kbytes(0) - } - - /// Steps the garbage collector as though memory had been allocated. + /// In incremental mode, a basic step corresponds to the current step size. In generational + /// mode, a basic step performs a full minor collection or an incremental step, if the collector + /// has scheduled one. /// - /// if `kbytes` is 0, then this is the same as calling `gc_step`. Returns true if this step has - /// finished a collection cycle. - pub fn gc_step_kbytes(&self, kbytes: c_int) -> Result { + /// In incremental mode, returns `true` if this step has finished a collection cycle. + /// In generational mode, returns `true` if the step finished a major collection. + pub fn gc_step(&self) -> Result { let lua = self.lock(); let state = lua.main_state(); unsafe { check_stack(state, 3)?; protect_lua!(state, 0, 0, |state| { - ffi::lua_gc(state, ffi::LUA_GCSTEP, kbytes) != 0 + ffi::lua_gc(state, ffi::LUA_GCSTEP, 0) != 0 }) } } - /// Sets the `pause` value of the collector. - /// - /// Returns the previous value of `pause`. More information can be found in the Lua - /// [documentation]. - /// - /// For Luau this parameter sets GC goal - /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 - pub fn gc_set_pause(&self, pause: c_int) -> c_int { - let lua = self.lock(); - let state = lua.main_state(); - unsafe { - #[cfg(feature = "lua55")] - return ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, pause); - - #[cfg(not(any(feature = "lua55", feature = "luau")))] - return ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, pause); - - #[cfg(feature = "luau")] - return ffi::lua_gc(state, ffi::LUA_GCSETGOAL, pause); - } - } - - /// Sets the `step multiplier` value of the collector. + /// Switches the GC to the given mode with the provided parameters. /// - /// Returns the previous value of the `step multiplier`. More information can be found in the - /// Lua [documentation]. + /// Returns the previous [`GcMode`]. The returned value's parameter fields are always + /// `None` because Lua's C API does not provide a way to read back current parameter values + /// without changing them. /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 - pub fn gc_set_step_multiplier(&self, step_multiplier: c_int) -> c_int { - let lua = self.lock(); - unsafe { - #[cfg(feature = "lua55")] - return ffi::lua_gc( - lua.main_state(), - ffi::LUA_GCPARAM, - ffi::LUA_GCPSTEPMUL, - step_multiplier, - ); - - #[cfg(not(feature = "lua55"))] - return ffi::lua_gc(lua.main_state(), ffi::LUA_GCSETSTEPMUL, step_multiplier); - } - } - - /// Changes the collector to incremental mode with the given parameters. + /// # Examples /// - /// Returns the previous mode (always `GCMode::Incremental` in Lua < 5.4). - /// More information can be found in the Lua [documentation]. + /// Switch to generational mode (Lua 5.4+): + /// ```ignore + /// let prev = lua.gc_set_mode(GcMode::Generational(GcGenParams::default())); + /// ``` /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5.1 - pub fn gc_inc(&self, pause: c_int, step_multiplier: c_int, step_size: c_int) -> GCMode { + /// Switch to incremental mode with custom parameters: + /// ```ignore + /// lua.gc_set_mode(GcMode::Incremental( + /// GcIncParams::default().pause(200).step_multiplier(100) + /// )); + /// ``` + pub fn gc_set_mode(&self, mode: GcMode) -> GcMode { let lua = self.lock(); let state = lua.main_state(); - #[cfg(any( - feature = "lua53", - feature = "lua52", - feature = "lua51", - feature = "luajit", - feature = "luau" - ))] - unsafe { - if pause > 0 { - #[cfg(not(feature = "luau"))] - ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, pause); - #[cfg(feature = "luau")] - ffi::lua_gc(state, ffi::LUA_GCSETGOAL, pause); - } - - if step_multiplier > 0 { - ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, step_multiplier); - } - + match mode { + #[cfg(feature = "lua55")] + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.pause { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPMUL, v); + } + if let Some(v) = params.step_size { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPSIZE, v); + } + match ffi::lua_gc(state, ffi::LUA_GCINC) { + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + _ => unreachable!(), + } + }, + #[cfg(feature = "lua54")] + GcMode::Incremental(params) => unsafe { + let pause = params.pause.unwrap_or(0); + let step_mul = params.step_multiplier.unwrap_or(0); + let step_size = params.step_size.unwrap_or(0); + match ffi::lua_gc(state, ffi::LUA_GCINC, pause, step_mul, step_size) { + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + _ => unreachable!(), + } + }, + #[cfg(any(feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit"))] + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.pause { + ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, v); + } + GcMode::Incremental(GcIncParams::default()) + }, #[cfg(feature = "luau")] - if step_size > 0 { - ffi::lua_gc(state, ffi::LUA_GCSETSTEPSIZE, step_size); - } - #[cfg(not(feature = "luau"))] - let _ = step_size; // Ignored - - GCMode::Incremental - } - - #[cfg(feature = "lua55")] - let prev_mode = unsafe { - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, pause); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPMUL, step_multiplier); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPSIZE, step_size); - ffi::lua_gc(state, ffi::LUA_GCINC) - }; - #[cfg(feature = "lua54")] - let prev_mode = unsafe { ffi::lua_gc(state, ffi::LUA_GCINC, pause, step_multiplier, step_size) }; - #[cfg(any(feature = "lua55", feature = "lua54"))] - match prev_mode { - ffi::LUA_GCINC => GCMode::Incremental, - ffi::LUA_GCGEN => GCMode::Generational, - _ => unreachable!(), - } - } + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.goal { + ffi::lua_gc(state, ffi::LUA_GCSETGOAL, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, v); + } + if let Some(v) = params.step_size { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPSIZE, v); + } + GcMode::Incremental(GcIncParams::default()) + }, - /// Changes the collector to generational mode with the given parameters. - /// - /// Returns the previous mode. More information about the generational GC - /// can be found in the Lua 5.4 [documentation][lua_doc]. - /// - /// [lua_doc]: https://www.lua.org/manual/5.4/manual.html#2.5.2 - #[cfg(any(feature = "lua55", feature = "lua54"))] - #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] - pub fn gc_gen(&self, minor_multiplier: c_int, major_multiplier: c_int) -> GCMode { - let lua = self.lock(); - let state = lua.main_state(); - #[cfg(feature = "lua55")] - let prev_mode = unsafe { - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMUL, minor_multiplier); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMAJOR, major_multiplier); - // TODO: LUA_GCPMAJORMINOR - ffi::lua_gc(state, ffi::LUA_GCGEN) - }; - #[cfg(not(feature = "lua55"))] - let prev_mode = unsafe { ffi::lua_gc(state, ffi::LUA_GCGEN, minor_multiplier, major_multiplier) }; - match prev_mode { - ffi::LUA_GCGEN => GCMode::Generational, - ffi::LUA_GCINC => GCMode::Incremental, - _ => unreachable!(), + #[cfg(feature = "lua55")] + GcMode::Generational(params) => unsafe { + if let Some(v) = params.minor_multiplier { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMUL, v); + } + if let Some(v) = params.minor_to_major { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMAJOR, v); + } + if let Some(v) = params.major_to_minor { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMAJORMINOR, v); + } + match ffi::lua_gc(state, ffi::LUA_GCGEN) { + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + _ => unreachable!(), + } + }, + #[cfg(feature = "lua54")] + GcMode::Generational(params) => unsafe { + let minor = params.minor_multiplier.unwrap_or(0); + let minor_to_major = params.minor_to_major.unwrap_or(0); + match ffi::lua_gc(state, ffi::LUA_GCGEN, minor, minor_to_major) { + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + _ => unreachable!(), + } + }, } } diff --git a/tests/memory.rs b/tests/memory.rs index 930aa95d..d3aac5a3 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use mlua::{Error, GCMode, Lua, Result, UserData}; +#[cfg(any(feature = "lua54", feature = "lua55"))] +use mlua::GcGenParams; +use mlua::{Error, GcIncParams, GcMode, Lua, Result, UserData}; #[test] fn test_memory_limit() -> Result<()> { @@ -74,8 +76,14 @@ fn test_gc_control() -> Result<()> { #[cfg(any(feature = "lua55", feature = "lua54"))] { - assert_eq!(lua.gc_gen(0, 0), GCMode::Incremental); - assert_eq!(lua.gc_inc(0, 0, 0), GCMode::Generational); + assert!(matches!( + lua.gc_set_mode(GcMode::Generational(GcGenParams::default())), + GcMode::Incremental(_) + )); + assert!(matches!( + lua.gc_set_mode(GcMode::Incremental(GcIncParams::default())), + GcMode::Generational(_) + )); } #[cfg(any( @@ -93,7 +101,17 @@ fn test_gc_control() -> Result<()> { assert!(lua.gc_is_running()); } - assert_eq!(lua.gc_inc(200, 100, 13), GCMode::Incremental); + assert!(matches!( + lua.gc_set_mode(GcMode::Incremental({ + let p = GcIncParams::default().step_multiplier(100); + #[cfg(not(feature = "luau"))] + let p = p.pause(200); + #[cfg(feature = "luau")] + let p = p.goal(200); + p + })), + GcMode::Incremental(_) + )); struct MyUserdata(#[allow(unused)] Arc<()>); impl UserData for MyUserdata {} From 56c227fd7e099335284fe0ec9d5038e5c485fa0a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 8 Mar 2026 00:06:11 +0000 Subject: [PATCH 028/138] Make `state` module public --- src/lib.rs | 8 +++----- src/prelude.rs | 24 ++++++++++++------------ src/state.rs | 6 ++++++ tests/memory.rs | 6 ++++-- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 180804b4..78d4227b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,6 @@ mod error; mod memory; mod multi; mod scope; -mod state; mod stdlib; mod traits; mod types; @@ -93,6 +92,7 @@ pub mod function; #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub mod luau; pub mod prelude; +pub mod state; pub mod string; pub mod table; pub mod thread; @@ -107,10 +107,8 @@ pub use crate::error::{Error, ErrorContext, ExternalError, ExternalResult, Resul pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; pub use crate::scope::Scope; -#[cfg(any(feature = "lua54", feature = "lua55"))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "lua54", feature = "lua55"))))] -pub use crate::state::GcGenParams; -pub use crate::state::{GcIncParams, GcMode, Lua, LuaOptions, WeakLua}; +#[doc(inline)] +pub use crate::state::{Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; #[doc(inline)] pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; diff --git a/src/prelude.rs b/src/prelude.rs index ac3e6cdc..4eae1763 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,17 +5,17 @@ pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, - FromLua, FromLuaMulti, Function as LuaFunction, GcIncParams as LuaGcIncParams, GcMode as LuaGcMode, - Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, - LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, - Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, - Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, - UserData as LuaUserData, UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, - UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, - UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, - Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, - table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, - thread::ThreadStatus as LuaThreadStatus, + FromLua, FromLuaMulti, Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, + LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, + MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, + ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, + Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, + UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, + UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, + UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, + VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, + state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, + table::TableSequence as LuaTableSequence, thread::ThreadStatus as LuaThreadStatus, }; #[cfg(not(feature = "luau"))] @@ -24,7 +24,7 @@ pub use crate::HookTriggers as LuaHookTriggers; #[cfg(any(feature = "lua54", feature = "lua55"))] #[doc(no_inline)] -pub use crate::GcGenParams as LuaGcGenParams; +pub use crate::state::GcGenParams as LuaGcGenParams; #[cfg(feature = "luau")] #[doc(no_inline)] diff --git a/src/state.rs b/src/state.rs index 0bc4dc6a..7513d7b2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,8 @@ +//! Lua state management. +//! +//! This module provides the main [`Lua`] state handle together with state-specific +//! configuration and garbage collector controls. + use std::any::TypeId; use std::cell::{BorrowError, BorrowMutError, RefCell}; use std::marker::PhantomData; @@ -44,6 +49,7 @@ use { use serde::Serialize; pub(crate) use extra::ExtraData; +#[doc(hidden)] pub use raw::RawLua; pub(crate) use util::callback_error_ext; diff --git a/tests/memory.rs b/tests/memory.rs index d3aac5a3..4f91221e 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -1,8 +1,10 @@ use std::sync::Arc; +use mlua::state::{GcIncParams, GcMode}; +use mlua::{Error, Lua, Result, UserData}; + #[cfg(any(feature = "lua54", feature = "lua55"))] -use mlua::GcGenParams; -use mlua::{Error, GcIncParams, GcMode, Lua, Result, UserData}; +use mlua::state::GcGenParams; #[test] fn test_memory_limit() -> Result<()> { From a24d2151af1421ea1a2687424138148318319d75 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 27 Mar 2026 22:54:56 +0000 Subject: [PATCH 029/138] Minor fixes in docs --- README.md | 2 +- docs/release_notes/v0.10.md | 6 +++--- docs/release_notes/v0.9.md | 6 +++--- src/buffer.rs | 2 +- src/lib.rs | 8 ++++---- src/serde/mod.rs | 4 ++-- src/thread.rs | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fa462e10..b253b571 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ my_project $ LUA_LIB=$HOME/tmp/lua-5.2.4/src LUA_LIB_NAME=lua LUA_LINK=static ca Just enable the `vendored` feature and cargo will automatically build and link the specified Lua/LuaJIT version. This is the easiest way to get started with `mlua`. ### Standalone mode -In standalone mode, `mlua` allows adding scripting support to your application with a gently configured Lua runtime to ensure safety and soundness. +In standalone mode, `mlua` allows adding scripting support to your application with a properly configured Lua runtime to ensure safety and soundness. Add to `Cargo.toml`: diff --git a/docs/release_notes/v0.10.md b/docs/release_notes/v0.10.md index db01e8b2..2684a4f3 100644 --- a/docs/release_notes/v0.10.md +++ b/docs/release_notes/v0.10.md @@ -1,6 +1,6 @@ ## mlua v0.10 release notes -The v0.10 version of mlua has goal to improve the user experience while keeping the same performance and safety guarantees. +The v0.10 version of mlua has a goal to improve the user experience while keeping the same performance and safety guarantees. This document highlights the most notable features. For a full list of changes, see the [CHANGELOG]. [CHANGELOG]: https://github.com/mlua-rs/mlua/blob/main/CHANGELOG.md @@ -40,7 +40,7 @@ assert_eq!(lua.globals().get::("i")?, 20); Under the hood, to synchronize access to the Lua state, mlua uses [`ReentrantMutex`] which can be recursively locked by a single thread. Only one thread can execute Lua code at a time, but it's possible to share Lua values between threads. -This has some performance penalties (about 10-20%) compared to the lock free mode. This flag is disabled by default and does not supported in module mode. +This has some performance penalties (about 10-20%) compared to the lock free mode. This flag is disabled by default and is not supported in module mode. [`ReentrantMutex`]: https://docs.rs/parking_lot/latest/parking_lot/type.ReentrantMutex.html @@ -144,7 +144,7 @@ The following `Scope` methods were changed: Instead, scope has comprehensive support for borrowed userdata: `create_any_userdata_ref`, `create_any_userdata_ref_mut`, `create_userdata_ref`, `create_userdata_ref_mut`. `UserDataRef` and `UserDataRefMut` are no longer acceptable for scoped userdata access as they require owned underlying data. -In mlua v0.9 this can cause read-after-free bug in some edge cases. +In mlua v0.9 this could cause a read-after-free bug in some edge cases. To temporarily borrow underlying data, the `AnyUserData::borrow_scoped` and `AnyUserData::borrow_mut_scoped` methods were introduced: diff --git a/docs/release_notes/v0.9.md b/docs/release_notes/v0.9.md index 7f3a0327..9de0ce2d 100644 --- a/docs/release_notes/v0.9.md +++ b/docs/release_notes/v0.9.md @@ -152,9 +152,9 @@ It will automatically trigger JIT compilation for new Lua chunks. To disable it, #### 1. Better error reporting -When calling a Rust function from Lua and passing wrong arguments, previous mlua versions reported a error message without any context or reference to the particular argument. +When calling a Rust function from Lua and passing wrong arguments, previous mlua versions reported an error message without any context or reference to the particular argument. -In v0.9 it reports a error message with the argument index and expected type: +In v0.9 it reports an error message with the argument index and expected type: ```rust let func = lua.create_function(|_, _a: i32| Ok(()))?; @@ -327,7 +327,7 @@ Under the hood a new function `luaopen_alt_module` will be created for the Lua m - `skip_memory_check` - skip memory allocation checks for some operations. -In module mode, mlua runs in unknown environment and cannot say are there any memory limits or not. As result, some operations that require memory allocation runs in +In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory limits or not. As a result, some operations that require memory allocation run in protected mode. Setting this attribute will improve performance of such operations with risk of having uncaught exceptions and memory leaks. #### Improved Windows target diff --git a/src/buffer.rs b/src/buffer.rs index 070391fe..d27f8b3d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -59,7 +59,7 @@ impl Buffer { /// Returns an adaptor implementing [`io::Read`], [`io::Write`] and [`io::Seek`] over the /// buffer. /// - /// Buffer operations are infallible, none of the read/write functions will return a Err. + /// Buffer operations are infallible, none of the read/write functions will return an Err. pub fn cursor(self) -> impl io::Read + io::Write + io::Seek { BufferCursor(self, 0) } diff --git a/src/lib.rs b/src/lib.rs index 78d4227b..ea2d709f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,10 +262,10 @@ pub use mlua_derive::FromLua; /// /// * skip_memory_check - skip memory allocation checks for some operations. /// -/// In module mode, mlua runs in unknown environment and cannot say are there any memory -/// limits or not. As result, some operations that require memory allocation runs in -/// protected mode. Setting this attribute will improve performance of such operations -/// with risk of having uncaught exceptions and memory leaks. +/// In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory +/// limits or not. As a result, some operations that require memory allocation run in protected +/// mode. Setting this attribute will improve performance of such operations with risk of having +/// uncaught exceptions and memory leaks. /// /// ```ignore /// #[mlua::lua_module(skip_memory_check)] diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 1b85a763..52b2757f 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -37,8 +37,8 @@ pub trait LuaSerdeExt: Sealed { fn null(&self) -> Value; /// A metatable attachable to a Lua table to systematically encode it as Array (instead of Map). - /// As result, encoded Array will contain only sequence part of the table, with the same length - /// as the `#` operator on that table. + /// As a result, encoded Array will contain only sequence part of the table, with the same + /// length as the `#` operator on that table. /// /// # Example /// diff --git a/src/thread.rs b/src/thread.rs index 935f4546..65d209ed 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -356,7 +356,7 @@ impl Thread { /// Resets a thread /// /// In [Lua 5.4]: cleans its call stack and closes all pending to-be-closed variables. - /// Returns a error in case of either the original error that stopped the thread or errors + /// Returns an error in case of either the original error that stopped the thread or errors /// in closing methods. /// /// In Luau: resets to the initial state of a newly created Lua thread. From 59872da63d27aea52ccc89b8cebd2dd4ff34c5a9 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 16:31:05 +0000 Subject: [PATCH 030/138] mlua-sys: Bump lua-src and luajit-src dependencies --- mlua-sys/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index c6804892..bd86453a 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -41,8 +41,8 @@ libc = "0.2" cc = "1.0" cfg-if = "1.0" pkg-config = "0.3.17" -lua-src = { version = ">= 550.0.0, < 550.1.0", optional = true } -luajit-src = { version = ">= 210.6.0, < 210.7.0", optional = true } +lua-src = { version = ">= 550.1.0, < 550.2.0", optional = true } +luajit-src = { version = ">= 210.7.0, < 210.8.0", optional = true } luau0-src = { version = "0.19.0", optional = true } [lints.rust] From a5ae2a1fc355ecc7c9d96b40d6458660ebd0a93a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 17:07:03 +0000 Subject: [PATCH 031/138] Update useratom doc --- mlua-sys/src/luau/lua.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlua-sys/src/luau/lua.rs b/mlua-sys/src/luau/lua.rs index 15862331..d6d545e3 100644 --- a/mlua-sys/src/luau/lua.rs +++ b/mlua-sys/src/luau/lua.rs @@ -565,7 +565,7 @@ pub struct lua_Callbacks { /// gets called when L is created (LP == parent) or destroyed (LP == NULL) pub userthread: Option, - /// gets called when a string is created; returned atom can be retrieved via tostringatom + /// gets called when a string is created to assign an atom id pub useratom: Option i16>, /// gets called when BREAK instruction is encountered From c5aadc68cdc475871d30cc53411b059f02daceb8 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 17:17:57 +0000 Subject: [PATCH 032/138] Open `UserDataMethods::add_method_once` and `UserDataMethods::add_async_method_once` --- src/userdata.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/userdata.rs b/src/userdata.rs index 6e5211eb..1fbcbb5d 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -340,7 +340,6 @@ pub trait UserDataMethods { /// /// The method can be called only once per userdata instance, subsequent calls will result in a /// [`Error::UserDataDestructed`] error. - #[doc(hidden)] fn add_method_once(&mut self, name: impl Into, method: M) where T: 'static, @@ -395,7 +394,6 @@ pub trait UserDataMethods { /// [`Error::UserDataDestructed`] error. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] - #[doc(hidden)] fn add_async_method_once(&mut self, name: impl Into, method: M) where T: 'static, From be56e2205c6c1f5c1cecdec4157243a8877d0d6b Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 17:38:49 +0000 Subject: [PATCH 033/138] Update GC `step_size` doc --- src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state.rs b/src/state.rs index 7513d7b2..f7a68c75 100644 --- a/src/state.rs +++ b/src/state.rs @@ -90,7 +90,7 @@ pub struct GcIncParams { /// GC work performed per unit of memory allocated. pub step_multiplier: Option, - /// Granularity of each GC step in kilobytes. + /// Granularity of each GC step (see Lua reference for details). #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] pub step_size: Option, From 7f1d716a44cb63a83ff954a92ee269f3915ca727 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 17:40:22 +0000 Subject: [PATCH 034/138] Remove deprecated `Lua::load_from_function` --- src/state.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/state.rs b/src/state.rs index f7a68c75..6f2b6498 100644 --- a/src/state.rs +++ b/src/state.rs @@ -549,31 +549,6 @@ impl Lua { Ok(()) } - #[doc(hidden)] - #[deprecated(since = "0.11.0", note = "Use `register_module` instead")] - #[cfg(not(feature = "luau"))] - #[cfg(not(tarpaulin_include))] - pub fn load_from_function(&self, modname: &str, func: Function) -> Result { - let loaded = unsafe { - self.exec_raw::((), |state| { - ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_LOADED_TABLE); - })? - }; - - let value = match loaded.raw_get(modname)? { - Value::Nil => { - let result = match func.call(modname)? { - Value::Nil => Value::Boolean(true), - res => res, - }; - loaded.raw_set(modname, &result)?; - result - } - res => res, - }; - T::from_lua(value, self) - } - /// Unloads module `modname`. /// /// This method does not support unloading binary Lua modules since they are internally cached From 9126bb8ce03f2d4bd2287dfc0e443b2205992037 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 22:37:25 +0000 Subject: [PATCH 035/138] Add `RawLua::pop` method --- src/state/raw.rs | 16 +++++++++++++--- src/table.rs | 7 ++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/state/raw.rs b/src/state/raw.rs index 4b2fa2da..1458870d 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -16,7 +16,7 @@ use crate::stdlib::StdLib; use crate::string::LuaString; use crate::table::Table; use crate::thread::Thread; -use crate::traits::IntoLua; +use crate::traits::{FromLua, IntoLua}; use crate::types::{ AppDataRef, AppDataRefMut, Callback, CallbackUpvalue, DestructedUserdata, Integer, LightUserData, LuaType, MaybeSend, ReentrantMutex, RegistryKey, ValueRef, XRc, @@ -50,7 +50,7 @@ use { std::task::{Context, Poll, Waker}, }; -/// An inner Lua struct which holds a raw Lua state. +/// An internal Lua struct which holds a raw Lua state. #[doc(hidden)] pub struct RawLua { // The state is dynamic and depends on context @@ -736,9 +736,19 @@ impl RawLua { value.push_into_stack(self) } + /// Pops a value that implements [`FromLua`] from the top of the Lua stack. + /// + /// Uses up to 1 stack space, does not call `checkstack`. + #[inline(always)] + pub unsafe fn pop(&self) -> Result { + let v = R::from_stack(-1, self)?; + ffi::lua_pop(self.state(), 1); + Ok(v) + } + /// Pushes a `Value` (by reference) onto the Lua stack. /// - /// Uses 2 stack spaces, does not call `checkstack`. + /// Uses up to 2 stack spaces, does not call `checkstack`. pub unsafe fn push_value(&self, value: &Value) -> Result<()> { let state = self.state(); match value { diff --git a/src/table.rs b/src/table.rs index 694e9927..2d767fb8 100644 --- a/src/table.rs +++ b/src/table.rs @@ -784,10 +784,8 @@ impl Table { ffi::lua_pushnil(state); while ffi::lua_next(state, -2) != 0 { let k = K::from_stack(-2, &lua)?; - let v = V::from_stack(-1, &lua)?; + let v = lua.pop::()?; f(k, v)?; - // Keep key for next iteration - ffi::lua_pop(state, 1); } } Ok(()) @@ -860,8 +858,7 @@ impl Table { if len.is_none() && t == ffi::LUA_TNIL { break; } - f(V::from_stack(-1, &lua)?)?; - ffi::lua_pop(state, 1); + f(lua.pop::()?)?; } } Ok(()) From c9848d6fafb9151834ff85046396144002015e44 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 28 Mar 2026 23:57:35 +0000 Subject: [PATCH 036/138] Update doc/example for (hidden) `Lua::exec_raw_lua` --- src/state.rs | 25 +++++++++++-------------- src/state/raw.rs | 4 ++++ src/util/error.rs | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/state.rs b/src/state.rs index 6f2b6498..7e624ec2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -449,30 +449,27 @@ impl Lua { R::from_stack_multi(nresults, &lua) } - /// Runs callback with the inner RawLua value. It can be used to manually push and get values on - /// the stack. + /// Calls provided function passing a reference to the [`RawLua`] handle. /// - /// This function is safe because all unsafe actions with RawLua can only be done with unsafe + /// Provided [`RawLua`] handle can be used to manually pushing/popping values to/from the stack. /// /// # Example /// ``` - /// # use mlua::{Lua, Result, FromLua, IntoLua}; + /// # use mlua::{Lua, Result, FromLua, IntoLua, IntoLuaMulti}; /// # fn main() -> Result<()> { /// let lua = Lua::new(); /// let n: i32 = { - /// let num = 11i32; - /// lua.exec_raw_lua(|lua| { - /// unsafe { - /// ::push_into_stack(num, lua)?; + /// let nums = (3, 4, 5); + /// lua.exec_raw_lua(|rawlua| unsafe { + /// nums.push_into_stack_multi(rawlua)?; + /// let mut sum = 0; + /// for _ in 0..3 { + /// sum += rawlua.pop::()?; /// } - /// - /// let n = unsafe { - /// ::from_stack(-1, lua)? - /// }; - /// Result::Ok(n) + /// Result::Ok(sum) /// }) /// }?; - /// assert_eq!(n, 11); + /// assert_eq!(n, 12); /// # Ok(()) /// # } /// ``` diff --git a/src/state/raw.rs b/src/state/raw.rs index 1458870d..1a6edcb5 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -731,6 +731,7 @@ impl RawLua { /// Pushes a value that implements `IntoLua` onto the Lua stack. /// /// Uses up to 2 stack spaces to push a single value, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] #[inline(always)] pub unsafe fn push(&self, value: impl IntoLua) -> Result<()> { value.push_into_stack(self) @@ -739,6 +740,7 @@ impl RawLua { /// Pops a value that implements [`FromLua`] from the top of the Lua stack. /// /// Uses up to 1 stack space, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] #[inline(always)] pub unsafe fn pop(&self) -> Result { let v = R::from_stack(-1, self)?; @@ -749,6 +751,7 @@ impl RawLua { /// Pushes a `Value` (by reference) onto the Lua stack. /// /// Uses up to 2 stack spaces, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] pub unsafe fn push_value(&self, value: &Value) -> Result<()> { let state = self.state(); match value { @@ -783,6 +786,7 @@ impl RawLua { /// Pops a value from the Lua stack. /// /// Uses up to 1 stack spaces, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] #[inline] pub unsafe fn pop_value(&self) -> Value { let value = self.stack_value(-1, None); diff --git a/src/util/error.rs b/src/util/error.rs index 297754d0..c84902de 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -197,7 +197,7 @@ where F: FnOnce(*mut ffi::lua_State) -> R, R: Copy, { - struct Params { + struct Params { function: Option, result: MaybeUninit, nresults: c_int, From d27693b61aee9e2a6282f288a6be8b3d9d8a7fcc Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 29 Mar 2026 09:55:38 +0100 Subject: [PATCH 037/138] Make `error` module public --- src/error.rs | 5 +++++ src/lib.rs | 7 +++++-- src/prelude.rs | 13 +++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/error.rs b/src/error.rs index c5b5e150..42aba318 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,8 @@ +//! Lua error handling. +//! +//! This module provides the [`Error`] type returned by all fallible `mlua` operations, together +//! with extension traits for adapting Rust errors for use within Lua. + use std::error::Error as StdError; use std::fmt; use std::io::Error as IoError; diff --git a/src/lib.rs b/src/lib.rs index ea2d709f..7ccda161 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,6 @@ mod macros; mod buffer; mod chunk; mod conversion; -mod error; mod memory; mod multi; mod scope; @@ -87,6 +86,7 @@ mod value; mod vector; pub mod debug; +pub mod error; pub mod function; #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] @@ -102,7 +102,8 @@ pub use bstr::BString; pub use ffi::{self, lua_CFunction, lua_State}; pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; -pub use crate::error::{Error, ErrorContext, ExternalError, ExternalResult, Result}; +#[doc(inline)] +pub use crate::error::{Error, Result}; #[doc(inline)] pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; @@ -129,6 +130,8 @@ pub use crate::value::{Nil, Value}; // Re-export some types to keep backward compatibility and avoid breaking changes in the public API. #[doc(hidden)] +pub use crate::error::{ErrorContext, ExternalError, ExternalResult}; +#[doc(hidden)] pub use crate::string::LuaString as String; #[doc(hidden)] pub use crate::table::{TablePairs, TableSequence}; diff --git a/src/prelude.rs b/src/prelude.rs index 4eae1763..f40ecec0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,9 +3,8 @@ #[doc(no_inline)] pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, - Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, - ErrorContext as LuaErrorContext, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, - FromLua, FromLuaMulti, Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, + Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, FromLua, + FromLuaMulti, Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, @@ -13,9 +12,11 @@ pub use crate::{ UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, - VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, - state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, - table::TableSequence as LuaTableSequence, thread::ThreadStatus as LuaThreadStatus, + VmState as LuaVmState, WeakLua, error::ErrorContext as LuaErrorContext, + error::ExternalError as LuaExternalError, error::ExternalResult as LuaExternalResult, + function::FunctionInfo as LuaFunctionInfo, state::GcIncParams as LuaGcIncParams, + state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, + thread::ThreadStatus as LuaThreadStatus, }; #[cfg(not(feature = "luau"))] From e7e92b4f6f7211f4e61c4cf7b4b9774f44d329ac Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 3 Apr 2026 13:44:49 +0100 Subject: [PATCH 038/138] Make `chunk` module public --- src/lib.rs | 14 +++++++------- src/prelude.rs | 23 ++++++++++++----------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7ccda161..e3b9be60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,6 @@ mod macros; mod buffer; -mod chunk; mod conversion; mod memory; mod multi; @@ -85,6 +84,7 @@ mod util; mod value; mod vector; +pub mod chunk; pub mod debug; pub mod error; pub mod function; @@ -101,7 +101,6 @@ pub mod userdata; pub use bstr::BString; pub use ffi::{self, lua_CFunction, lua_State}; -pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; #[doc(inline)] pub use crate::error::{Error, Result}; #[doc(inline)] @@ -130,6 +129,11 @@ pub use crate::value::{Nil, Value}; // Re-export some types to keep backward compatibility and avoid breaking changes in the public API. #[doc(hidden)] +pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; +#[cfg(feature = "luau")] +#[doc(hidden)] +pub use crate::chunk::{CompileConstant, Compiler}; +#[doc(hidden)] pub use crate::error::{ErrorContext, ExternalError, ExternalResult}; #[doc(hidden)] pub use crate::string::LuaString as String; @@ -149,11 +153,7 @@ pub use crate::debug::HookTriggers; #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] -pub use crate::{ - buffer::Buffer, - chunk::{CompileConstant, Compiler}, - vector::Vector, -}; +pub use crate::{buffer::Buffer, vector::Vector}; #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] diff --git a/src/prelude.rs b/src/prelude.rs index f40ecec0..790b71ce 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,16 +3,16 @@ #[doc(no_inline)] pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, - Chunk as LuaChunk, ChunkMode as LuaChunkMode, Either as LuaEither, Error as LuaError, FromLua, - FromLuaMulti, Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, - LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, - MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, - ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, - Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, - UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, - UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, - UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, - VmState as LuaVmState, WeakLua, error::ErrorContext as LuaErrorContext, + Either as LuaEither, Error as LuaError, FromLua, FromLuaMulti, Function as LuaFunction, + Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, + LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, + Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, + Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, + UserData as LuaUserData, UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, + UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, + UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, + Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, + chunk::Chunk as LuaChunk, chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, error::ExternalError as LuaExternalError, error::ExternalResult as LuaExternalResult, function::FunctionInfo as LuaFunctionInfo, state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, @@ -30,7 +30,8 @@ pub use crate::state::GcGenParams as LuaGcGenParams; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, Compiler as LuaCompiler, Vector as LuaVector, + Vector as LuaVector, + chunk::{CompileConstant as LuaCompileConstant, Compiler as LuaCompiler}, luau::{ FsRequirer as LuaFsRequirer, HeapDump as LuaHeapDump, NavigateError as LuaNavigateError, Require as LuaRequire, From 5872ed70f5066194bf002f61b6a265fdb2bf7162 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 4 Apr 2026 15:04:00 +0100 Subject: [PATCH 039/138] Make `traits` module public --- src/lib.rs | 11 +++-------- src/prelude.rs | 22 +++++++++++----------- src/traits.rs | 5 +++++ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e3b9be60..126df8f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,7 +78,6 @@ mod memory; mod multi; mod scope; mod stdlib; -mod traits; mod types; mod util; mod value; @@ -96,6 +95,7 @@ pub mod state; pub mod string; pub mod table; pub mod thread; +pub mod traits; pub mod userdata; pub use bstr::BString; @@ -116,9 +116,8 @@ pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; pub use crate::table::Table; #[doc(inline)] pub use crate::thread::Thread; -pub use crate::traits::{ - FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut, ObjectLike, -}; +#[doc(inline)] +pub use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, ObjectLike}; pub use crate::types::{ AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, MaybeSync, Number, RegistryKey, VmState, @@ -155,10 +154,6 @@ pub use crate::debug::HookTriggers; #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub use crate::{buffer::Buffer, vector::Vector}; -#[cfg(feature = "async")] -#[cfg_attr(docsrs, doc(cfg(feature = "async")))] -pub use crate::traits::LuaNativeAsyncFn; - #[cfg(feature = "serde")] #[doc(inline)] pub use crate::{ diff --git a/src/prelude.rs b/src/prelude.rs index 790b71ce..cfdb7268 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,19 +4,19 @@ pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, Either as LuaEither, Error as LuaError, FromLua, FromLuaMulti, Function as LuaFunction, - Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaNativeFn, - LuaNativeFnMut, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, - Nil as LuaNil, Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, - Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, - UserData as LuaUserData, UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, - UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, - UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, - Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, - chunk::Chunk as LuaChunk, chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, + Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaOptions, + LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, + ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, + Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, + UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, + UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, + UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, + VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, chunk::Chunk as LuaChunk, + chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, error::ExternalError as LuaExternalError, error::ExternalResult as LuaExternalResult, function::FunctionInfo as LuaFunctionInfo, state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, - thread::ThreadStatus as LuaThreadStatus, + thread::ThreadStatus as LuaThreadStatus, traits::LuaNativeFn, traits::LuaNativeFnMut, }; #[cfg(not(feature = "luau"))] @@ -40,7 +40,7 @@ pub use crate::{ #[cfg(feature = "async")] #[doc(no_inline)] -pub use crate::{LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThread}; +pub use crate::{thread::AsyncThread as LuaAsyncThread, traits::LuaNativeAsyncFn}; #[cfg(feature = "serde")] #[doc(no_inline)] diff --git a/src/traits.rs b/src/traits.rs index 93429108..54db0044 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,3 +1,8 @@ +//! Core conversion and extension traits. +//! +//! This module provides the fundamental traits for converting values between Rust and Lua, +//! and for defining native Lua callable functions. + use std::os::raw::c_int; use std::sync::Arc; From 3ab3c997b3a9cf3fc0eab0cc9c5228e7b3796f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 9 Apr 2026 17:45:15 +0800 Subject: [PATCH 040/138] feat: support external strings for `Cow` and `Cow` (#692) --- src/conversion.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index f2f7ca5a..301d37ef 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -523,7 +523,10 @@ impl IntoLua for &str { impl IntoLua for Cow<'_, str> { #[inline] fn into_lua(self, lua: &Lua) -> Result { - Ok(Value::String(lua.create_string(self.as_bytes())?)) + match self { + Cow::Borrowed(s) => s.into_lua(lua), + Cow::Owned(s) => s.into_lua(lua), + } } } @@ -585,7 +588,10 @@ impl IntoLua for &CStr { impl IntoLua for Cow<'_, CStr> { #[inline] fn into_lua(self, lua: &Lua) -> Result { - Ok(Value::String(lua.create_string(self.to_bytes())?)) + match self { + Cow::Borrowed(s) => s.into_lua(lua), + Cow::Owned(s) => s.into_lua(lua), + } } } From c52deec988324428e8272f5ccec79a1439262140 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Tue, 14 Apr 2026 23:45:42 +0100 Subject: [PATCH 041/138] Use c_int for userdata metatable id --- src/state/raw.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/state/raw.rs b/src/state/raw.rs index 1a6edcb5..f969b4b0 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -963,7 +963,7 @@ impl RawLua { // Check if userdata/metatable is already registered let type_id = TypeId::of::(); if let Some(&table_id) = (*self.extra.get()).registered_userdata_t.get(&type_id) { - return Ok(table_id as Integer); + return Ok(table_id); } // Create a new metatable from `UserData` definition @@ -982,7 +982,7 @@ impl RawLua { // Check if userdata/metatable is already registered let type_id = TypeId::of::(); if let Some(&table_id) = (*self.extra.get()).registered_userdata_t.get(&type_id) { - return Ok(table_id as Integer); + return Ok(table_id); } // Check if metatable creation is pending or create an empty metatable otherwise @@ -997,7 +997,7 @@ impl RawLua { unsafe fn make_userdata_with_metatable( &self, data: UserDataStorage, - get_metatable_id: impl FnOnce() -> Result, + get_metatable_id: impl FnOnce() -> Result, ) -> Result { let state = self.state(); let _sg = StackGuard::new(state); @@ -1007,7 +1007,7 @@ impl RawLua { let mt_id = get_metatable_id()?; let protect = !self.unlikely_memory_error(); push_userdata(state, data, protect)?; - ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id); + ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id as _); ffi::lua_setmetatable(state, -2); // Set empty environment for Lua 5.1 @@ -1025,7 +1025,7 @@ impl RawLua { Ok(AnyUserData(self.pop_ref())) } - pub(crate) unsafe fn create_userdata_metatable(&self, registry: RawUserDataRegistry) -> Result { + pub(crate) unsafe fn create_userdata_metatable(&self, registry: RawUserDataRegistry) -> Result { let state = self.state(); let type_id = registry.type_id; @@ -1041,7 +1041,7 @@ impl RawLua { } self.register_userdata_metatable(mt_ptr, type_id); - Ok(id as Integer) + Ok(id) } pub(crate) unsafe fn push_userdata_metatable(&self, mut registry: RawUserDataRegistry) -> Result<()> { From 3be47451906a3faab9db48ebb8c39a59646949db Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 13:12:31 +0100 Subject: [PATCH 042/138] Add initial Luau integer64 type support RFC: https://rfcs.luau.org/type-long-integer.html Unfortunately this type is not backward compatible with regular numbers and require a special "integer" library. It's not integrated with `Value` enum to keep it simple. --- mlua-sys/Cargo.toml | 2 +- mlua-sys/src/luau/lauxlib.rs | 2 ++ mlua-sys/src/luau/lua.rs | 22 +++++++++++++++------- mlua-sys/src/luau/luacode.rs | 1 + mlua-sys/src/luau/lualib.rs | 2 ++ src/conversion.rs | 7 +++++++ src/state/raw.rs | 18 +++++++++++++++--- src/stdlib.rs | 7 ++++++- tests/luau.rs | 22 +++++++++++++++++++++- 9 files changed, 70 insertions(+), 13 deletions(-) diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index bd86453a..09ddca3b 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -43,7 +43,7 @@ cfg-if = "1.0" pkg-config = "0.3.17" lua-src = { version = ">= 550.1.0, < 550.2.0", optional = true } luajit-src = { version = ">= 210.7.0, < 210.8.0", optional = true } -luau0-src = { version = "0.19.0", optional = true } +luau0-src = { version = "0.20.0", optional = true } [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/src/luau/lauxlib.rs b/mlua-sys/src/luau/lauxlib.rs index f57c3cdc..0328aaf0 100644 --- a/mlua-sys/src/luau/lauxlib.rs +++ b/mlua-sys/src/luau/lauxlib.rs @@ -38,8 +38,10 @@ unsafe extern "C-unwind" { #[link_name = "luaL_checkinteger"] pub fn luaL_checkinteger_(L: *mut lua_State, narg: c_int) -> c_int; + pub fn luaL_checkinteger64(L: *mut lua_State, narg: c_int) -> i64; #[link_name = "luaL_optinteger"] pub fn luaL_optinteger_(L: *mut lua_State, narg: c_int, def: c_int) -> c_int; + pub fn luaL_optinteger64(L: *mut lua_State, narg: c_int, def: i64) -> i64; pub fn luaL_checkunsigned(L: *mut lua_State, narg: c_int) -> lua_Unsigned; pub fn luaL_optunsigned(L: *mut lua_State, narg: c_int, def: lua_Unsigned) -> lua_Unsigned; diff --git a/mlua-sys/src/luau/lua.rs b/mlua-sys/src/luau/lua.rs index d6d545e3..7fc6b19b 100644 --- a/mlua-sys/src/luau/lua.rs +++ b/mlua-sys/src/luau/lua.rs @@ -65,14 +65,15 @@ pub const LUA_TBOOLEAN: c_int = 1; pub const LUA_TLIGHTUSERDATA: c_int = 2; pub const LUA_TNUMBER: c_int = 3; -pub const LUA_TVECTOR: c_int = 4; +pub const LUA_TINTEGER: c_int = 4; +pub const LUA_TVECTOR: c_int = 5; -pub const LUA_TSTRING: c_int = 5; -pub const LUA_TTABLE: c_int = 6; -pub const LUA_TFUNCTION: c_int = 7; -pub const LUA_TUSERDATA: c_int = 8; -pub const LUA_TTHREAD: c_int = 9; -pub const LUA_TBUFFER: c_int = 10; +pub const LUA_TSTRING: c_int = 6; +pub const LUA_TTABLE: c_int = 7; +pub const LUA_TFUNCTION: c_int = 8; +pub const LUA_TUSERDATA: c_int = 9; +pub const LUA_TTHREAD: c_int = 10; +pub const LUA_TBUFFER: c_int = 11; /// Guaranteed number of Lua stack slots available to a C function. pub const LUA_MINSTACK: c_int = 20; @@ -153,6 +154,7 @@ unsafe extern "C-unwind" { pub fn lua_tounsignedx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Unsigned; pub fn lua_tovector(L: *mut lua_State, idx: c_int) -> *const c_float; pub fn lua_toboolean(L: *mut lua_State, idx: c_int) -> c_int; + pub fn lua_tointeger64(L: *mut lua_State, idx: c_int, isinteger: *mut c_int) -> i64; pub fn lua_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; pub fn lua_tostringatom(L: *mut lua_State, idx: c_int, atom: *mut c_int) -> *const c_char; pub fn lua_tolstringatom( @@ -182,6 +184,7 @@ unsafe extern "C-unwind" { pub fn lua_pushnumber(L: *mut lua_State, n: lua_Number); #[link_name = "lua_pushinteger"] pub fn lua_pushinteger_(L: *mut lua_State, n: c_int); + pub fn lua_pushinteger64(L: *mut lua_State, n: i64); pub fn lua_pushunsigned(L: *mut lua_State, n: lua_Unsigned); #[cfg(not(feature = "luau-vector4"))] pub fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float); @@ -412,6 +415,11 @@ pub unsafe fn lua_isboolean(L: *mut lua_State, n: c_int) -> c_int { (lua_type(L, n) == LUA_TBOOLEAN) as c_int } +#[inline(always)] +pub unsafe fn lua_isinteger64(L: *mut lua_State, n: c_int) -> c_int { + (lua_type(L, n) == LUA_TINTEGER) as c_int +} + #[inline(always)] pub unsafe fn lua_isvector(L: *mut lua_State, n: c_int) -> c_int { (lua_type(L, n) == LUA_TVECTOR) as c_int diff --git a/mlua-sys/src/luau/luacode.rs b/mlua-sys/src/luau/luacode.rs index cd7ec376..1d74d453 100644 --- a/mlua-sys/src/luau/luacode.rs +++ b/mlua-sys/src/luau/luacode.rs @@ -80,6 +80,7 @@ unsafe extern "C" { pub fn luau_set_compile_constant_nil(cons: *mut lua_CompileConstant); pub fn luau_set_compile_constant_boolean(cons: *mut lua_CompileConstant, b: c_int); pub fn luau_set_compile_constant_number(cons: *mut lua_CompileConstant, n: f64); + pub fn luau_set_compile_constant_integer64(cons: *mut lua_CompileConstant, l: i64); pub fn luau_set_compile_constant_vector(cons: *mut lua_CompileConstant, x: f32, y: f32, z: f32, w: f32); pub fn luau_set_compile_constant_string(cons: *mut lua_CompileConstant, s: *const c_char, l: usize); } diff --git a/mlua-sys/src/luau/lualib.rs b/mlua-sys/src/luau/lualib.rs index a28a2a61..dcef58c6 100644 --- a/mlua-sys/src/luau/lualib.rs +++ b/mlua-sys/src/luau/lualib.rs @@ -14,6 +14,7 @@ pub const LUA_UTF8LIBNAME: *const c_char = cstr!("utf8"); pub const LUA_MATHLIBNAME: *const c_char = cstr!("math"); pub const LUA_DBLIBNAME: *const c_char = cstr!("debug"); pub const LUA_VECLIBNAME: *const c_char = cstr!("vector"); +pub const LUA_INTLIBNAME : *const c_char = cstr!("integer"); unsafe extern "C-unwind" { pub fn luaopen_base(L: *mut lua_State) -> c_int; @@ -27,6 +28,7 @@ unsafe extern "C-unwind" { pub fn luaopen_math(L: *mut lua_State) -> c_int; pub fn luaopen_debug(L: *mut lua_State) -> c_int; pub fn luaopen_vector(L: *mut lua_State) -> c_int; + pub fn luaopen_integer(L: *mut lua_State) -> c_int; // open all builtin libraries pub fn luaL_openlibs(L: *mut lua_State); diff --git a/src/conversion.rs b/src/conversion.rs index 301d37ef..1fdacb03 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -812,6 +812,13 @@ macro_rules! lua_convert_int { }); } } + #[cfg(feature = "luau")] + if type_id == ffi::LUA_TINTEGER { + let i = ffi::lua_tointeger64(state, idx, std::ptr::null_mut()); + return cast(i).ok_or_else(|| { + Error::from_lua_conversion("integer", stringify!($x), "out of range".to_string()) + }); + } // Fallback to default Self::from_lua(lua.stack_value(idx, Some(type_id)), lua.lua()) } diff --git a/src/state/raw.rs b/src/state/raw.rs index f969b4b0..dcc860a3 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -817,15 +817,22 @@ impl RawLua { #[cfg(any(feature = "lua52", feature = "lua51", feature = "luajit", feature = "luau"))] ffi::LUA_TNUMBER => { - use crate::types::Number; - let n = ffi::lua_tonumber(state, idx); match num_traits::cast(n) { - Some(i) if n.to_bits() == (i as Number).to_bits() => Value::Integer(i), + Some(i) if n.to_bits() == (i as crate::types::Number).to_bits() => Value::Integer(i), _ => Value::Number(n), } } + #[cfg(feature = "luau")] + ffi::LUA_TINTEGER => { + let i = ffi::lua_tointeger64(state, idx, ptr::null_mut()); + match num_traits::cast(i) { + Some(i) => Value::Integer(i), + _ => Value::Number(i as crate::types::Number), + } + } + #[cfg(feature = "luau")] ffi::LUA_TVECTOR => { let v = ffi::lua_tovector(state, idx); @@ -1598,6 +1605,11 @@ unsafe fn load_std_libs(state: *mut ffi::lua_State, libs: StdLib) -> Result<()> requiref(state, ffi::LUA_VECLIBNAME, ffi::luaopen_vector, 1)?; } + #[cfg(feature = "luau")] + if libs.contains(StdLib::INTEGER) { + requiref(state, ffi::LUA_INTLIBNAME, ffi::luaopen_integer, 1)?; + } + if libs.contains(StdLib::MATH) { requiref(state, ffi::LUA_MATHLIBNAME, ffi::luaopen_math, 1)?; } diff --git a/src/stdlib.rs b/src/stdlib.rs index 46eb33bc..48111fed 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -73,10 +73,15 @@ impl StdLib { #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub const VECTOR: StdLib = StdLib(1 << 10); + /// [`integer`](https://luau.org/library#integer-library) library + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub const INTEGER: StdLib = StdLib(1 << 11); + /// [`jit`](http://luajit.org/ext_jit.html) library #[cfg(any(feature = "luajit", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luajit")))] - pub const JIT: StdLib = StdLib(1 << 11); + pub const JIT: StdLib = StdLib(1 << 12); /// (**unsafe**) [`ffi`](http://luajit.org/ext_ffi.html) library #[cfg(any(feature = "luajit", doc))] diff --git a/tests/luau.rs b/tests/luau.rs index 5143e7ac..6b5c9296 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -6,7 +6,9 @@ use std::os::raw::c_void; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; -use mlua::{Compiler, Error, Function, Lua, LuaOptions, Result, StdLib, Table, Value, Vector, VmState}; +use mlua::{ + Compiler, Error, Function, Lua, LuaOptions, ObjectLike, Result, StdLib, Table, Value, Vector, VmState, +}; #[test] fn test_version() -> Result<()> { @@ -533,5 +535,23 @@ fn test_heap_dump() -> Result<()> { Ok(()) } +#[test] +fn test_integer64_type() -> Result<()> { + let lua = Lua::new(); + + _ = Lua::set_fflag("LuauIntegerType", true); + + let integer_lib = lua.globals().get::
("integer")?; + let n = integer_lib.call_function::("create", 42)?; + assert_eq!(n, 42); + + let n: i64 = lua.load("return 42i").eval()?; + assert_eq!(n, 42); + let n: i64 = lua.load("return -42i").eval()?; + assert_eq!(n, -42); + + Ok(()) +} + #[path = "luau/require.rs"] mod require; From 31b88e85bb632dabf97eeaf65f2c352a37c75777 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 13:26:42 +0100 Subject: [PATCH 043/138] cargo fmt --- mlua-sys/src/luau/lualib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlua-sys/src/luau/lualib.rs b/mlua-sys/src/luau/lualib.rs index dcef58c6..02ccf561 100644 --- a/mlua-sys/src/luau/lualib.rs +++ b/mlua-sys/src/luau/lualib.rs @@ -14,7 +14,7 @@ pub const LUA_UTF8LIBNAME: *const c_char = cstr!("utf8"); pub const LUA_MATHLIBNAME: *const c_char = cstr!("math"); pub const LUA_DBLIBNAME: *const c_char = cstr!("debug"); pub const LUA_VECLIBNAME: *const c_char = cstr!("vector"); -pub const LUA_INTLIBNAME : *const c_char = cstr!("integer"); +pub const LUA_INTLIBNAME: *const c_char = cstr!("integer"); unsafe extern "C-unwind" { pub fn luaopen_base(L: *mut lua_State) -> c_int; From 65bb6279eed77de37bacf747e1c3ba55e4182be6 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 14:47:26 +0100 Subject: [PATCH 044/138] Update serde types visibility --- src/lib.rs | 5 ++++- src/serde/mod.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 126df8f5..600bd1fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,8 +156,11 @@ pub use crate::{buffer::Buffer, vector::Vector}; #[cfg(feature = "serde")] #[doc(inline)] +pub use crate::serde::LuaSerdeExt; +#[cfg(feature = "serde")] +#[doc(hidden)] pub use crate::{ - serde::{LuaSerdeExt, de::Options as DeserializeOptions, ser::Options as SerializeOptions}, + serde::{DeserializeOptions, SerializeOptions}, value::SerializableValue, }; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 52b2757f..52540a25 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -243,6 +243,6 @@ pub mod de; pub mod ser; #[doc(inline)] -pub use de::Deserializer; +pub use de::{Deserializer, Options as DeserializeOptions}; #[doc(inline)] -pub use ser::Serializer; +pub use ser::{Options as SerializeOptions, Serializer}; From df6097ab3820b3ee90af782f7f0d1e1cfb90ac2c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 14:51:54 +0100 Subject: [PATCH 045/138] Mark Luau `CompileConstant` as non_exhaustive --- src/chunk.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chunk.rs b/src/chunk.rs index cd1b0597..5c25f773 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -160,6 +160,7 @@ pub enum ChunkMode { /// Represents a constant value that can be used by Luau compiler. #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] +#[non_exhaustive] #[derive(Clone, Debug)] pub enum CompileConstant { Nil, From 75ff11f7951051d4279efd946db68557aa6e6842 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 14:56:24 +0100 Subject: [PATCH 046/138] Move LuaNativeFn/Mut into `function` module --- src/function.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++-- src/prelude.rs | 8 ++--- src/traits.rs | 94 +------------------------------------------------ 3 files changed, 97 insertions(+), 99 deletions(-) diff --git a/src/function.rs b/src/function.rs index 736b7ce4..88be94f1 100644 --- a/src/function.rs +++ b/src/function.rs @@ -86,7 +86,7 @@ use std::{mem, ptr, slice}; use crate::error::{Error, Result}; use crate::state::Lua; use crate::table::Table; -use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut}; +use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti}; use crate::types::{Callback, LuaType, MaybeSend, ValueRef}; use crate::util::{ StackGuard, assert_stack, check_stack, linenumber_to_usize, pop_error, ptr_to_lossy_str, ptr_to_str, @@ -96,7 +96,6 @@ use crate::value::Value; #[cfg(feature = "async")] use { crate::thread::AsyncThread, - crate::traits::LuaNativeAsyncFn, crate::types::AsyncCallback, std::future::{self, Future}, std::pin::{Pin, pin}, @@ -788,6 +787,97 @@ impl Future for AsyncCallFuture { } } +/// A trait for types that can be used as Lua functions. +pub trait LuaNativeFn { + type Output: IntoLuaMulti; + + fn call(&self, args: A) -> Self::Output; +} + +/// A trait for types with mutable state that can be used as Lua functions. +pub trait LuaNativeFnMut { + type Output: IntoLuaMulti; + + fn call(&mut self, args: A) -> Self::Output; +} + +/// A trait for types that returns a future and can be used as Lua functions. +#[cfg(feature = "async")] +pub trait LuaNativeAsyncFn { + type Output: IntoLuaMulti; + + fn call(&self, args: A) -> impl Future + MaybeSend + 'static; +} + +macro_rules! impl_lua_native_fn { + ($($A:ident),*) => { + impl LuaNativeFn<($($A,)*)> for FN + where + FN: Fn($($A,)*) -> R + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + R: IntoLuaMulti, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&self, args: ($($A,)*)) -> Self::Output { + let ($($A,)*) = args; + self($($A,)*) + } + } + + impl LuaNativeFnMut<($($A,)*)> for FN + where + FN: FnMut($($A,)*) -> R + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + R: IntoLuaMulti, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&mut self, args: ($($A,)*)) -> Self::Output { + let ($($A,)*) = args; + self($($A,)*) + } + } + + #[cfg(feature = "async")] + impl LuaNativeAsyncFn<($($A,)*)> for FN + where + FN: Fn($($A,)*) -> Fut + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + Fut: Future + MaybeSend + 'static, + R: IntoLuaMulti, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&self, args: ($($A,)*)) -> impl Future + MaybeSend + 'static { + let ($($A,)*) = args; + self($($A,)*) + } + } + }; +} + +impl_lua_native_fn!(); +impl_lua_native_fn!(A); +impl_lua_native_fn!(A, B); +impl_lua_native_fn!(A, B, C); +impl_lua_native_fn!(A, B, C, D); +impl_lua_native_fn!(A, B, C, D, E); +impl_lua_native_fn!(A, B, C, D, E, F); +impl_lua_native_fn!(A, B, C, D, E, F, G); +impl_lua_native_fn!(A, B, C, D, E, F, G, H); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); + #[cfg(test)] mod assertions { use super::*; diff --git a/src/prelude.rs b/src/prelude.rs index cfdb7268..d109d2b3 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,9 +14,9 @@ pub use crate::{ VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, chunk::Chunk as LuaChunk, chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, error::ExternalError as LuaExternalError, error::ExternalResult as LuaExternalResult, - function::FunctionInfo as LuaFunctionInfo, state::GcIncParams as LuaGcIncParams, - state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, - thread::ThreadStatus as LuaThreadStatus, traits::LuaNativeFn, traits::LuaNativeFnMut, + function::FunctionInfo as LuaFunctionInfo, function::LuaNativeFn, function::LuaNativeFnMut, + state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, + table::TableSequence as LuaTableSequence, thread::ThreadStatus as LuaThreadStatus, }; #[cfg(not(feature = "luau"))] @@ -40,7 +40,7 @@ pub use crate::{ #[cfg(feature = "async")] #[doc(no_inline)] -pub use crate::{thread::AsyncThread as LuaAsyncThread, traits::LuaNativeAsyncFn}; +pub use crate::{function::LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThread}; #[cfg(feature = "serde")] #[doc(no_inline)] diff --git a/src/traits.rs b/src/traits.rs index 54db0044..405a95f7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,12 +10,11 @@ use crate::error::{Error, Result}; use crate::multi::MultiValue; use crate::private::Sealed; use crate::state::{Lua, RawLua, WeakLua}; -use crate::types::MaybeSend; use crate::util::{check_stack, parse_lookup_path, short_type_name}; use crate::value::Value; #[cfg(feature = "async")] -use {crate::function::AsyncCallFuture, std::future::Future}; +use crate::function::AsyncCallFuture; /// Trait for types convertible to [`Value`]. pub trait IntoLua: Sized { @@ -250,97 +249,6 @@ pub trait ObjectLike: Sealed { fn weak_lua(&self) -> &WeakLua; } -/// A trait for types that can be used as Lua functions. -pub trait LuaNativeFn { - type Output: IntoLuaMulti; - - fn call(&self, args: A) -> Self::Output; -} - -/// A trait for types with mutable state that can be used as Lua functions. -pub trait LuaNativeFnMut { - type Output: IntoLuaMulti; - - fn call(&mut self, args: A) -> Self::Output; -} - -/// A trait for types that returns a future and can be used as Lua functions. -#[cfg(feature = "async")] -pub trait LuaNativeAsyncFn { - type Output: IntoLuaMulti; - - fn call(&self, args: A) -> impl Future + MaybeSend + 'static; -} - -macro_rules! impl_lua_native_fn { - ($($A:ident),*) => { - impl LuaNativeFn<($($A,)*)> for FN - where - FN: Fn($($A,)*) -> R + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&self, args: ($($A,)*)) -> Self::Output { - let ($($A,)*) = args; - self($($A,)*) - } - } - - impl LuaNativeFnMut<($($A,)*)> for FN - where - FN: FnMut($($A,)*) -> R + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&mut self, args: ($($A,)*)) -> Self::Output { - let ($($A,)*) = args; - self($($A,)*) - } - } - - #[cfg(feature = "async")] - impl LuaNativeAsyncFn<($($A,)*)> for FN - where - FN: Fn($($A,)*) -> Fut + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - Fut: Future + MaybeSend + 'static, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&self, args: ($($A,)*)) -> impl Future + MaybeSend + 'static { - let ($($A,)*) = args; - self($($A,)*) - } - } - }; -} - -impl_lua_native_fn!(); -impl_lua_native_fn!(A); -impl_lua_native_fn!(A, B); -impl_lua_native_fn!(A, B, C); -impl_lua_native_fn!(A, B, C, D); -impl_lua_native_fn!(A, B, C, D, E); -impl_lua_native_fn!(A, B, C, D, E, F); -impl_lua_native_fn!(A, B, C, D, E, F, G); -impl_lua_native_fn!(A, B, C, D, E, F, G, H); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); - pub(crate) trait ShortTypeName { #[inline(always)] fn type_name() -> String { From 27f91dfd1b8867867b1dea20e15dbbd38275c487 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 16:01:35 +0100 Subject: [PATCH 047/138] Accept any error in `Function::wrap/wrap_mut/wrap_async` Previously wrapped functions were required to return `mlua::Result`. Now it's possible to wrap functions returning any errors as long as they implement `std::error::Error`. Existing code remains compatible with `mlua::Result` as this type is not converted to an external error. --- src/error.rs | 6 +++++- src/function.rs | 36 ++++++++++++++++++++---------------- tests/async.rs | 2 +- tests/error.rs | 13 +++++++++++++ tests/function.rs | 38 +++++++++++++++++++++++++++++++++++--- tests/types.rs | 17 ++++++++++------- 6 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/error.rs b/src/error.rs index 42aba318..a7ef2a15 100644 --- a/src/error.rs +++ b/src/error.rs @@ -345,7 +345,11 @@ impl Error { /// Wraps an external error object. #[inline] pub fn external>>(err: T) -> Self { - Error::ExternalError(err.into().into()) + let boxed = err.into(); + match boxed.downcast::() { + Ok(err) => *err, + Err(boxed) => Error::ExternalError(boxed.into()), + } } /// Attempts to downcast the external error object to a concrete type by reference. diff --git a/src/function.rs b/src/function.rs index 88be94f1..894e8e87 100644 --- a/src/function.rs +++ b/src/function.rs @@ -81,9 +81,10 @@ use std::cell::RefCell; use std::os::raw::{c_int, c_void}; +use std::result::Result as StdResult; use std::{mem, ptr, slice}; -use crate::error::{Error, Result}; +use crate::error::{Error, ExternalError, ExternalResult, Result}; use crate::state::Lua; use crate::table::Table; use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti}; @@ -635,30 +636,32 @@ impl Function { /// Wraps a Rust function or closure, returning an opaque type that implements [`IntoLua`] /// trait. #[inline] - pub fn wrap(func: F) -> impl IntoLua + pub fn wrap(func: F) -> impl IntoLua where - F: LuaNativeFn> + MaybeSend + 'static, + F: LuaNativeFn> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { WrappedFunction(Box::new(move |lua, nargs| unsafe { let args = A::from_stack_args(nargs, 1, None, lua)?; - func.call(args)?.push_into_stack_multi(lua) + func.call(args).into_lua_err()?.push_into_stack_multi(lua) })) } /// Wraps a Rust mutable closure, returning an opaque type that implements [`IntoLua`] trait. - pub fn wrap_mut(func: F) -> impl IntoLua + pub fn wrap_mut(func: F) -> impl IntoLua where - F: LuaNativeFnMut> + MaybeSend + 'static, + F: LuaNativeFnMut> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { let func = RefCell::new(func); WrappedFunction(Box::new(move |lua, nargs| unsafe { let mut func = func.try_borrow_mut().map_err(|_| Error::RecursiveMutCallback)?; let args = A::from_stack_args(nargs, 1, None, lua)?; - func.call(args)?.push_into_stack_multi(lua) + func.call(args).into_lua_err()?.push_into_stack_multi(lua) })) } @@ -671,6 +674,7 @@ impl Function { pub fn wrap_raw(func: F) -> impl IntoLua where F: LuaNativeFn + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { WrappedFunction(Box::new(move |lua, nargs| unsafe { @@ -687,6 +691,7 @@ impl Function { pub fn wrap_raw_mut(func: F) -> impl IntoLua where F: LuaNativeFnMut + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { let func = RefCell::new(func); @@ -701,11 +706,12 @@ impl Function { /// trait. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] - pub fn wrap_async(func: F) -> impl IntoLua + pub fn wrap_async(func: F) -> impl IntoLua where - F: LuaNativeAsyncFn> + MaybeSend + 'static, + F: LuaNativeAsyncFn> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { WrappedAsyncFunction(Box::new(move |rawlua, nargs| unsafe { let args = match A::from_stack_args(nargs, 1, None, rawlua) { @@ -714,7 +720,7 @@ impl Function { }; let lua = rawlua.lua(); let fut = func.call(args); - Box::pin(async move { fut.await?.push_into_stack_multi(lua.raw_lua()) }) + Box::pin(async move { fut.await.into_lua_err()?.push_into_stack_multi(lua.raw_lua()) }) })) } @@ -728,6 +734,7 @@ impl Function { pub fn wrap_raw_async(func: F) -> impl IntoLua where F: LuaNativeAsyncFn + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { WrappedAsyncFunction(Box::new(move |rawlua, nargs| unsafe { @@ -789,14 +796,14 @@ impl Future for AsyncCallFuture { /// A trait for types that can be used as Lua functions. pub trait LuaNativeFn { - type Output: IntoLuaMulti; + type Output; fn call(&self, args: A) -> Self::Output; } /// A trait for types with mutable state that can be used as Lua functions. pub trait LuaNativeFnMut { - type Output: IntoLuaMulti; + type Output; fn call(&mut self, args: A) -> Self::Output; } @@ -804,7 +811,7 @@ pub trait LuaNativeFnMut { /// A trait for types that returns a future and can be used as Lua functions. #[cfg(feature = "async")] pub trait LuaNativeAsyncFn { - type Output: IntoLuaMulti; + type Output; fn call(&self, args: A) -> impl Future + MaybeSend + 'static; } @@ -815,7 +822,6 @@ macro_rules! impl_lua_native_fn { where FN: Fn($($A,)*) -> R + MaybeSend + 'static, ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, { type Output = R; @@ -830,7 +836,6 @@ macro_rules! impl_lua_native_fn { where FN: FnMut($($A,)*) -> R + MaybeSend + 'static, ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, { type Output = R; @@ -847,7 +852,6 @@ macro_rules! impl_lua_native_fn { FN: Fn($($A,)*) -> Fut + MaybeSend + 'static, ($($A,)*): FromLuaMulti, Fut: Future + MaybeSend + 'static, - R: IntoLuaMulti, { type Output = R; diff --git a/tests/async.rs b/tests/async.rs index 16ddd9ee..55e41593 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -41,7 +41,7 @@ async fn test_async_function_wrap() -> Result<()> { let f = Function::wrap_async(|s: String| async move { tokio::task::yield_now().await; - Ok(s) + Ok::<_, Error>(s) }); lua.globals().set("f", f)?; let res: String = lua.load(r#"f("hello")"#).eval_async().await?; diff --git a/tests/error.rs b/tests/error.rs index 09bdd5b1..6f70f770 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -77,6 +77,19 @@ fn test_error_chain() -> Result<()> { Ok(()) } +#[test] +fn test_external_error() { + // `Error::external` should preserve `mlua::Error` + let runtime_err = Error::runtime("test error"); + let converted = Error::external(runtime_err); + assert!(matches!(converted, Error::RuntimeError(ref msg) if msg == "test error")); + + // Other errors should become `ExternalError` + let converted = Error::external(io::Error::other("other error")); + assert!(matches!(converted, Error::ExternalError(_))); + assert!(converted.downcast_ref::().is_some()); +} + #[cfg(feature = "anyhow")] #[test] fn test_error_anyhow() -> Result<()> { diff --git a/tests/function.rs b/tests/function.rs index d01e4fa9..8cb75d4c 100644 --- a/tests/function.rs +++ b/tests/function.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::result::Result as StdResult; + use mlua::{Error, Function, Lua, LuaString, Result, Table, Variadic}; #[test] @@ -343,7 +346,7 @@ fn test_function_deep_clone() -> Result<()> { fn test_function_wrap() -> Result<()> { let lua = Lua::new(); - let f = Function::wrap(|s: LuaString, n| Ok(s.to_str().unwrap().repeat(n))); + let f = Function::wrap(|s: LuaString, n| Ok::<_, Error>(s.to_str().unwrap().repeat(n))); lua.globals().set("f", f)?; lua.load(r#"assert(f("hello", 2) == "hellohello")"#) .exec() @@ -361,11 +364,40 @@ fn test_function_wrap() -> Result<()> { .exec() .unwrap(); + // Return external error + #[derive(Debug)] + struct MyError(String); + impl fmt::Display for MyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MyError: {}", self.0) + } + } + impl std::error::Error for MyError {} + + let fext = Function::wrap(|s: String| -> StdResult { + if s == "bad" { + return Err(MyError("bad input".into())); + } + Ok(format!("ok: {s}")) + }); + lua.globals().set("fext", fext)?; + lua.load(r#"assert(fext("hello") == "ok: hello")"#) + .exec() + .unwrap(); + lua.load( + r#" + local ok, err = pcall(fext, "bad") + assert(not ok and tostring(err):find("MyError: bad input")) + "#, + ) + .exec() + .unwrap(); + // Mutable callback let mut i = 0; let fmut = Function::wrap_mut(move || { i += 1; - Ok(i) + Ok::<_, Error>(i) }); lua.globals().set("fmut", fmut)?; lua.load(r#"fmut(); fmut(); assert(fmut() == 3)"#).exec().unwrap(); @@ -385,7 +417,7 @@ fn test_function_wrap() -> Result<()> { // Check recursive mut callback error let fmut = Function::wrap_mut(|f: Function| match f.call::<()>(&f) { Err(Error::CallbackError { cause, .. }) => match cause.as_ref() { - Error::RecursiveMutCallback { .. } => Ok(()), + Error::RecursiveMutCallback { .. } => Ok::<_, Error>(()), other => panic!("incorrect result: {other:?}"), }, other => panic!("incorrect result: {other:?}"), diff --git a/tests/types.rs b/tests/types.rs index 0cd775bc..6475acd6 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -1,6 +1,6 @@ use std::os::raw::c_void; -use mlua::{Function, LightUserData, Lua, LuaString, Number, Result, Thread}; +use mlua::{Error, Function, LightUserData, Lua, LuaString, Number, Result, Thread}; #[test] fn test_lightuserdata() -> Result<()> { @@ -30,7 +30,7 @@ fn test_boolean_type_metatable() -> Result<()> { let lua = Lua::new(); let mt = lua.create_table()?; - mt.set("__add", Function::wrap(|a, b| Ok(a || b)))?; + mt.set("__add", Function::wrap(|a, b| Ok::<_, mlua::Error>(a || b)))?; assert_eq!(lua.type_metatable::(), None); lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -51,7 +51,7 @@ fn test_lightuserdata_type_metatable() -> Result<()> { mt.set( "__add", Function::wrap(|a: LightUserData, b: LightUserData| { - Ok(LightUserData((a.0 as usize + b.0 as usize) as *mut c_void)) + Ok::<_, Error>(LightUserData((a.0 as usize + b.0 as usize) as *mut c_void)) }), )?; lua.set_type_metatable::(Some(mt.clone())); @@ -79,7 +79,10 @@ fn test_number_type_metatable() -> Result<()> { let lua = Lua::new(); let mt = lua.create_table()?; - mt.set("__call", Function::wrap(|n1: f64, n2: f64| Ok(n1 * n2)))?; + mt.set( + "__call", + Function::wrap(|n1: f64, n2: f64| Ok::<_, Error>(n1 * n2)), + )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -96,7 +99,7 @@ fn test_string_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__add", - Function::wrap(|a: String, b: String| Ok(format!("{a}{b}"))), + Function::wrap(|a: String, b: String| Ok::<_, Error>(format!("{a}{b}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -113,7 +116,7 @@ fn test_function_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__index", - Function::wrap(|_: Function, key: String| Ok(format!("function.{key}"))), + Function::wrap(|_: Function, key: String| Ok::<_, Error>(format!("function.{key}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::(), Some(mt)); @@ -132,7 +135,7 @@ fn test_thread_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__index", - Function::wrap(|_: Thread, key: String| Ok(format!("thread.{key}"))), + Function::wrap(|_: Thread, key: String| Ok::<_, Error>(format!("thread.{key}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::(), Some(mt)); From f2b5cc44de44074e4a84b5ecd28bc8af2d5fbffb Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 16:09:17 +0100 Subject: [PATCH 048/138] `traits` module no longer need to be public The LuaNativeFn traits were moved to the `function` module and all other traits as re-exported. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 600bd1fd..21f346d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ mod memory; mod multi; mod scope; mod stdlib; +mod traits; mod types; mod util; mod value; @@ -95,7 +96,6 @@ pub mod state; pub mod string; pub mod table; pub mod thread; -pub mod traits; pub mod userdata; pub use bstr::BString; From 8c93948f2fc4d8f11f0acd8e30d0e7e1397c5f76 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 18 Apr 2026 16:13:23 +0100 Subject: [PATCH 049/138] Fix tests --- src/error.rs | 6 ++---- tests/chunk.rs | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/error.rs b/src/error.rs index a7ef2a15..0664618a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -559,10 +559,8 @@ impl<'a> Iterator for Chain<'a> { #[cfg(test)] mod assertions { - use super::*; - #[cfg(not(feature = "error-send"))] - static_assertions::assert_not_impl_any!(Error: Send, Sync); + static_assertions::assert_not_impl_any!(super::Error: Send, Sync); #[cfg(feature = "send")] - static_assertions::assert_impl_all!(Error: Send, Sync); + static_assertions::assert_impl_all!(super::Error: Send, Sync); } diff --git a/tests/chunk.rs b/tests/chunk.rs index 3f7b4849..95c54274 100644 --- a/tests/chunk.rs +++ b/tests/chunk.rs @@ -1,3 +1,4 @@ +#[cfg(not(target_os = "wasi"))] use std::{fs, io}; use mlua::{Chunk, ChunkMode, Lua, Result}; @@ -85,7 +86,7 @@ fn test_chunk_macro() -> Result<()> { data.raw_set("num", 1)?; let ud = mlua::AnyUserData::wrap("hello"); - let f = mlua::Function::wrap(|| Ok(())); + let f = mlua::Function::wrap(|| Ok::<_, mlua::Error>(())); lua.globals().set("g", 123)?; From 3d1ae981d31ddb1b745b3269720783e7d1d432a2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 19 Apr 2026 22:12:17 +0100 Subject: [PATCH 050/138] Update docs --- src/function.rs | 6 ------ src/lib.rs | 11 ++++------- src/luau/mod.rs | 13 ++----------- src/serde/mod.rs | 2 -- src/string.rs | 8 -------- src/table.rs | 6 ------ src/userdata.rs | 12 ------------ 7 files changed, 6 insertions(+), 52 deletions(-) diff --git a/src/function.rs b/src/function.rs index 894e8e87..234bbe23 100644 --- a/src/function.rs +++ b/src/function.rs @@ -3,12 +3,6 @@ //! This module provides types for working with Lua functions from Rust, including //! both Lua-defined functions and native Rust callbacks. //! -//! # Main Types -//! -//! - [`Function`] - A handle to a Lua function that can be called from Rust. -//! - [`FunctionInfo`] - Debug information about a function (name, source, line numbers, etc.). -//! - [`CoverageInfo`] - Code coverage data for Luau functions (requires `luau` feature). -//! //! # Calling Functions //! //! Use [`Function::call`] to invoke a Lua function synchronously: diff --git a/src/lib.rs b/src/lib.rs index 21f346d5..7937831a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,15 +154,12 @@ pub use crate::debug::HookTriggers; #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub use crate::{buffer::Buffer, vector::Vector}; -#[cfg(feature = "serde")] -#[doc(inline)] -pub use crate::serde::LuaSerdeExt; #[cfg(feature = "serde")] #[doc(hidden)] -pub use crate::{ - serde::{DeserializeOptions, SerializeOptions}, - value::SerializableValue, -}; +pub use crate::serde::{DeserializeOptions, SerializeOptions}; +#[cfg(feature = "serde")] +#[doc(inline)] +pub use crate::{serde::LuaSerdeExt, value::SerializableValue}; #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] diff --git a/src/luau/mod.rs b/src/luau/mod.rs index bcda08c3..5bb54069 100644 --- a/src/luau/mod.rs +++ b/src/luau/mod.rs @@ -1,18 +1,9 @@ //! Luau-specific extensions and types. //! -//! This module provides Luau-specific functionality including custom `require` implementations, +//! This module provides Luau-specific functionality including custom [`require`] implementations, //! heap memory analysis, and Luau VM integration utilities. //! -//! # Overview -//! -//! - [`Require`] — trait for implementing custom module loaders used with -//! [`Lua::create_require_function`] -//! - [`FsRequirer`] — default filesystem-based [`Require`] implementation -//! - [`NavigateError`] — error type returned when navigating the module path -//! - [`HeapDump`] — snapshot of Luau heap memory usage, obtained via [`Lua::heap_dump`] -//! -//! [`Lua::create_require_function`]: crate::Lua::create_require_function -//! [`Lua::heap_dump`]: crate::Lua::heap_dump +//! [`require`]: crate::Lua::create_require_function use std::ffi::{CStr, CString}; use std::os::raw::c_int; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 52540a25..2d39f59e 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -242,7 +242,5 @@ static ARRAY_METATABLE_REGISTRY_KEY: u8 = 0; pub mod de; pub mod ser; -#[doc(inline)] pub use de::{Deserializer, Options as DeserializeOptions}; -#[doc(inline)] pub use ser::{Options as SerializeOptions, Serializer}; diff --git a/src/string.rs b/src/string.rs index 3093c99c..c7961a2e 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,14 +1,6 @@ //! Lua string handling. //! //! This module provides types for working with Lua strings from Rust. -//! -//! # Main Types -//! -//! - [`LuaString`] - A handle to an internal Lua string (may not be valid UTF-8). -//! - [`BorrowedStr`] - A borrowed `&str` view of a Lua string that holds a strong reference to the -//! Lua state. -//! - [`BorrowedBytes`] - A borrowed `&[u8]` view of a Lua string that holds a strong reference to -//! the Lua state. use std::borrow::Borrow; use std::hash::{Hash, Hasher}; diff --git a/src/table.rs b/src/table.rs index 2d767fb8..e938000e 100644 --- a/src/table.rs +++ b/src/table.rs @@ -3,12 +3,6 @@ //! Tables are Lua's primary data structure, used for arrays, dictionaries, objects, modules, //! and more. This module provides types for creating and manipulating Lua tables from Rust. //! -//! # Main Types -//! -//! - [`Table`] - A handle to a Lua table. -//! - [`TablePairs`] - An iterator over key-value pairs in a table. -//! - [`TableSequence`] - An iterator over the array (sequence) portion of a table. -//! //! # Basic Operations //! //! Tables support key-value access similar to Rust's `HashMap`: diff --git a/src/userdata.rs b/src/userdata.rs index 1fbcbb5d..7ba3ef74 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -1,18 +1,6 @@ //! Lua userdata handling. //! //! This module provides types for creating and working with Lua userdata from Rust. -//! -//! # Main Types -//! -//! - [`AnyUserData`] - A handle to a Lua userdata value of any Rust type. -//! - [`UserData`] - Trait to implement for types that should be exposed to Lua as userdata. -//! - [`UserDataFields`] - Trait for registering fields on userdata types. -//! - [`UserDataMethods`] - Trait for registering methods on userdata types. -//! - [`UserDataRegistry`] - Registry for userdata methods and fields. -//! - [`UserDataMetatable`] - A handle to the metatable of a userdata type. -//! - [`UserDataRef`] - A borrowed reference to a userdata value. -//! - [`UserDataRefMut`] - A mutably borrowed reference to a userdata value. -//! - [`MetaMethod`] - Metamethod names for customizing Lua operators. use std::any::TypeId; use std::ffi::CStr; From 4e028d8409ba3719c86431202a6dd8b3a0606a93 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 19 Apr 2026 23:26:24 +0100 Subject: [PATCH 051/138] Change `AnyUserData::type_name` to return `LuaString` instead. This avoids unnecessary allocation and returns type name as it stored in metatable. --- src/userdata.rs | 15 +++++++++------ tests/userdata.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/userdata.rs b/src/userdata.rs index 7ba3ef74..e174945d 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -8,6 +8,7 @@ use std::fmt; use std::hash::Hash; use std::os::raw::{c_char, c_void}; +use crate::Either; use crate::error::{Error, Result}; use crate::function::Function; use crate::state::Lua; @@ -1028,8 +1029,8 @@ impl AnyUserData { /// Returns a type name of this userdata (from a metatable field). /// - /// If no type name is set, returns `None`. - pub fn type_name(&self) -> Result> { + /// If no type name is set, returns `userdata`. + pub fn type_name(&self) -> Result { let lua = self.0.lua.lock(); let state = lua.state(); unsafe { @@ -1046,8 +1047,8 @@ impl AnyUserData { ffi::luaL_getmetafield(state, -1, MetaMethod::Type.as_cstr().as_ptr()) }; match name_type { - ffi::LUA_TSTRING => Ok(Some(LuaString(lua.pop_ref()).to_str()?.to_owned())), - _ => Ok(None), + ffi::LUA_TSTRING => Ok(LuaString(lua.pop_ref())), + _ => lua.create_string(b"userdata"), } } } @@ -1108,8 +1109,10 @@ impl AnyUserData { match unsafe { self.invoke_tostring_dbg() } { Ok(Some(s)) => write!(fmt, "{s}"), _ => { - let name = self.type_name().ok().flatten(); - let name = name.as_deref().unwrap_or("userdata"); + let name = self.type_name().ok(); + let name = (name.as_ref()) + .map(|s| Either::Left(s.display())) + .unwrap_or(Either::Right("userdata")); write!(fmt, "{name}: {:?}", self.to_pointer()) } } diff --git a/tests/userdata.rs b/tests/userdata.rs index 4af59814..93bbe312 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -733,6 +733,43 @@ fn test_metatable() -> Result<()> { Ok(()) } +#[test] +fn test_userdata_type_name() -> Result<()> { + struct MyUserData; + impl UserData for MyUserData {} + + struct MyUserdataCustom; + impl UserData for MyUserdataCustom { + fn add_fields>(fields: &mut F) { + fields.add_meta_field_with(MetaMethod::Type, |_| Ok("MyCustomName")); + } + } + + // mlua always sets __name/__type; override with a non-string to test the "userdata" fallback + struct MyUserdataInvalid; + impl UserData for MyUserdataInvalid { + fn add_fields>(fields: &mut F) { + fields.add_meta_field_with(MetaMethod::Type, |_| Ok(42_i64)); + } + } + + let lua = Lua::new(); + + // Default is the Rust type name + let ud = lua.create_userdata(MyUserData)?; + assert_eq!(ud.type_name()?, "MyUserData"); + + // Custom name from metatable + let ud = lua.create_userdata(MyUserdataCustom)?; + assert_eq!(ud.type_name()?, "MyCustomName"); + + // Invalid type name should fallback to "userdata" + let ud = lua.create_userdata(MyUserdataInvalid)?; + assert_eq!(ud.type_name()?.to_str()?, "userdata"); + + Ok(()) +} + #[test] fn test_userdata_proxy() -> Result<()> { struct MyUserData(i64); From 201e30bc070af96b9e4e868acd95d6d252cfa428 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 20 Apr 2026 00:05:57 +0100 Subject: [PATCH 052/138] Add `UserDataOwned` wrapper to take ownership of userdata `T` It implements `FromLua` and takes ownership of a Lua userdata value. The semantics is similar to `AnyUserData::take`, preventing any further use from Lua. Closes #686 --- src/lib.rs | 4 +-- src/prelude.rs | 2 +- src/userdata.rs | 2 +- src/userdata/ref.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++- tests/userdata.rs | 48 ++++++++++++++++++++++++++++++++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7937831a..1a00dba3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,8 +142,8 @@ pub use crate::table::{TablePairs, TableSequence}; pub use crate::thread::ThreadStatus; #[doc(hidden)] pub use crate::userdata::{ - MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, UserDataRefMut, - UserDataRegistry, + MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataOwned, UserDataRef, + UserDataRefMut, UserDataRegistry, }; #[cfg(not(feature = "luau"))] diff --git a/src/prelude.rs b/src/prelude.rs index d109d2b3..fb571e4b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,7 +9,7 @@ pub use crate::{ ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, - UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, + UserDataOwned as LuaUserDataOwned, UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, chunk::Chunk as LuaChunk, chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, diff --git a/src/userdata.rs b/src/userdata.rs index e174945d..7749c9b0 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -30,7 +30,7 @@ use { // Re-export for convenience pub(crate) use cell::UserDataStorage; -pub use r#ref::{UserDataRef, UserDataRefMut}; +pub use r#ref::{UserDataOwned, UserDataRef, UserDataRefMut}; pub use registry::UserDataRegistry; pub(crate) use registry::{RawUserDataRegistry, UserDataProxy}; pub(crate) use util::{ diff --git a/src/userdata/ref.rs b/src/userdata/ref.rs index 3cc59ef4..131b84d6 100644 --- a/src/userdata/ref.rs +++ b/src/userdata/ref.rs @@ -7,7 +7,7 @@ use crate::error::{Error, Result}; use crate::state::{Lua, RawLua}; use crate::traits::FromLua; use crate::userdata::AnyUserData; -use crate::util::get_userdata; +use crate::util::{check_stack, get_userdata, take_userdata}; use crate::value::Value; use super::cell::{UserDataStorage, UserDataVariant}; @@ -440,6 +440,66 @@ impl DerefMut for UserDataRefMutInner { } } +/// A wrapper type that takes ownership of a userdata value. +/// +/// It implements [`FromLua`] and can be used to receive a typed userdata from Lua by taking +/// ownership of it. +/// The original Lua userdata is marked as destructed and cannot be used further. +pub struct UserDataOwned(pub T); + +impl Deref for UserDataOwned { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for UserDataOwned { + #[inline] + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for UserDataOwned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for UserDataOwned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl FromLua for UserDataOwned { + fn from_lua(value: Value, _: &Lua) -> Result { + try_value_to_userdata::(value)?.take().map(UserDataOwned) + } + + unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { + let state = lua.state(); + let type_id = lua.get_userdata_type_id::(state, idx)?; + match type_id { + Some(type_id) if type_id == TypeId::of::() => { + let ud = get_userdata::>(state, idx); + if (*ud).has_exclusive_access() { + check_stack(state, 1)?; + take_userdata::>(state, idx) + .into_inner() + .map(UserDataOwned) + } else { + Err(Error::UserDataBorrowMutError) + } + } + _ => Err(Error::UserDataTypeMismatch), + } + } +} + #[inline] fn try_value_to_userdata(value: Value) -> Result { match value { @@ -464,6 +524,10 @@ mod assertions { static_assertions::assert_impl_all!(UserDataRefMut<()>: Sync, Send); #[cfg(feature = "send")] static_assertions::assert_not_impl_all!(UserDataRefMut>: Send, Sync); + #[cfg(feature = "send")] + static_assertions::assert_impl_all!(UserDataOwned<()>: Send, Sync); + #[cfg(feature = "send")] + static_assertions::assert_not_impl_all!(UserDataOwned>: Send, Sync); #[cfg(not(feature = "send"))] static_assertions::assert_not_impl_all!(UserDataRef<()>: Send, Sync); diff --git a/tests/userdata.rs b/tests/userdata.rs index 93bbe312..c5233557 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicI64, Ordering}; use mlua::{ AnyUserData, Error, ExternalError, Function, Lua, LuaString, MetaMethod, Nil, ObjectLike, Result, - UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value, Variadic, + UserData, UserDataFields, UserDataMethods, UserDataOwned, UserDataRef, UserDataRegistry, Value, Variadic, }; #[test] @@ -1459,3 +1459,49 @@ fn test_userdata_get_path() -> Result<()> { Ok(()) } + +#[test] +fn test_userdata_owned() -> Result<()> { + #[derive(Debug)] + struct MyUserdata(Arc); + + impl UserData for MyUserdata { + fn register(registry: &mut UserDataRegistry) { + registry.add_method("num", |_, this, ()| Ok(*this.0)); + } + } + + let lua = Lua::new(); + let rc = Arc::new(42); + + // It takes ownership and destructs the Lua userdata + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + assert_eq!(Arc::strong_count(&rc), 2); + let owned: UserDataOwned = lua.convert(&ud)?; + assert_eq!(*owned.0.0, 42); + drop(owned); + assert_eq!(Arc::strong_count(&rc), 1); + match ud.borrow::() { + Err(Error::UserDataDestructed) => {} + r => panic!("expected UserDataDestructed, got {:?}", r), + } + + // Cannot take while borrowed + let rc = Arc::new(7); + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + let borrowed = ud.borrow::()?; + match lua.convert::>(&ud) { + Err(Error::UserDataBorrowMutError) => {} + r => panic!("expected UserDataBorrowMutError, got {:?}", r), + } + drop(borrowed); + + // Works as a function parameter + let f = lua.create_function(|_, owned: UserDataOwned| Ok(*owned.0.0))?; + let rc = Arc::new(55); + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + assert_eq!(f.call::(ud)?, 55); + assert_eq!(Arc::strong_count(&rc), 1); // dropped after call + + Ok(()) +} From cc26dcd4ffd2c8093b705e9bfa4e12da8e5d778a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 20 Apr 2026 00:16:34 +0100 Subject: [PATCH 053/138] Bump rustyline --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 29c4b8f7..2a2d0d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ http-body-util = "0.1.1" reqwest = { version = "0.13", features = ["json"] } tempfile = "3" criterion = { version = "0.8", features = ["async_tokio"] } -rustyline = "17.0" +rustyline = "18.0" tokio = { version = "1.0", features = ["full"] } [lints.rust] From 4e827179d190f6957e252fed3da8e4986779537d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 20 Apr 2026 00:19:56 +0100 Subject: [PATCH 054/138] Update compile tests messages --- tests/compile/lua_norefunwindsafe.stderr | 22 +++++++++++----------- tests/compile/ref_nounwindsafe.stderr | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/compile/lua_norefunwindsafe.stderr b/tests/compile/lua_norefunwindsafe.stderr index 814ae6b6..4094cd2b 100644 --- a/tests/compile/lua_norefunwindsafe.stderr +++ b/tests/compile/lua_norefunwindsafe.stderr @@ -1,28 +1,28 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` +note: required because it appears within the type `PhantomData>>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` +note: required because it appears within the type `Arc>` --> $RUST/alloc/src/sync.rs | | pub struct Arc< @@ -63,22 +63,22 @@ note: required because it appears within the type `lock_api::remutex::RawReentra | | pub struct RawReentrantMutex { | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` +note: required because it appears within the type `PhantomData>>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` +note: required because it appears within the type `Arc>` --> $RUST/alloc/src/sync.rs | | pub struct Arc< diff --git a/tests/compile/ref_nounwindsafe.stderr b/tests/compile/ref_nounwindsafe.stderr index 49032555..757083df 100644 --- a/tests/compile/ref_nounwindsafe.stderr +++ b/tests/compile/ref_nounwindsafe.stderr @@ -1,24 +1,24 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` --> $RUST/alloc/src/sync.rs | | pub struct Weak< @@ -57,7 +57,7 @@ error[E0277]: the type `UnsafeCell` may contain interior mutability and a | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` note: required because it appears within the type `Cell` --> $RUST/core/src/cell.rs | @@ -68,18 +68,18 @@ note: required because it appears within the type `lock_api::remutex::RawReentra | | pub struct RawReentrantMutex { | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` --> $RUST/alloc/src/sync.rs | | pub struct Weak< From 181c9d07b7f99ec47e95786bc55499ece8e4236b Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 20 Apr 2026 00:24:58 +0100 Subject: [PATCH 055/138] v0.12.0-rc.1 --- Cargo.toml | 4 ++-- mlua-sys/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2a2d0d8f..1488205b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua" -version = "0.12.0-dev.1" # remember to update mlua_derive +version = "0.12.0-rc.1" # remember to update mlua_derive authors = ["Aleksandr Orlenko ", "kyren "] rust-version = "1.88" edition = "2024" @@ -63,7 +63,7 @@ parking_lot = { version = "0.12", features = ["arc_lock"] } anyhow = { version = "1.0", optional = true } libc = "0.2" -ffi = { package = "mlua-sys", version = "0.10.0", path = "mlua-sys" } +ffi = { package = "mlua-sys", version = "0.11.0-rc.1", path = "mlua-sys" } [dev-dependencies] trybuild = "1.0" diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index 09ddca3b..e1bb93ce 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua-sys" -version = "0.10.0" +version = "0.11.0-rc.1" authors = ["Aleksandr Orlenko "] rust-version = "1.88" edition = "2024" From 5f0e06fb66d8d5ec59b75dc8a21e4291496e4dc4 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 22 Apr 2026 00:06:11 +0100 Subject: [PATCH 056/138] Update CHANGELOG --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 257ee8fa..fb8e8562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## v0.12.0-rc.1 (Apr 21, 2026) + +- Rust 2024 edition +- Removed `Error::ToLuaConversionError` variant as it was unused (and not practically useful) +- New modules to group data types: `chunk`, `debug`, `error`, `function`, `table`, `string`, `state`, `thread`, `userdata`, `luau` +- Support `__todebugstring` metamethod for pretty formatting userdata value (for debugging) +- New `MaybeSync` trait that is required for userdata types +- Removed lifetime from `BorrowedStr` and `BorrowedBytes` +- New `Thread` methods: `is_resumable`, `is_running`, `is_finished`, `is_error` +- Added `Thread::state` to get raw Lua state pointer +- Luau `TextRequirer` is renamed to `FsRequirer` +- GC interface refactor: `Lua::gc_inc/Lua::gc_gen` is replaced with `gc_set_mode` +- Added `GcIncParams` and `GcGenParams` for GC tuning +- New `UserDataMethods::add_method_once` and `UserDataMethods::add_async_method_once` +- Initial Luau integer64 type support +- Changed interface of `Function::wrap/wrap_mut/wrap_async` to support any Error type +- Changed `AnyUserData::type_name` to return `LuaString` instead +- Added `UserDataOwned` wrapper to take ownership of userdata `T` and implements `FromLua` + ## v0.11.6 (Jan 27, 2026) - Added Lua 5.5 support (`lua55` feature flag) From c54b90623c077bf2a92786e5cfad21c0792e1bb7 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 22 Apr 2026 22:23:25 +0100 Subject: [PATCH 057/138] Fix `String::to_pointer` return NULL in Lua <5.4 Lua 5.4+ can return string pointer when calling `lua_topointer`. In earlier Lua versions this API always returns NULL. Let's unify this behavior. --- src/string.rs | 5 ++++- src/value.rs | 7 +------ tests/value.rs | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/string.rs b/src/string.rs index c7961a2e..03dfbcbf 100644 --- a/src/string.rs +++ b/src/string.rs @@ -149,7 +149,10 @@ impl LuaString { /// Typically this function is used only for hashing and debug information. #[inline] pub fn to_pointer(&self) -> *const c_void { - self.0.to_pointer() + // In Lua < 5.4 (excluding Luau), string pointers are NULL + // Use alternative approach + let lua = self.0.lua.lock(); + unsafe { ffi::lua_tostring(lua.ref_thread(), self.0.index) as *const c_void } } } diff --git a/src/value.rs b/src/value.rs index 10d4c4d0..b4891e73 100644 --- a/src/value.rs +++ b/src/value.rs @@ -128,18 +128,13 @@ impl Value { #[inline] pub fn to_pointer(&self) -> *const c_void { match self { - Value::String(LuaString(vref)) => { - // In Lua < 5.4 (excluding Luau), string pointers are NULL - // Use alternative approach - let lua = vref.lua.lock(); - unsafe { ffi::lua_tostring(lua.ref_thread(), vref.index) as *const c_void } - } Value::LightUserData(ud) => ud.0, Value::Table(Table(vref)) | Value::Function(Function(vref)) | Value::Thread(Thread(vref, ..)) | Value::UserData(AnyUserData(vref)) | Value::Other(vref) => vref.to_pointer(), + Value::String(s) => s.to_pointer(), #[cfg(feature = "luau")] Value::Buffer(crate::Buffer(vref)) => vref.to_pointer(), _ => ptr::null(), diff --git a/tests/value.rs b/tests/value.rs index 506aaa0e..9ed3b2bf 100644 --- a/tests/value.rs +++ b/tests/value.rs @@ -65,7 +65,7 @@ fn test_value_eq() -> Result<()> { assert!(!table1.to_pointer().is_null()); assert!(!ptr::eq(table1.to_pointer(), table2.to_pointer())); - assert!(ptr::eq(string1.to_pointer(), string2.to_pointer())); + assert!(ptr::eq(string1.to_pointer(), string2.to_pointer()) && !string1.to_pointer().is_null()); assert!(ptr::eq(func1.to_pointer(), func2.to_pointer())); assert!(num1.to_pointer().is_null()); From 72824a468a1fdb78c3cb40c2709be2fa714ce1cd Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 22 Apr 2026 22:28:17 +0100 Subject: [PATCH 058/138] Remove custom `PartialEq` for `LuaString` and use derived one Lua can compare strings using `lua_rawequal` and it's more efficient than always compare bytes. Under the hood Lua compare pointers for interned strings and content for long ones. Close #694 --- src/string.rs | 8 +------- tests/string.rs | 36 +++++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/string.rs b/src/string.rs index 03dfbcbf..63ceaf3f 100644 --- a/src/string.rs +++ b/src/string.rs @@ -23,7 +23,7 @@ use { /// Handle to an internal Lua string. /// /// Unlike Rust strings, Lua strings may not be valid UTF-8. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct LuaString(pub(crate) ValueRef); impl LuaString { @@ -186,12 +186,6 @@ where } } -impl PartialEq for LuaString { - fn eq(&self, other: &LuaString) -> bool { - self.as_bytes() == other.as_bytes() - } -} - impl Eq for LuaString {} impl PartialOrd for LuaString diff --git a/tests/string.rs b/tests/string.rs index 158a5b5f..6802d906 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -5,27 +5,37 @@ use mlua::{Lua, LuaString, Result}; #[test] fn test_string_compare() { - fn with_str(s: &str, f: F) { - f(Lua::new().create_string(s).unwrap()); + let lua = Lua::new(); + + fn with_str(lua: &Lua, s: &str, f: F) { + f(lua.create_string(s).unwrap()); } // Tests that all comparisons we want to have are usable - with_str("teststring", |t| assert_eq!(t, "teststring")); // &str - with_str("teststring", |t| assert_eq!(t, b"teststring")); // &[u8] - with_str("teststring", |t| assert_eq!(t, b"teststring".to_vec())); // Vec - with_str("teststring", |t| assert_eq!(t, "teststring".to_string())); // String - with_str("teststring", |t| assert_eq!(t, t)); // mlua::String - with_str("teststring", |t| assert_eq!(t, Cow::from(b"teststring".as_ref()))); // Cow (borrowed) - with_str("bla", |t| assert_eq!(t, Cow::from(b"bla".to_vec()))); // Cow (owned) + with_str(&lua, "teststring", |t| assert_eq!(t, "teststring")); // &str + with_str(&lua, "teststring", |t| assert_eq!(t, b"teststring")); // &[u8] + with_str(&lua, "teststring", |t| assert_eq!(t, b"teststring".to_vec())); // Vec + with_str(&lua, "teststring", |t| assert_eq!(t, "teststring".to_string())); // String + with_str(&lua, "teststring", |t| assert_eq!(t, t)); // mlua::String + with_str(&lua, "teststring", |t| { + assert_eq!(t, Cow::from(b"teststring".as_ref())) // Cow (borrowed) + }); + with_str(&lua, "bla", |t| assert_eq!(t, Cow::from(b"bla".to_vec()))); // Cow (owned) // Test ordering - with_str("a", |a| { + with_str(&lua, "a", |a| { assert!(!(a < a)); assert!(!(a > a)); }); - with_str("a", |a| assert!(a < "b")); - with_str("a", |a| assert!(a < b"b")); - with_str("a", |a| with_str("b", |b| assert!(a < b))); + with_str(&lua, "a", |a| assert!(a < "b")); + with_str(&lua, "a", |a| assert!(a < b"b")); + with_str(&lua, "a", |a| with_str(&lua, "b", |b| assert!(a < b))); + + // Long strings (not interned by Lua) + let long_str = "abc".repeat(100); + with_str(&lua, &long_str, |s1| { + with_str(&lua, &long_str, |s2| assert_eq!(s1, s2)) + }); } #[test] From 4aa6214b45ddc7b9099a1f3b280969b1d6cdbe26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=AF=E5=87=9B?= <34085039+mokurin000@users.noreply.github.com> Date: Fri, 1 May 2026 05:56:50 +0800 Subject: [PATCH 059/138] feat: implement `Not` for `StdLib` (#699) --- src/stdlib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stdlib.rs b/src/stdlib.rs index 48111fed..e1c32e9a 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -1,4 +1,4 @@ -use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; /// Flags describing the set of lua standard libraries to load. #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -144,3 +144,10 @@ impl BitXorAssign for StdLib { *self = StdLib(self.0 ^ rhs.0) } } + +impl Not for StdLib { + type Output = Self; + fn not(self) -> Self::Output { + StdLib(!self.0) + } +} From 39d3201848fe4bbccf93075ef4b0b8b71ca1719f Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Tue, 26 May 2026 23:13:44 +0100 Subject: [PATCH 060/138] Prevent `XRc` overflow when dropping `RawLua` with foreign Lua state Calling `lua_close` triggers GC collection of the `ExtraData` that cascades to `RawLua::drop` where extra is already decremented to 0, causing a subtraction overflow. --- src/state/raw.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/state/raw.rs b/src/state/raw.rs index dcc860a3..06b9bfae 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -1,7 +1,7 @@ use std::any::TypeId; use std::cell::{Cell, UnsafeCell}; use std::ffi::CStr; -use std::mem; +use std::mem::{self, ManuallyDrop}; use std::os::raw::{c_char, c_int, c_void}; use std::panic::resume_unwind; use std::ptr::{self, NonNull}; @@ -56,7 +56,7 @@ pub struct RawLua { // The state is dynamic and depends on context pub(super) state: Cell<*mut ffi::lua_State>, pub(super) main_state: Option>, - pub(super) extra: XRc>, + pub(super) extra: ManuallyDrop>>, owned: bool, } @@ -82,6 +82,9 @@ impl Drop for RawLua { if !mem_state.is_null() { drop(Box::from_raw(mem_state)); } + + // Drop the `ExtraData` reference after `lua_close` has collected the registry entry + ManuallyDrop::drop(&mut self.extra); } } } @@ -245,7 +248,7 @@ impl RawLua { state: Cell::new(state), // Make sure that we don't store current state as main state (if it's not available) main_state: get_main_state(state).and_then(NonNull::new), - extra: XRc::clone(&extra), + extra: ManuallyDrop::new(XRc::clone(&extra)), owned, })); (*extra.get()).set_lua(&rawlua); From 1573dd1242308c7786ff78b0776b419e88567688 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 00:03:11 +0100 Subject: [PATCH 061/138] Add #[mlua::userdata] and #[mlua::userdata_impl] macros --- Cargo.toml | 3 +- examples/userdata.rs | 54 +- mlua_derive/Cargo.toml | 2 +- mlua_derive/src/attr.rs | 87 +++ mlua_derive/src/from_lua.rs | 2 +- mlua_derive/src/lib.rs | 36 +- mlua_derive/src/userdata.rs | 139 +++++ mlua_derive/src/userdata_impl.rs | 550 ++++++++++++++++++ src/lib.rs | 23 + tests/compile.rs | 14 + tests/compile/lua_norefunwindsafe.stderr | 80 +-- tests/compile/ref_nounwindsafe.stderr | 129 ++-- tests/compile/userdata_const_getter.rs | 11 + tests/compile/userdata_const_getter.stderr | 5 + tests/compile/userdata_getter_and_meta.rs | 13 + tests/compile/userdata_getter_and_meta.stderr | 5 + tests/compile/userdata_getter_and_setter.rs | 15 + .../compile/userdata_getter_and_setter.stderr | 5 + tests/compile/userdata_getter_extra_arg.rs | 15 + .../compile/userdata_getter_extra_arg.stderr | 5 + tests/compile/userdata_getter_mut_self.rs | 15 + tests/compile/userdata_getter_mut_self.stderr | 5 + tests/compile/userdata_meta_owned_self.rs | 13 + tests/compile/userdata_meta_owned_self.stderr | 5 + tests/compile/userdata_mut_slice_arg.rs | 12 + tests/compile/userdata_mut_slice_arg.stderr | 5 + tests/compile/userdata_setter_no_value.rs | 15 + tests/compile/userdata_setter_no_value.stderr | 5 + tests/compile/userdata_setter_ref_self.rs | 16 + tests/compile/userdata_setter_ref_self.stderr | 5 + tests/compile/userdata_static_with_self.rs | 15 + .../compile/userdata_static_with_self.stderr | 5 + tests/userdata_macro.rs | 374 ++++++++++++ 33 files changed, 1564 insertions(+), 119 deletions(-) create mode 100644 mlua_derive/src/attr.rs create mode 100644 mlua_derive/src/userdata.rs create mode 100644 mlua_derive/src/userdata_impl.rs create mode 100644 tests/compile/userdata_const_getter.rs create mode 100644 tests/compile/userdata_const_getter.stderr create mode 100644 tests/compile/userdata_getter_and_meta.rs create mode 100644 tests/compile/userdata_getter_and_meta.stderr create mode 100644 tests/compile/userdata_getter_and_setter.rs create mode 100644 tests/compile/userdata_getter_and_setter.stderr create mode 100644 tests/compile/userdata_getter_extra_arg.rs create mode 100644 tests/compile/userdata_getter_extra_arg.stderr create mode 100644 tests/compile/userdata_getter_mut_self.rs create mode 100644 tests/compile/userdata_getter_mut_self.stderr create mode 100644 tests/compile/userdata_meta_owned_self.rs create mode 100644 tests/compile/userdata_meta_owned_self.stderr create mode 100644 tests/compile/userdata_mut_slice_arg.rs create mode 100644 tests/compile/userdata_mut_slice_arg.stderr create mode 100644 tests/compile/userdata_setter_no_value.rs create mode 100644 tests/compile/userdata_setter_no_value.stderr create mode 100644 tests/compile/userdata_setter_ref_self.rs create mode 100644 tests/compile/userdata_setter_ref_self.stderr create mode 100644 tests/compile/userdata_static_with_self.rs create mode 100644 tests/compile/userdata_static_with_self.stderr create mode 100644 tests/userdata_macro.rs diff --git a/Cargo.toml b/Cargo.toml index 1488205b..6d8f44a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ async = ["dep:futures-util"] send = ["error-send"] error-send = [] serde = ["dep:serde", "dep:erased-serde", "dep:serde-value", "bstr/serde"] -macros = ["mlua_derive/macros"] +macros = ["mlua_derive/macros", "dep:inventory"] anyhow = ["dep:anyhow", "error-send"] userdata-wrappers = ["parking_lot/send_guard"] @@ -61,6 +61,7 @@ erased-serde = { version = "0.4", optional = true } serde-value = { version = "0.7", optional = true } parking_lot = { version = "0.12", features = ["arc_lock"] } anyhow = { version = "1.0", optional = true } +inventory = { version = "0.3", optional = true } libc = "0.2" ffi = { package = "mlua-sys", version = "0.11.0-rc.1", path = "mlua-sys" } diff --git a/examples/userdata.rs b/examples/userdata.rs index 6a21e90b..986e546f 100644 --- a/examples/userdata.rs +++ b/examples/userdata.rs @@ -1,45 +1,47 @@ -use mlua::{Lua, MetaMethod, Result, UserData, chunk}; +use mlua::{Lua, Result, chunk}; #[derive(Default)] +#[mlua::userdata] struct Rectangle { length: u32, width: u32, } -impl UserData for Rectangle { - fn add_fields>(fields: &mut F) { - fields.add_field_method_get("length", |_, this| Ok(this.length)); - fields.add_field_method_set("length", |_, this, val| { - this.length = val; - Ok(()) - }); - fields.add_field_method_get("width", |_, this| Ok(this.width)); - fields.add_field_method_set("width", |_, this, val| { - this.width = val; - Ok(()) - }); +#[mlua::userdata_impl] +impl Rectangle { + const NAME: &str = "Rectangle"; + + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Self { length, width } + } + + #[lua(getter, name = "area", infallible)] + fn calculate_area(&self) -> u32 { + self.length * self.width } - fn add_methods>(methods: &mut M) { - methods.add_method("area", |_, this, ()| Ok(this.length * this.width)); - methods.add_method("diagonal", |_, this, ()| { - Ok((this.length.pow(2) as f64 + this.width.pow(2) as f64).sqrt()) - }); + fn diagonal(&self) -> Result { + Ok((self.length.pow(2) as f64 + self.width.pow(2) as f64).sqrt()) + } - // Constructor - methods.add_meta_function(MetaMethod::Call, |_, ()| Ok(Rectangle::default())); + // Constructor via `__call` metamethod + #[lua(meta, infallible)] + fn __call(length: u32, width: u32) -> Self { + Rectangle::new(length, width) } } fn main() -> Result<()> { let lua = Lua::new(); - let rectangle = Rectangle::default(); + lua.globals().set("Rectangle", lua.create_proxy::()?)?; lua.load(chunk! { - local rect = $rectangle() - rect.width = 10 - rect.length = 5 - assert(rect:area() == 50) - assert(rect:diagonal() - 11.1803 < 0.0001) + local rect = Rectangle(10, 5) + rect.width = rect.width + 5 + rect.length = rect.length + 5 + assert(rect.NAME == "Rectangle") + assert(rect.area == 150) + assert(math.floor(rect:diagonal()) == 18) }) .exec() } diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 74d3c1ad..7454f63c 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -2,7 +2,7 @@ name = "mlua_derive" version = "0.11.0" authors = ["Aleksandr Orlenko "] -edition = "2021" +edition = "2024" description = "Procedural macros for the mlua crate." repository = "https://github.com/mlua-rs/mlua" keywords = ["lua", "mlua"] diff --git a/mlua_derive/src/attr.rs b/mlua_derive/src/attr.rs new file mode 100644 index 00000000..b6457810 --- /dev/null +++ b/mlua_derive/src/attr.rs @@ -0,0 +1,87 @@ +use proc_macro2::Span; +use syn::meta::ParseNestedMeta; +use syn::{Ident, LitStr, Result}; + +/// Parsed `#[lua(...)]` attribute. +/// +/// Some flags are context-dependent: +/// - Struct fields: `get`, `set`, `name`, `skip` +/// - Impl methods: `getter`, `setter`, `field`, `meta`, `infallible`, `name`, `skip` +#[derive(Default)] +pub(crate) struct LuaAttr { + pub(crate) name: Option, + pub(crate) infallible: bool, + pub(crate) skip: bool, + + // Struct field context flags + pub(crate) get: bool, + pub(crate) set: bool, + + // Impl method context flags + pub(crate) getter: bool, + pub(crate) setter: bool, + pub(crate) field: bool, + pub(crate) meta: bool, +} + +impl LuaAttr { + pub(crate) fn parse_inner(&mut self, meta: ParseNestedMeta) -> Result<()> { + match &meta.path { + path if path.is_ident("skip") => { + if meta.value().is_ok() { + return Err(meta.error("`skip` does not take a value")); + } + self.skip = true; + } + path if path.is_ident("infallible") => { + if meta.value().is_ok() { + return Err(meta.error("`infallible` does not take a value")); + } + self.infallible = true; + } + path if path.is_ident("get") => self.get = true, + path if path.is_ident("set") => self.set = true, + path if path.is_ident("getter") => self.getter = true, + path if path.is_ident("setter") => self.setter = true, + path if path.is_ident("field") => self.field = true, + path if path.is_ident("meta") => self.meta = true, + path if path.is_ident("name") => { + let value = meta.value()?; + let lit: LitStr = value.parse()?; + self.name = Some(lit.value()); + } + _ => { + return Err(meta.error( + "unsupported lua attribute, expected: ".to_string() + + "`skip`, `infallible`, `get`, `set`, `getter`, `setter`, `field`, `meta`, `name`", + )); + } + } + Ok(()) + } + + /// Returns the effective Lua name. + pub(crate) fn name(&self, ident: &Ident) -> String { + self.name.clone().unwrap_or_else(|| ident.to_string()) + } + + /// Returns the effective Lua metamethod name. + /// + /// If `name` is set via attribute, use it. Otherwise, if the function name + /// starts with `__`, use that. Returns an error if neither is available. + pub(crate) fn effective_meta_name(&self, fn_name: &Ident) -> Result { + if let Some(ref name) = self.name { + return Ok(name.clone()); + } + let fn_name = fn_name.to_string(); + if fn_name.starts_with("__") { + return Ok(fn_name); + } + Err(syn::Error::new( + Span::call_site(), + format!( + "could not infer metamethod name from `{fn_name}`, either add `name = \"...\"` to `#[lua(meta, ...)]` or prefix the function with `__`" + ), + )) + } +} diff --git a/mlua_derive/src/from_lua.rs b/mlua_derive/src/from_lua.rs index e74eb868..fbd2f22f 100644 --- a/mlua_derive/src/from_lua.rs +++ b/mlua_derive/src/from_lua.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::{DeriveInput, parse_macro_input}; pub fn from_lua(input: TokenStream) -> TokenStream { let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput); diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index f7d04803..74bcf295 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -2,14 +2,24 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::quote; use syn::meta::ParseNestedMeta; -use syn::{parse_macro_input, ItemFn, LitStr, Result}; +use syn::{ItemFn, LitStr, Result, parse_macro_input}; #[cfg(feature = "macros")] use { - crate::chunk::Chunk, proc_macro::TokenTree, proc_macro2::TokenStream as TokenStream2, - proc_macro_error2::proc_macro_error, + crate::chunk::Chunk, proc_macro::TokenTree, proc_macro_error2::proc_macro_error, + proc_macro2::TokenStream as TokenStream2, }; +#[cfg(feature = "macros")] +macro_rules! try_compile { + ($expr:expr) => { + match $expr { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), + } + }; +} + #[derive(Default)] struct ModuleAttributes { name: Option, @@ -151,9 +161,29 @@ pub fn from_lua(input: TokenStream) -> TokenStream { from_lua::from_lua(input) } +/// Attribute macro for exposing a Rust type as a Lua userdata. +#[cfg(feature = "macros")] +#[proc_macro_attribute] +pub fn userdata(attr: TokenStream, item: TokenStream) -> TokenStream { + userdata::userdata_type(attr, item) +} + +/// Attribute macro for exposing impl block methods to Lua userdata. +#[cfg(feature = "macros")] +#[proc_macro_attribute] +pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + userdata_impl::userdata_impl(attr, item) +} + +#[cfg(feature = "macros")] +mod attr; #[cfg(feature = "macros")] mod chunk; #[cfg(feature = "macros")] mod from_lua; #[cfg(feature = "macros")] mod token; +#[cfg(feature = "macros")] +mod userdata; +#[cfg(feature = "macros")] +mod userdata_impl; diff --git a/mlua_derive/src/userdata.rs b/mlua_derive/src/userdata.rs new file mode 100644 index 00000000..393f3b62 --- /dev/null +++ b/mlua_derive/src/userdata.rs @@ -0,0 +1,139 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; + +use crate::attr::LuaAttr; + +/// 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(); + for attr in attrs { + if attr.path().is_ident("lua") + && let Meta::List(_) = &attr.meta + { + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + } + } + Ok(lua_attr) +} + +/// Strip `#[lua(...)]` attributes from a field, keeping all others. +fn strip_lua_attrs(attrs: &[Attribute]) -> Vec { + (attrs.iter()) + .filter(|attr| !attr.path().is_ident("lua")) + .cloned() + .collect() +} + +pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return Error::new_spanned( + proc_macro2::TokenStream::from(attr), + "`#[userdata]` does not accept arguments", + ) + .to_compile_error() + .into(); + } + + let mut input = parse_macro_input!(item as DeriveInput); + let type_name = &input.ident; + + let mut named_fields: Option<&mut FieldsNamed> = match &mut input.data { + Data::Struct(data) => match &mut data.fields { + Fields::Named(fields) => Some(fields), + Fields::Unnamed(_) | Fields::Unit => None, + }, + Data::Enum(_) => None, + Data::Union(_) => { + return Error::new_spanned(&input, "`#[userdata]` cannot be applied to unions") + .to_compile_error() + .into(); + } + }; + + // Check for generic type parameters (not supported) + let has_type_params = input.generics.type_params().next().is_some(); + if has_type_params { + return Error::new_spanned( + &input.generics, + "`#[userdata]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead." + ) + .to_compile_error() + .into(); + } + + let mut field_registrations = Vec::new(); + if let Some(fields) = &mut named_fields { + for field in &fields.named { + let field_name = field.ident.as_ref().unwrap(); + + let lua_attr = try_compile!(parse_field_lua_attr(&field.attrs)); + if lua_attr.skip { + continue; + } + + let lua_name = lua_attr.name.unwrap_or_else(|| field_name.to_string()); + + // Assume get/set by default (unless explicitly specified) + let (has_get, has_set) = if lua_attr.get || lua_attr.set { + (lua_attr.get, lua_attr.set) + } else { + (true, true) + }; + + if has_get { + field_registrations.push(quote! { + registry.add_field_method_get(#lua_name, |_lua, this| Ok(this.#field_name.clone())); + }); + } + if has_set { + field_registrations.push(quote! { + registry.add_field_method_set(#lua_name, |_lua, this, val| { + this.#field_name = val; + Ok(()) + }); + }); + } + } + + // Strip mlua-specific attributes from fields before re-emitting + for field in &mut fields.named { + field.attrs = strip_lua_attrs(&field.attrs); + } + } + + let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}"); + let register_fields_fn_name = format_ident!("__mlua_register_{type_name}_fields"); + + let output = quote! { + #input + + #[doc(hidden)] + #[allow(non_camel_case_types)] + struct #registration_type_name { + register: fn(&mut ::mlua::userdata::UserDataRegistry<#type_name>), + } + + ::mlua::__inventory::collect!(#registration_type_name); + + #[allow(non_snake_case)] + fn #register_fields_fn_name(registry: &mut ::mlua::userdata::UserDataRegistry<#type_name>) { + use ::mlua::userdata::UserDataFields as _; + #(#field_registrations)* + } + + ::mlua::__inventory::submit! { + #registration_type_name { register: #register_fields_fn_name } + } + + impl ::mlua::userdata::UserData for #type_name { + fn register(registry: &mut ::mlua::userdata::UserDataRegistry) { + for item in ::mlua::__inventory::iter::<#registration_type_name> { + (item.register)(registry); + } + } + } + }; + + output.into() +} diff --git a/mlua_derive/src/userdata_impl.rs b/mlua_derive/src/userdata_impl.rs new file mode 100644 index 00000000..10662cb7 --- /dev/null +++ b/mlua_derive/src/userdata_impl.rs @@ -0,0 +1,550 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + Attribute, FnArg, Ident, ImplItem, ItemImpl, Meta, Signature, Type, parse_macro_input, parse_quote, +}; + +use crate::attr::LuaAttr; + +/// `&T` reference types that mlua provides as wrapper types via `FromLua`. +static BORROW_WRAPPERS: &[(&str, &str)] = &[ + ("str", "::mlua::string::BorrowedStr"), + ("[u8]", "::mlua::string::BorrowedBytes"), +]; + +enum SelfKind { + Ref(RefKind), + Owned, + None, +} + +enum RefKind { + Ref, + Mut, +} + +struct ArgInfo { + ident: Ident, + userdata_ref: Option, + callback_type: Type, +} + +struct MethodInfo { + self_kind: SelfKind, + has_lua: bool, + args: Vec, +} + +/// Extract the inner type from a reference type. +fn ref_inner_type(ty: &Type) -> Type { + match ty { + Type::Reference(ref_ty) => (*ref_ty.elem).clone(), + _ => ty.clone(), + } +} + +/// Check if the type is `&Lua` or `&mlua::Lua`. +fn is_lua_ref(ty: &Type) -> bool { + let Type::Reference(ref_ty) = ty else { return false }; + match &*ref_ty.elem { + Type::Path(p) if p.path.segments.len() == 1 => p.path.segments[0].ident == "Lua", + Type::Path(p) if p.path.segments.len() == 2 => { + p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua" + } + _ => false, + } +} + +/// Classify a `&[mut] T` parameter, returning the callback wrapper type. +/// +/// Known borrow types come from the mapping table `BORROW_WRAPPERS`. +/// Everything else gets `UserDataRef[Mut]`. +fn classify_ref_type(ty: &Type) -> Option { + let Type::Reference(ref_ty) = ty else { return None }; + + // Check known borrow wrappers: + // - For `&T` check the path name + // - For `&[T]` unpack the slice and format the element as `[T]` for lookup + if ref_ty.mutability.is_none() { + let lookup_name: Option = match &*ref_ty.elem { + Type::Path(path) => path.path.segments.last().map(|seg| seg.ident.to_string()), + Type::Slice(slice) + if let Type::Path(path) = &*slice.elem + && let Some(seg) = path.path.segments.last() => + { + Some(format!("[{}]", seg.ident)) + } + _ => None, + }; + if let Some(ref name) = lookup_name { + for &(inner, wrapper) in BORROW_WRAPPERS { + if name == inner { + let wrapper = syn::parse_str(wrapper).expect("invalid wrapper type"); + return Some(wrapper); + } + } + } + } + // Mutable references to slices are not supported. + if matches!(&*ref_ty.elem, Type::Slice(_)) && ref_ty.mutability.is_some() { + return None; + } + + let inner = ref_inner_type(ty); + if ref_ty.mutability.is_none() { + Some(parse_quote! { ::mlua::userdata::UserDataRef<#inner> }) + } else { + Some(parse_quote! { ::mlua::userdata::UserDataRefMut<#inner> }) + } +} + +/// Analyze method signature. +/// +/// Determine `self` kind and collect the callback arguments. +/// Auto-detects `&Lua` as the first non-self parameter. +fn analyze_self_and_args(sig: &Signature) -> syn::Result { + let mut self_kind = SelfKind::None; + let mut has_lua = false; + let mut args = Vec::new(); + let mut check_first_typed = true; + + for param in &sig.inputs { + match param { + FnArg::Receiver(recv) if recv.reference.is_some() && recv.mutability.is_some() => { + self_kind = SelfKind::Ref(RefKind::Mut); + } + FnArg::Receiver(recv) if recv.reference.is_some() => { + self_kind = SelfKind::Ref(RefKind::Ref); + } + FnArg::Receiver(_) => { + self_kind = SelfKind::Owned; + } + FnArg::Typed(typed) => { + if check_first_typed && is_lua_ref(&typed.ty) { + has_lua = true; + check_first_typed = false; + continue; + } + check_first_typed = false; + if let syn::Pat::Ident(pat_ident) = &*typed.pat { + let arg_type = &*typed.ty; + let ref_kind = match arg_type { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::Mut), + Type::Reference(_) => Some(RefKind::Ref), + _ => None, + }; + let callback_type = match &ref_kind { + Some(_) => match classify_ref_type(arg_type) { + Some(ty) => ty, + None => { + return Err(syn::Error::new_spanned( + arg_type, + "this reference type is not supported as a callback parameter", + )); + } + }, + None => arg_type.clone(), + }; + args.push(ArgInfo { + ident: pat_ident.ident.clone(), + userdata_ref: ref_kind, + callback_type, + }); + } + } + } + } + + Ok(MethodInfo { + self_kind, + has_lua, + args, + }) +} + +fn strip_item_attrs(attrs: &[Attribute]) -> Vec { + (attrs.iter()) + .filter(|attr| !attr.path().is_ident("lua")) + .cloned() + .collect() +} + +fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { + let mut lua_attr = LuaAttr::default(); + for attr in attrs { + if attr.path().is_ident("lua") + && let Meta::List(_) = &attr.meta + { + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + } + } + Ok(lua_attr) +} + +pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return syn::Error::new_spanned( + proc_macro2::TokenStream::from(attr), + "`#[userdata_impl]` does not accept arguments", + ) + .to_compile_error() + .into(); + } + + let mut input = parse_macro_input!(item as ItemImpl); + + let type_path = match &*input.self_ty { + Type::Path(type_path) => &type_path.path, + _ => { + return syn::Error::new_spanned(&input.self_ty, "`#[userdata_impl]` requires a simple path type") + .to_compile_error() + .into(); + } + }; + let type_name = (type_path.segments) + .last() + .map(|seg| seg.ident.clone()) + .ok_or_else(|| syn::Error::new_spanned(&input.self_ty, "cannot determine type name")); + let type_name = try_compile!(type_name); + + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let unique_suffix = COUNTER.fetch_add(1, Ordering::Relaxed); + let register_fn_name = format_ident!("__mlua_register_{type_name}_{unique_suffix}"); + let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}"); + + let mut registration_calls = Vec::new(); + for item in &input.items { + match item { + ImplItem::Const(const_item) => { + let lua_attr = try_compile!(parse_lua_attr(&const_item.attrs)); + if lua_attr.skip { + continue; + } + if lua_attr.getter || lua_attr.setter { + return syn::Error::new_spanned( + &const_item.ident, + "const items do not support `getter` or `setter`", + ) + .to_compile_error() + .into(); + } + let const_name = &const_item.ident; + let lua_name = lua_attr.name(const_name); + if lua_attr.meta { + registration_calls.push(quote! { + registry.add_meta_field(#lua_name, #type_path::#const_name); + }); + } else { + registration_calls.push(quote! { + registry.add_field(#lua_name, #type_path::#const_name); + }); + } + } + ImplItem::Fn(method) => { + let lua_attr = try_compile!(parse_lua_attr(&method.attrs)); + if lua_attr.skip { + continue; + } + + // Validate mutually exclusive role flags. + // `getter`, `setter`, `field` are exclusive. + // `meta` on its own means a metamethod. + // `meta` combined with `field` means a meta static field. + // `meta` with `getter` or `setter` is invalid. + let primary = [lua_attr.getter, lua_attr.setter, lua_attr.field]; + let primary_count = primary.iter().filter(|&&x| x).count(); + if primary_count > 1 { + return syn::Error::new_spanned( + &method.sig, + "at most one of `getter`, `setter`, `field` can be specified", + ) + .to_compile_error() + .into(); + } + if lua_attr.meta && primary_count == 1 && !lua_attr.field { + return syn::Error::new_spanned(&method.sig, "`meta` can only be combined with `field`") + .to_compile_error() + .into(); + } + + let fn_name = &method.sig.ident; + let info = try_compile!(analyze_self_and_args(&method.sig)); + + if lua_attr.getter { + if !matches!(info.self_kind, SelfKind::Ref(RefKind::Ref)) { + return syn::Error::new_spanned(&method.sig, "field getter must take `&self`") + .to_compile_error() + .into(); + } + if !info.args.is_empty() { + return syn::Error::new_spanned( + &method.sig, + "field getter must not take additional arguments", + ) + .to_compile_error() + .into(); + } + registration_calls.push(gen_field_getter(type_path, fn_name, &lua_attr, &info)); + continue; + } + if lua_attr.setter { + if !matches!(info.self_kind, SelfKind::Ref(_)) { + return syn::Error::new_spanned(&method.sig, "field setter must take `&[mut] self`") + .to_compile_error() + .into(); + } + if info.args.len() != 1 { + return syn::Error::new_spanned( + &method.sig, + "field setter must take exactly one value argument", + ) + .to_compile_error() + .into(); + } + registration_calls.push(gen_field_setter(type_path, fn_name, &lua_attr, &info)); + continue; + } + if lua_attr.field { + if !matches!(info.self_kind, SelfKind::None) { + return syn::Error::new_spanned(&method.sig, "field function must not take `self`") + .to_compile_error() + .into(); + } + 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()); + }); + } else { + registration_calls.push(quote! { + registry.add_field(#lua_name, #type_path::#fn_name()); + }); + } + continue; + } + + if lua_attr.meta { + if matches!(info.self_kind, SelfKind::Owned) { + return syn::Error::new_spanned( + &method.sig, + "meta methods cannot take `self`, use `&[mut] self` instead", + ) + .to_compile_error() + .into(); + } + registration_calls.push(gen_meta(type_path, fn_name, &lua_attr, &info)); + continue; + } + + registration_calls.push(gen_regular_method(type_path, fn_name, &lua_attr, &info)); + } + _ => {} + } + } + + for item in &mut input.items { + match item { + ImplItem::Const(c) => c.attrs = strip_item_attrs(&c.attrs), + ImplItem::Fn(m) => m.attrs = strip_item_attrs(&m.attrs), + _ => {} + } + } + input.attrs = strip_item_attrs(&input.attrs); + + let output = quote! { + #[allow(non_snake_case)] + fn #register_fn_name(registry: &mut ::mlua::userdata::UserDataRegistry<#type_path>) { + use ::mlua::userdata::{UserDataFields as _, UserDataMethods as _}; + #(#registration_calls)* + } + + ::mlua::__inventory::submit! { + #registration_type_name { register: #register_fn_name } + } + + #input + }; + + output.into() +} + +/// Generate the closure argument destructuring pattern. +fn gen_closure_destructure(info: &MethodInfo) -> TokenStream2 { + if info.args.is_empty() { + return quote! { () }; + } + let idents: Vec<_> = (info.args) + .iter() + .map(|a| { + let ident = &a.ident; + if matches!(a.userdata_ref, Some(RefKind::Mut)) { + quote! { mut #ident } + } else { + quote! { #ident } + } + }) + .collect(); + let types: Vec<_> = info.args.iter().map(|a| &a.callback_type).collect(); + quote! { (#(#idents),*): (#(#types),*) } +} + +/// Generate call arguments for invoking the original method. +fn gen_call_args(info: &MethodInfo) -> TokenStream2 { + let mut call_args: Vec = Vec::new(); + + match info.self_kind { + SelfKind::None => {} + _ => 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); + match info.self_kind { + SelfKind::None => quote! { |lua, #destructure| }, + _ => quote! { |lua, this, #destructure| }, + } +} + +fn gen_field_getter( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let lua_name = lua_attr.name(&fn_name); + let call_args = gen_call_args(info); + + if lua_attr.infallible { + return quote! { + registry.add_field_method_get(#lua_name, |lua, this| { + let _ = lua; // silence unused variable warning + Ok(#type_path::#fn_name(#call_args)) + }); + }; + } + + quote! { + registry.add_field_method_get(#lua_name, |lua, this| { + let _ = lua; // silence unused variable warning + #type_path::#fn_name(#call_args) + }); + } +} + +fn gen_field_setter( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let lua_name = lua_attr.name(fn_name); + let call_args = gen_call_args(info); + + if lua_attr.infallible { + let val_ident = info.args.first().map(|a| &a.ident); + return quote! { + registry.add_field_method_set(#lua_name, |lua, this, #val_ident| { + let _ = lua; // silence unused variable warning + Ok(#type_path::#fn_name(#call_args)) + }); + }; + } + + let val_ident = info.args.first().map(|a| &a.ident); + quote! { + registry.add_field_method_set(#lua_name, |lua, this, #val_ident| { + let _ = lua; // silence unused variable warning + #type_path::#fn_name(#call_args) + }); + } +} + +fn gen_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) { + // Lua always passes `self` to the stack arg, just ignore it. + 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_closure_params(info) + }; + let call_args = gen_call_args(info); + let fn_path = quote! { #type_path::#fn_name }; + + let body = if lua_attr.infallible { + quote! { Ok(#fn_path(#call_args)) } + } else { + quote! { #fn_path(#call_args) } + }; + match info.self_kind { + SelfKind::None => quote! { + registry.add_meta_function(#meta_name, #closure_params { #body }); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_meta_method_mut(#meta_name, #closure_params { #body }); + }, + _ => quote! { + registry.add_meta_method(#meta_name, #closure_params { #body }); + }, + } +} + +fn gen_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_closure_params(info); + let call_args = gen_call_args(info); + let lua_name = lua_attr.name(fn_name); + + let body = if lua_attr.infallible { + quote! { Ok(#fn_path(#call_args)) } + } else { + quote! { #fn_path(#call_args) } + }; + match info.self_kind { + SelfKind::Ref(RefKind::Ref) => quote! { + registry.add_method(#lua_name, #closure_params { #body }); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_method_mut(#lua_name, #closure_params { #body }); + }, + SelfKind::Owned => quote! { + registry.add_method_once(#lua_name, #closure_params { #body }); + }, + SelfKind::None => quote! { + registry.add_function(#lua_name, #closure_params { #body }); + }, + } +} diff --git a/src/lib.rs b/src/lib.rs index 1a00dba3..bf06d472 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,9 @@ pub mod userdata; pub use bstr::BString; pub use ffi::{self, lua_CFunction, lua_State}; +#[cfg(feature = "macros")] +#[doc(hidden)] +pub use inventory as __inventory; #[doc(inline)] pub use crate::error::{Error, Result}; @@ -230,6 +233,26 @@ pub use mlua_derive::chunk; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::FromLua; +/// Attribute macro for exposing a struct as Lua userdata. +/// +/// All fields are auto-exposed as get/set. +/// +/// Use `#[lua(...)]` to customize. +/// +/// This generates a [`UserData`] trait implementation. +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use mlua_derive::userdata; + +/// Attribute macro for exposing impl block methods to Lua userdata. +/// +/// All methods and constants are auto-exposed. +/// +/// Use `#[lua(...)]` to customize. +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use mlua_derive::userdata_impl; + /// Registers Lua module entrypoint. /// /// You can register multiple entrypoints as required. diff --git a/tests/compile.rs b/tests/compile.rs index c8ee4511..4fe315bd 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -21,4 +21,18 @@ fn test_compilation() { t.compile_fail("tests/compile/non_send.rs"); #[cfg(not(feature = "send"))] t.pass("tests/compile/non_send.rs"); + + #[cfg(feature = "macros")] + { + t.compile_fail("tests/compile/userdata_getter_and_meta.rs"); + t.compile_fail("tests/compile/userdata_getter_and_setter.rs"); + t.compile_fail("tests/compile/userdata_getter_mut_self.rs"); + t.compile_fail("tests/compile/userdata_getter_extra_arg.rs"); + t.compile_fail("tests/compile/userdata_setter_ref_self.rs"); + t.compile_fail("tests/compile/userdata_mut_slice_arg.rs"); + t.compile_fail("tests/compile/userdata_setter_no_value.rs"); + t.compile_fail("tests/compile/userdata_static_with_self.rs"); + t.compile_fail("tests/compile/userdata_meta_owned_self.rs"); + t.compile_fail("tests/compile/userdata_const_getter.rs"); + } } diff --git a/tests/compile/lua_norefunwindsafe.stderr b/tests/compile/lua_norefunwindsafe.stderr index 4094cd2b..2b3e6648 100644 --- a/tests/compile/lua_norefunwindsafe.stderr +++ b/tests/compile/lua_norefunwindsafe.stderr @@ -1,32 +1,28 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs - | - | pub struct ReentrantMutex { - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` - --> $RUST/alloc/src/sync.rs + = help: within `mlua::types::sync::inner::ReentrantMutex`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<*mut lua_State>` +note: required because it appears within the type `Cell<*mut lua_State>` + --> $RUST/core/src/cell.rs | - | struct ArcInner { - | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` - --> $RUST/core/src/marker.rs + | pub struct Cell { + | ^^^^ +note: required because it appears within the type `mlua::state::RawLua` + --> src/state/raw.rs | - | pub struct PhantomData; - | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` - --> $RUST/alloc/src/sync.rs + | pub struct RawLua { + | ^^^^^^ +note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` + --> src/types/sync.rs | - | pub struct Arc< - | ^^^ + | pub(crate) struct ReentrantMutex(T); + | ^^^^^^^^^^^^^^ + = note: required for `Rc>` to implement `RefUnwindSafe` note: required because it appears within the type `Lua` --> src/state.rs | @@ -44,45 +40,27 @@ note: required by a bound in `std::panic::catch_unwind` | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { | ^^^^^^^^^^ required by this bound in `catch_unwind` -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `Cell` - --> $RUST/core/src/cell.rs + = help: the trait `RefUnwindSafe` is not implemented for `UnsafeCell` + = note: required for `Rc>` to implement `RefUnwindSafe` +note: required because it appears within the type `mlua::state::RawLua` + --> src/state/raw.rs | - | pub struct Cell { - | ^^^^ -note: required because it appears within the type `lock_api::remutex::RawReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs - | - | pub struct RawReentrantMutex { - | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs - | - | pub struct ReentrantMutex { - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` - --> $RUST/alloc/src/sync.rs + | pub struct RawLua { + | ^^^^^^ +note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` + --> src/types/sync.rs | - | struct ArcInner { - | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` - --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` - --> $RUST/alloc/src/sync.rs - | - | pub struct Arc< - | ^^^ + | pub(crate) struct ReentrantMutex(T); + | ^^^^^^^^^^^^^^ + = note: required for `Rc>` to implement `RefUnwindSafe` note: required because it appears within the type `Lua` --> src/state.rs | diff --git a/tests/compile/ref_nounwindsafe.stderr b/tests/compile/ref_nounwindsafe.stderr index 757083df..07613de8 100644 --- a/tests/compile/ref_nounwindsafe.stderr +++ b/tests/compile/ref_nounwindsafe.stderr @@ -1,25 +1,25 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs + = help: within `rc::RcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `Cell` + --> $RUST/core/src/cell.rs | - | pub struct ReentrantMutex { - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` - --> $RUST/alloc/src/sync.rs + | pub struct Cell { + | ^^^^ +note: required because it appears within the type `rc::RcInner>` + --> $RUST/alloc/src/rc.rs | - | struct ArcInner { - | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` - --> $RUST/alloc/src/sync.rs + | struct RcInner { + | ^^^^^^^ + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::rc::Weak>` + --> $RUST/alloc/src/rc.rs | | pub struct Weak< | ^^^^ @@ -49,38 +49,95 @@ note: required by a bound in `std::panic::catch_unwind` | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { | ^^^^^^^^^^ required by this bound in `catch_unwind` -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `Cell` + = help: within `rc::RcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<*mut lua_State>` +note: required because it appears within the type `Cell<*mut lua_State>` --> $RUST/core/src/cell.rs | | pub struct Cell { | ^^^^ -note: required because it appears within the type `lock_api::remutex::RawReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs - | - | pub struct RawReentrantMutex { - | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` - --> $CARGO/lock_api-$VERSION/src/remutex.rs - | - | pub struct ReentrantMutex { - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` - --> $RUST/alloc/src/sync.rs - | - | struct ArcInner { - | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` - --> $RUST/alloc/src/sync.rs +note: required because it appears within the type `mlua::state::RawLua` + --> src/state/raw.rs + | + | pub struct RawLua { + | ^^^^^^ +note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` + --> src/types/sync.rs + | + | pub(crate) struct ReentrantMutex(T); + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `rc::RcInner>` + --> $RUST/alloc/src/rc.rs + | + | struct RcInner { + | ^^^^^^^ + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::rc::Weak>` + --> $RUST/alloc/src/rc.rs + | + | pub struct Weak< + | ^^^^ +note: required because it appears within the type `WeakLua` + --> src/state.rs + | + | pub struct WeakLua(XWeak>); + | ^^^^^^^ +note: required because it appears within the type `mlua::types::value_ref::ValueRef` + --> src/types/value_ref.rs + | + | pub struct ValueRef { + | ^^^^^^^^ +note: required because it appears within the type `LuaTable` + --> src/table.rs + | + | pub struct Table(pub(crate) ValueRef); + | ^^^^^ +note: required because it's used within this closure + --> tests/compile/ref_nounwindsafe.rs:8:18 + | +8 | catch_unwind(move || table.set("a", "b").unwrap()); + | ^^^^^^^ +note: required by a bound in `std::panic::catch_unwind` + --> $RUST/std/src/panic.rs + | + | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { + | ^^^^^^^^^^ required by this bound in `catch_unwind` + +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + --> tests/compile/ref_nounwindsafe.rs:8:18 + | +8 | catch_unwind(move || table.set("a", "b").unwrap()); + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | | + | required by a bound introduced by this call + | + = help: the trait `RefUnwindSafe` is not implemented for `UnsafeCell` + = note: required for `Rc>` to implement `RefUnwindSafe` +note: required because it appears within the type `mlua::state::RawLua` + --> src/state/raw.rs + | + | pub struct RawLua { + | ^^^^^^ +note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` + --> src/types/sync.rs + | + | pub(crate) struct ReentrantMutex(T); + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `rc::RcInner>` + --> $RUST/alloc/src/rc.rs + | + | struct RcInner { + | ^^^^^^^ + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::rc::Weak>` + --> $RUST/alloc/src/rc.rs | | pub struct Weak< | ^^^^ diff --git a/tests/compile/userdata_const_getter.rs b/tests/compile/userdata_const_getter.rs new file mode 100644 index 00000000..6394ed87 --- /dev/null +++ b/tests/compile/userdata_const_getter.rs @@ -0,0 +1,11 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + const X: u32 = 42; +} + +fn main() {} diff --git a/tests/compile/userdata_const_getter.stderr b/tests/compile/userdata_const_getter.stderr new file mode 100644 index 00000000..6b7ef6ad --- /dev/null +++ b/tests/compile/userdata_const_getter.stderr @@ -0,0 +1,5 @@ +error: const items do not support `getter` or `setter` + --> tests/compile/userdata_const_getter.rs:8:11 + | +8 | const X: u32 = 42; + | ^ diff --git a/tests/compile/userdata_getter_and_meta.rs b/tests/compile/userdata_getter_and_meta.rs new file mode 100644 index 00000000..1c78f30f --- /dev/null +++ b/tests/compile/userdata_getter_and_meta.rs @@ -0,0 +1,13 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter, meta)] + fn bar(&self) -> mlua::Result { + Ok(42) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_and_meta.stderr b/tests/compile/userdata_getter_and_meta.stderr new file mode 100644 index 00000000..9b7e2192 --- /dev/null +++ b/tests/compile/userdata_getter_and_meta.stderr @@ -0,0 +1,5 @@ +error: `meta` can only be combined with `field` + --> tests/compile/userdata_getter_and_meta.rs:8:5 + | +8 | fn bar(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_setter.rs b/tests/compile/userdata_getter_and_setter.rs new file mode 100644 index 00000000..d6f33da9 --- /dev/null +++ b/tests/compile/userdata_getter_and_setter.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter, setter)] + fn x(&self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_and_setter.stderr b/tests/compile/userdata_getter_and_setter.stderr new file mode 100644 index 00000000..910ebc55 --- /dev/null +++ b/tests/compile/userdata_getter_and_setter.stderr @@ -0,0 +1,5 @@ +error: at most one of `getter`, `setter`, `field` can be specified + --> tests/compile/userdata_getter_and_setter.rs:10:5 + | +10 | fn x(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_extra_arg.rs b/tests/compile/userdata_getter_extra_arg.rs new file mode 100644 index 00000000..5e61ebc0 --- /dev/null +++ b/tests/compile/userdata_getter_extra_arg.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + fn x(&self, extra: u32) -> mlua::Result { + Ok(self.x + extra) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_extra_arg.stderr b/tests/compile/userdata_getter_extra_arg.stderr new file mode 100644 index 00000000..34adb6ea --- /dev/null +++ b/tests/compile/userdata_getter_extra_arg.stderr @@ -0,0 +1,5 @@ +error: field getter must not take additional arguments + --> tests/compile/userdata_getter_extra_arg.rs:10:5 + | +10 | fn x(&self, extra: u32) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_mut_self.rs b/tests/compile/userdata_getter_mut_self.rs new file mode 100644 index 00000000..b48c2e69 --- /dev/null +++ b/tests/compile/userdata_getter_mut_self.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + fn x(&mut self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_mut_self.stderr b/tests/compile/userdata_getter_mut_self.stderr new file mode 100644 index 00000000..79aa08f5 --- /dev/null +++ b/tests/compile/userdata_getter_mut_self.stderr @@ -0,0 +1,5 @@ +error: field getter must take `&self` + --> tests/compile/userdata_getter_mut_self.rs:10:5 + | +10 | fn x(&mut self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_meta_owned_self.rs b/tests/compile/userdata_meta_owned_self.rs new file mode 100644 index 00000000..ef85b801 --- /dev/null +++ b/tests/compile/userdata_meta_owned_self.rs @@ -0,0 +1,13 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(meta)] + fn __gc(self) -> mlua::Result<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_meta_owned_self.stderr b/tests/compile/userdata_meta_owned_self.stderr new file mode 100644 index 00000000..5cb10a18 --- /dev/null +++ b/tests/compile/userdata_meta_owned_self.stderr @@ -0,0 +1,5 @@ +error: meta methods cannot take `self`, use `&[mut] self` instead + --> tests/compile/userdata_meta_owned_self.rs:8:5 + | +8 | fn __gc(self) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_mut_slice_arg.rs b/tests/compile/userdata_mut_slice_arg.rs new file mode 100644 index 00000000..1ecc5055 --- /dev/null +++ b/tests/compile/userdata_mut_slice_arg.rs @@ -0,0 +1,12 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo(Vec); + +#[mlua::userdata_impl] +impl Foo { + fn first(&self, data: &mut [u8]) -> mlua::Result { + Ok(data[0]) + } +} + +fn main() {} diff --git a/tests/compile/userdata_mut_slice_arg.stderr b/tests/compile/userdata_mut_slice_arg.stderr new file mode 100644 index 00000000..43780fc9 --- /dev/null +++ b/tests/compile/userdata_mut_slice_arg.stderr @@ -0,0 +1,5 @@ +error: this reference type is not supported as a callback parameter + --> tests/compile/userdata_mut_slice_arg.rs:7:27 + | +7 | fn first(&self, data: &mut [u8]) -> mlua::Result { + | ^^^^^^^^^ diff --git a/tests/compile/userdata_setter_no_value.rs b/tests/compile/userdata_setter_no_value.rs new file mode 100644 index 00000000..8334dc78 --- /dev/null +++ b/tests/compile/userdata_setter_no_value.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + fn set_x(&mut self) -> mlua::Result<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_no_value.stderr b/tests/compile/userdata_setter_no_value.stderr new file mode 100644 index 00000000..b6ce7dc0 --- /dev/null +++ b/tests/compile/userdata_setter_no_value.stderr @@ -0,0 +1,5 @@ +error: field setter must take exactly one value argument + --> tests/compile/userdata_setter_no_value.rs:10:5 + | +10 | fn set_x(&mut self) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_setter_ref_self.rs b/tests/compile/userdata_setter_ref_self.rs new file mode 100644 index 00000000..afc7572a --- /dev/null +++ b/tests/compile/userdata_setter_ref_self.rs @@ -0,0 +1,16 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + fn set_x(self, val: u32) -> mlua::Result<()> { + let _ = val; + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_ref_self.stderr b/tests/compile/userdata_setter_ref_self.stderr new file mode 100644 index 00000000..49d85cf9 --- /dev/null +++ b/tests/compile/userdata_setter_ref_self.stderr @@ -0,0 +1,5 @@ +error: field setter must take `&[mut] self` + --> tests/compile/userdata_setter_ref_self.rs:10:5 + | +10 | fn set_x(self, val: u32) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_static_with_self.rs b/tests/compile/userdata_static_with_self.rs new file mode 100644 index 00000000..c97fa4de --- /dev/null +++ b/tests/compile/userdata_static_with_self.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + fn get_x(&self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_static_with_self.stderr b/tests/compile/userdata_static_with_self.stderr new file mode 100644 index 00000000..adc552e8 --- /dev/null +++ b/tests/compile/userdata_static_with_self.stderr @@ -0,0 +1,5 @@ +error: field function must not take `self` + --> tests/compile/userdata_static_with_self.rs:10:5 + | +10 | fn get_x(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs new file mode 100644 index 00000000..843da7fe --- /dev/null +++ b/tests/userdata_macro.rs @@ -0,0 +1,374 @@ +#![cfg(feature = "macros")] + +use mlua::{Lua, Result}; + +#[derive(Default, Clone, Debug)] +#[mlua::userdata] +struct Rectangle { + length: u32, + #[lua] + width: u32, + + #[lua(get, name = "version")] + version_ro: u32, + + #[lua(skip)] + _internal: u64, +} + +#[mlua::userdata_impl] +impl Rectangle { + const TYPE_NAME: &str = "Rectangle"; + + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Rectangle { + length, + width, + version_ro: 1, + _internal: 0, + } + } + + fn area(&self) -> Result { + Ok(self.length * self.width) + } + + #[lua(getter, infallible, name = "perimeter")] + fn calculate_perimeter(&self) -> u32 { + 2 * (self.length + self.width) + } + + #[lua(infallible)] + fn diagonal(&self) -> f64 { + (self.length.pow(2) as f64 + self.width.pow(2) as f64).sqrt() + } + + fn scale(&mut self, factor: u32) -> Result<()> { + self.length *= factor; + self.width *= factor; + Ok(()) + } + + #[lua(setter, name = "size", infallible)] + fn set_size(&mut self, _lua: &Lua, size: u32) { + self.length = size; + self.width = size; + } + + #[lua(field)] + fn description() -> &'static str { + "A rectangle shape" + } + + #[lua(meta)] + fn __tostring(&self) -> Result { + Ok(format!("Rectangle({}x{})", self.length, self.width)) + } + + #[lua(meta, name = "__call")] + fn call() -> Result { + Ok(Rectangle::default()) + } + + #[lua(meta, infallible, name = "__add")] + fn add(&self, other: &Rectangle) -> Rectangle { + Rectangle { + length: self.length + other.length, + width: self.width + other.width, + ..Default::default() + } + } + + #[lua(skip)] + #[allow(unused)] + fn helper() -> u32 { + 42 + } + + fn default_size() -> Result<(u32, u32)> { + Ok((100, 100)) + } + + fn get_width(&self, lua: &Lua) -> Result { + let _ = lua.globals().len(); + Ok(self.width) + } + + #[lua(getter, name = "lua_version")] + fn get_lua_version(&self, lua: &::mlua::Lua) -> Result { + // `::mlua::Lua` is used to check that the type is correctly resolved in macros + lua.globals().get("_VERSION") + } + + fn into_tuple(self) -> Result<(u32, u32)> { + Ok((self.length, self.width)) + } + + fn greet(&self, name: &str) -> Result { + Ok(format!("Hello, {name}!")) + } + + fn transfer_length(&mut self, other: &mut Rectangle) -> Result<()> { + other.length += self.length; + self.length = 0; + Ok(()) + } +} + +#[mlua::userdata_impl] +impl Rectangle { + #[lua(infallible)] + fn double_length(&self) -> u32 { + self.length * 2 + } +} + +fn make_lua() -> Lua { + let lua = Lua::new(); + lua.globals() + .set("Rectangle", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[test] +fn test_rectangle() { + let lua = make_lua(); + + // Basic fields, getters, setters, methods + lua.load( + r#" + rect = Rectangle.new(5, 10, 3) + assert(rect.length == 5, "length should be 5") + assert(rect.width == 10, "width should be 10") + assert(rect.perimeter == 30, "perimeter should be 30") + + -- read-only field + assert(rect.version == 1, "version should be 1") + local ok, err = pcall(function() rect.version = 2 end) + assert(not ok, "version should be read-only") + + -- skipped + assert(rect._internal == nil, "_internal should be nil") + assert(rect.helper == nil, "skipped method should be nil") + + rect.length = 15 + rect.width = 20 + assert(rect.length == 15, "length should be updated to 15") + assert(rect.width == 20, "width should be updated to 20") + assert(rect.perimeter == 70, "perimeter should be updated to 70") + assert(rect:area() == 300, "area should return 300") + + rect:scale(2) + assert(rect.length == 30, "length should be scaled to 30") + assert(rect.width == 40, "width should be scaled to 40") + assert(rect:diagonal() == 50.0, "diagonal should be 50.0") + + rect.size = 7 + assert(rect.length == 7, "length should be updated to 7") + assert(rect.width == 7, "width should be updated to 7") + + -- static / associated items + assert(rect.TYPE_NAME == 'Rectangle', "TYPE_NAME should be 'Rectangle'") + assert(rect.description == 'A rectangle shape', "description should be 'A rectangle shape'") + local w, h = rect.default_size() + assert(w == 100, "default_size width should be 100") + assert(h == 100, "default_size height should be 100") + + -- meta methods + local r1 = Rectangle.new(5, 10, 0) + assert(tostring(r1) == 'Rectangle(5x10)', "__tostring should return 'Rectangle(5x10)'") + local r2 = r1() + assert(r2:area() == 0, "__call should create a default rect") + local r3 = Rectangle.new(3, 4, 0) + local r4 = r1 + r3 + assert(r4.length == 8, "__add length should be 5 + 3 = 8") + assert(r4.width == 14, "__add width should be 10 + 4 = 14") + + -- method with &mut self and &mut Rectangle param + rect = Rectangle.new(5, 10, 3) + other = Rectangle.new(2, 3, 0) + rect:transfer_length(other) + assert(rect.length == 0, "length should be 0 after transfer") + assert(other.length == 7, "other length should be 7 after transfer") + assert(other:double_length() == 14, "double_length should be 14") + + assert(rect.lua_version == _VERSION, "lua_version should match Lua's _VERSION") + + -- Consuming method + local w, h = rect:into_tuple() + assert(w == 0, "into_tuple width should be 0") + assert(h == 10, "into_tuple height should be 7") + local ok, err = pcall(function() rect:area() end) + assert(not ok and tostring(err):match("userdata has been destructed"), "rect should be consumed and unusable after into_tuple") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug)] +#[mlua::userdata] +enum Color { + Red, + Green, + Blue, +} + +fn make_lua_color() -> Lua { + let lua = Lua::new(); + lua.globals() + .set("Color", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[mlua::userdata_impl] +impl Color { + #[lua(infallible)] + fn new(r: u8, g: u8, b: u8) -> Self { + if r > 0 && g == 0 && b == 0 { + Color::Red + } else if g > 0 && r == 0 && b == 0 { + Color::Green + } else { + Color::Blue + } + } + + #[lua(infallible)] + fn name(&self) -> String { + match self { + Color::Red => "red".into(), + Color::Green => "green".into(), + Color::Blue => "blue".into(), + } + } + + #[lua(meta, infallible)] + fn __tostring(&self) -> String { + self.name() + } +} + +#[test] +fn test_color() { + let lua = make_lua_color(); + lua.load( + r#" + red = Color.new(255, 0, 0) + green = Color.new(0, 255, 0) + blue = Color.new(0, 0, 255) + + assert(red:name() == 'red', "red name should be 'red'") + assert(green:name() == 'green', "green name should be 'green'") + assert(blue:name() == 'blue', "blue name should be 'blue'") + + assert(tostring(red) == 'red', "red tostring should be 'red'") + assert(tostring(green) == 'green', "green tostring should be 'green'") + assert(tostring(blue) == 'blue', "blue tostring should be 'blue'") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug)] +#[mlua::userdata] +struct Point(i32, i32); + +fn make_lua_point() -> Lua { + let lua = Lua::new(); + lua.globals() + .set("Point", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[mlua::userdata_impl] +impl Point { + #[lua(infallible)] + fn new(x: i32, y: i32) -> Self { + Point(x, y) + } + + fn x(&self) -> Result { + Ok(self.0) + } + + fn y(&self) -> Result { + Ok(self.1) + } + + fn distance(&self, other: &Point) -> Result { + let dx = (self.0 - other.0) as f64; + let dy = (self.1 - other.1) as f64; + Ok((dx * dx + dy * dy).sqrt()) + } +} + +#[test] +fn test_point() { + let lua = make_lua_point(); + lua.load( + r#" + p1 = Point.new(0, 0) + p2 = Point.new(3, 4) + + assert(p1:x() == 0, "p1.x should be 0") + assert(p1:y() == 0, "p1.y should be 0") + assert(p2:x() == 3, "p2.x should be 3") + assert(p2:y() == 4, "p2.y should be 4") + + assert(p1:distance(p2) == 5.0, "distance should be 5.0") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug)] +#[mlua::userdata] +struct Bytes(Vec); + +#[mlua::userdata_impl] +impl Bytes { + #[lua(meta, name = "__type")] + const TYPE: &str = "MyBytes"; + + #[lua(infallible)] + fn new(data: &[u8]) -> Self { + Bytes(data.to_vec()) + } + + fn first(&self) -> Result> { + Ok(self.0.first().copied()) + } + + fn len(&self) -> Result { + Ok(self.0.len()) + } +} + +#[test] +fn test_known_borrow_wrappers() -> Result<()> { + let lua = Lua::new(); + lua.globals() + .set("Bytes", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local b = Bytes.new('abc') + assert(b:first() == 97, "first should return 97 ('a')") + assert(b:len() == 3, "len should return 3") + + if _VERSION:match("Luau") then + assert(typeof(b) == 'MyBytes', "type should be MyBytes in Luau") + end + "#, + ) + .exec() + .unwrap(); + Ok(()) +} From 72de602ec387059cf4ac04ee92dcb2986138f760 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 00:30:25 +0100 Subject: [PATCH 062/138] Fix modules compilation after switching to 2024 edition in mlua_derive --- mlua_derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index 74bcf295..b9911174 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -72,7 +72,7 @@ pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { #func - #[no_mangle] + #[unsafe(no_mangle)] unsafe extern "C-unwind" fn #ext_entrypoint_name(state: *mut mlua::lua_State) -> ::std::os::raw::c_int { mlua::Lua::entrypoint1(state, move |lua| { #skip_memory_check From cc7f7ce7b7373b575182b2815122779ff72bac36 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 00:42:23 +0100 Subject: [PATCH 063/138] mlua_derive: show better error message when $ is not followed by ident --- mlua_derive/src/token.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mlua_derive/src/token.rs b/mlua_derive/src/token.rs index c6ce7c97..880c47a2 100644 --- a/mlua_derive/src/token.rs +++ b/mlua_derive/src/token.rs @@ -170,12 +170,16 @@ impl Tokens { .flat_map(Tokens::from) .peekable() .batching(|iter| { - // Find variable tokens + // Find variable tokens: `$` + `ident` => `$ident` let t = iter.next()?; if t.is("$") { - // `$` + `ident` => `$ident` - let t = iter.next().expect("$ must trail an identifier"); - Some(t.attr(TokenAttr::Cap)) + if let Some(next) = iter.next() + && matches!(next.tree, TokenTree::Ident(_)) + { + Some(next.attr(TokenAttr::Cap)) + } else { + proc_macro_error2::abort!(t.tree.span(), "`$` must be followed by an identifier"); + } } else { Some(t) } From f4cacc524e1e2b5f8d5d5cb467dc979cd2a530fd Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 00:53:21 +0100 Subject: [PATCH 064/138] mlua_derive: Bump MSRV to 1.88 --- mlua_derive/Cargo.toml | 1 + mlua_derive/src/userdata_impl.rs | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 7454f63c..6a1b12c9 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -2,6 +2,7 @@ name = "mlua_derive" version = "0.11.0" authors = ["Aleksandr Orlenko "] +rust-version = "1.88" edition = "2024" description = "Procedural macros for the mlua crate." repository = "https://github.com/mlua-rs/mlua" diff --git a/mlua_derive/src/userdata_impl.rs b/mlua_derive/src/userdata_impl.rs index 10662cb7..876dd488 100644 --- a/mlua_derive/src/userdata_impl.rs +++ b/mlua_derive/src/userdata_impl.rs @@ -71,11 +71,8 @@ fn classify_ref_type(ty: &Type) -> Option { if ref_ty.mutability.is_none() { let lookup_name: Option = match &*ref_ty.elem { Type::Path(path) => path.path.segments.last().map(|seg| seg.ident.to_string()), - Type::Slice(slice) - if let Type::Path(path) = &*slice.elem - && let Some(seg) = path.path.segments.last() => - { - Some(format!("[{}]", seg.ident)) + Type::Slice(slice) if let Type::Path(path) = &*slice.elem => { + path.path.segments.last().map(|seg| format!("[{}]", seg.ident)) } _ => None, }; From 7114c0348976528a2a42ca6fe8448ac9a6c75698 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 01:15:35 +0100 Subject: [PATCH 065/138] mlua_derive: Remove unneeded `Span::line` hack for chunk! since we use Rust 1.95+ --- mlua_derive/Cargo.toml | 4 +-- mlua_derive/src/token.rs | 40 +++------------------ tests/compile.rs | 1 + tests/compile/chunk_dollar_non_ident.rs | 4 +++ tests/compile/chunk_dollar_non_ident.stderr | 5 +++ 5 files changed, 15 insertions(+), 39 deletions(-) create mode 100644 tests/compile/chunk_dollar_non_ident.rs create mode 100644 tests/compile/chunk_dollar_non_ident.stderr diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 6a1b12c9..7ba306a9 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT" proc-macro = true [features] -macros = ["proc-macro-error2", "itertools", "regex", "once_cell"] +macros = ["proc-macro-error2", "itertools"] [dependencies] quote = "1.0" @@ -21,5 +21,3 @@ proc-macro2 = { version = "1.0", features = ["span-locations"] } proc-macro-error2 = { version = "2.0.1", optional = true } syn = { version = "2.0", features = ["full"] } itertools = { version = "0.14", optional = true } -regex = { version = "1.4", optional = true } -once_cell = { version = "1.0", optional = true } diff --git a/mlua_derive/src/token.rs b/mlua_derive/src/token.rs index 880c47a2..afc89f88 100644 --- a/mlua_derive/src/token.rs +++ b/mlua_derive/src/token.rs @@ -3,10 +3,8 @@ use std::fmt::{self, Display, Formatter}; use std::vec::IntoIter; use itertools::Itertools; -use once_cell::sync::Lazy; use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; use proc_macro2::Span as Span2; -use regex::Regex; #[derive(Clone, Copy, Debug)] pub(crate) struct Pos { @@ -39,46 +37,16 @@ fn span_pos(span: &Span) -> (Pos, Pos) { let start = span2.start(); let end = span2.end(); - // In stable, line/column information is not provided - // and set to 0 (line is 1-indexed) + // Rust 1.88 stabilized Span APIs, so this branch must be unreachable if start.line == 0 || end.line == 0 { - return fallback_span_pos(span); + proc_macro_error2::abort_call_site!( + "cannot retrieve span location information; mlua requires nightly Rust or stable >= 1.88" + ); } (Pos::new(start.line, start.column), Pos::new(end.line, end.column)) } -fn parse_pos(span: &Span) -> Option<(usize, usize)> { - // Workaround to somehow retrieve location information in span in stable rust :( - - static RE: Lazy = Lazy::new(|| Regex::new(r"bytes\(([0-9]+)\.\.([0-9]+)\)").unwrap()); - - match RE.captures(&format!("{span:?}")) { - Some(caps) => match (caps.get(1), caps.get(2)) { - (Some(start), Some(end)) => Some(( - match start.as_str().parse() { - Ok(v) => v, - _ => return None, - }, - match end.as_str().parse() { - Ok(v) => v, - _ => return None, - }, - )), - _ => None, - }, - None => None, - } -} - -fn fallback_span_pos(span: &Span) -> (Pos, Pos) { - let (start, end) = match parse_pos(span) { - Some(v) => v, - None => proc_macro_error2::abort_call_site!("Cannot retrieve span information; please use nightly"), - }; - (Pos::new(1, start), Pos::new(1, end)) -} - /// Attribute of token. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum TokenAttr { diff --git a/tests/compile.rs b/tests/compile.rs index 4fe315bd..50f70f1a 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -24,6 +24,7 @@ fn test_compilation() { #[cfg(feature = "macros")] { + t.compile_fail("tests/compile/chunk_dollar_non_ident.rs"); t.compile_fail("tests/compile/userdata_getter_and_meta.rs"); t.compile_fail("tests/compile/userdata_getter_and_setter.rs"); t.compile_fail("tests/compile/userdata_getter_mut_self.rs"); diff --git a/tests/compile/chunk_dollar_non_ident.rs b/tests/compile/chunk_dollar_non_ident.rs new file mode 100644 index 00000000..47350132 --- /dev/null +++ b/tests/compile/chunk_dollar_non_ident.rs @@ -0,0 +1,4 @@ +use mlua::chunk; +fn main() { + let _ = chunk! { $42 }; +} diff --git a/tests/compile/chunk_dollar_non_ident.stderr b/tests/compile/chunk_dollar_non_ident.stderr new file mode 100644 index 00000000..480bc0f9 --- /dev/null +++ b/tests/compile/chunk_dollar_non_ident.stderr @@ -0,0 +1,5 @@ +error: `$` must be followed by an identifier + --> tests/compile/chunk_dollar_non_ident.rs:3:22 + | +3 | let _ = chunk! { $42 }; + | ^ From d8544bf038961f8cb1717d5f6cdefd59f69de794 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 22:36:13 +0100 Subject: [PATCH 066/138] mlua_derive: Optimize `Captures::add` --- mlua_derive/src/chunk.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/mlua_derive/src/chunk.rs b/mlua_derive/src/chunk.rs index ee9b8221..023e4386 100644 --- a/mlua_derive/src/chunk.rs +++ b/mlua_derive/src/chunk.rs @@ -32,18 +32,12 @@ impl Captures { Self(Vec::new()) } - pub(crate) fn add(&mut self, token: &Token) -> Capture { - let tt = token.tree(); - let key = token.clone(); - - match self.0.iter().find(|arg| arg.key() == &key) { - Some(arg) => arg.clone(), - None => { - let arg = Capture::new(key, tt.clone()); - self.0.push(arg.clone()); - arg - } + pub(crate) fn add(&mut self, token: &Token) { + if self.0.iter().any(|arg| arg.key() == token) { + return; } + let arg = Capture::new(token.clone(), token.tree().clone()); + self.0.push(arg); } pub(crate) fn captures(&self) -> &[Capture] { From 92bd06d3c1e76e7c7a060e502042e164b90fd348 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 16 May 2026 23:40:23 +0100 Subject: [PATCH 067/138] mlua_derive: Refactor `Capture` implementation for `chunk!` --- mlua_derive/src/chunk.rs | 62 ++++++++++++++++++++++------------------ mlua_derive/src/lib.rs | 14 ++------- mlua_derive/src/token.rs | 4 +-- tests/chunk.rs | 15 ++++++++++ 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/mlua_derive/src/chunk.rs b/mlua_derive/src/chunk.rs index 023e4386..1b5ea0cd 100644 --- a/mlua_derive/src/chunk.rs +++ b/mlua_derive/src/chunk.rs @@ -1,26 +1,36 @@ -use proc_macro::{TokenStream, TokenTree}; +use std::ops::Deref; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::ToTokens; use crate::token::{Pos, Token, Tokens}; #[derive(Debug, Clone)] -pub(crate) struct Capture { - key: Token, - rust: TokenTree, +pub(crate) struct Capture(Token); + +impl Deref for Capture { + type Target = Token; + + fn deref(&self) -> &Self::Target { + &self.0 + } } impl Capture { - fn new(key: Token, rust: TokenTree) -> Self { - Self { key, rust } + fn new(token: &Token) -> Self { + Self(token.clone()) } - /// Token string inside `chunk!` - pub(crate) fn key(&self) -> &Token { - &self.key + pub(crate) fn name(&self) -> String { + self.0.to_string() } +} - /// As rust variable, e.g. `x` - pub(crate) fn as_rust(&self) -> &TokenTree { - &self.rust +impl ToTokens for Capture { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ts: TokenStream = self.0.tree().clone().into(); + tokens.extend(TokenStream2::from(ts)); } } @@ -33,11 +43,10 @@ impl Captures { } pub(crate) fn add(&mut self, token: &Token) { - if self.0.iter().any(|arg| arg.key() == token) { + if self.0.iter().any(|arg| &**arg == token) { return; } - let arg = Capture::new(token.clone(), token.tree().clone()); - self.0.push(arg); + self.0.push(Capture::new(token)); } pub(crate) fn captures(&self) -> &[Capture] { @@ -58,29 +67,26 @@ impl Chunk { let mut source = String::new(); let mut caps = Captures::new(); - let mut pos: Option = None; + let mut prev_end: Option = None; for t in tokens { if t.is_cap() { caps.add(&t); } let (line, col) = (t.start().line, t.start().column); - let (prev_line, prev_col) = pos - .take() - .map(|lc| (lc.line, lc.column)) - .unwrap_or_else(|| (line, col)); - - #[allow(clippy::comparison_chain)] - if line > prev_line { - source.push('\n'); - } else if line == prev_line { - for _ in 0..col.saturating_sub(prev_col) { - source.push(' '); + if let Some(prev) = prev_end { + if line > prev.line { + source.push('\n'); + source.push_str(&" ".repeat(col.saturating_sub(1))); + } else if line == prev.line { + source.push_str(&" ".repeat(col.saturating_sub(prev.column))); } + } else { + source.push_str(&" ".repeat(col.saturating_sub(1))); } source.push_str(&t.to_string()); - pos = Some(t.end()); + prev_end = Some(t.end()); } Self { diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index b9911174..e943d9ff 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -5,10 +5,7 @@ use syn::meta::ParseNestedMeta; use syn::{ItemFn, LitStr, Result, parse_macro_input}; #[cfg(feature = "macros")] -use { - crate::chunk::Chunk, proc_macro::TokenTree, proc_macro_error2::proc_macro_error, - proc_macro2::TokenStream as TokenStream2, -}; +use {crate::chunk::Chunk, proc_macro_error2::proc_macro_error}; #[cfg(feature = "macros")] macro_rules! try_compile { @@ -84,12 +81,6 @@ pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { wrapped.into() } -#[cfg(feature = "macros")] -fn to_ident(tt: &TokenTree) -> TokenStream2 { - let s: TokenStream = tt.clone().into(); - s.into() -} - #[cfg(feature = "macros")] #[proc_macro] #[proc_macro_error] @@ -100,8 +91,7 @@ pub fn chunk(input: TokenStream) -> TokenStream { let caps_len = chunk.captures().len(); let caps = chunk.captures().iter().map(|cap| { - let cap_name = cap.as_rust().to_string(); - let cap = to_ident(cap.as_rust()); + let cap_name = cap.name(); quote! { env.raw_set(#cap_name, #cap)?; } }); diff --git a/mlua_derive/src/token.rs b/mlua_derive/src/token.rs index afc89f88..51c565c3 100644 --- a/mlua_derive/src/token.rs +++ b/mlua_derive/src/token.rs @@ -76,8 +76,9 @@ impl Eq for Token {} impl Token { fn new(tree: TokenTree) -> Self { let (start, end) = span_pos(&tree.span()); + let source = tree.span().source_text().unwrap_or_else(|| tree.to_string()); Self { - source: tree.to_string(), + source, start, end, tree, @@ -136,7 +137,6 @@ impl Tokens { Tokens( tt.into_iter() .flat_map(Tokens::from) - .peekable() .batching(|iter| { // Find variable tokens: `$` + `ident` => `$ident` let t = iter.next()?; diff --git a/tests/chunk.rs b/tests/chunk.rs index 95c54274..468757e1 100644 --- a/tests/chunk.rs +++ b/tests/chunk.rs @@ -110,6 +110,21 @@ fn test_chunk_macro() -> Result<()> { assert_eq!(lua.globals().get::("s")?, 321); + // Check line numbers in error reporting + match lua + .load(mlua::chunk! { + local x = 1 + -- comment + error("boom") + }) + .exec() + { + Err(mlua::Error::RuntimeError(ref msg)) => { + assert!(msg.contains(":3:"), "expected line 3, got: {msg}"); + } + other => panic!("expected RuntimeError, got {other:?}"), + } + Ok(()) } From 023e4c61d843f10ff4409145f0809ed8f51dafbd Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 18 May 2026 22:44:00 +0100 Subject: [PATCH 068/138] mlua_derive: Fix compilation / remove "if let" guards --- mlua_derive/src/userdata_impl.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mlua_derive/src/userdata_impl.rs b/mlua_derive/src/userdata_impl.rs index 876dd488..c640378e 100644 --- a/mlua_derive/src/userdata_impl.rs +++ b/mlua_derive/src/userdata_impl.rs @@ -71,8 +71,12 @@ fn classify_ref_type(ty: &Type) -> Option { if ref_ty.mutability.is_none() { let lookup_name: Option = match &*ref_ty.elem { Type::Path(path) => path.path.segments.last().map(|seg| seg.ident.to_string()), - Type::Slice(slice) if let Type::Path(path) = &*slice.elem => { - path.path.segments.last().map(|seg| format!("[{}]", seg.ident)) + Type::Slice(slice) => { + if let Type::Path(path) = &*slice.elem { + path.path.segments.last().map(|seg| format!("[{}]", seg.ident)) + } else { + None + } } _ => None, }; From 6e7d6c78eda36858865f99e95d1a8d97627e4a55 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 18 May 2026 23:21:37 +0100 Subject: [PATCH 069/138] mlua_derive: Group functionality in modules Create new `chunk`, `userdata`, `module` modules. --- mlua_derive/src/{chunk.rs => chunk/mod.rs} | 67 ++++++++- mlua_derive/src/{ => chunk}/token.rs | 0 mlua_derive/src/from_lua.rs | 24 ++-- mlua_derive/src/lib.rs | 134 +----------------- mlua_derive/src/module.rs | 68 +++++++++ mlua_derive/src/{ => userdata}/attr.rs | 0 .../src/{userdata.rs => userdata/mod.rs} | 5 +- .../src/{ => userdata}/userdata_impl.rs | 2 +- 8 files changed, 151 insertions(+), 149 deletions(-) rename mlua_derive/src/{chunk.rs => chunk/mod.rs} (54%) rename mlua_derive/src/{ => chunk}/token.rs (100%) create mode 100644 mlua_derive/src/module.rs rename mlua_derive/src/{ => userdata}/attr.rs (100%) rename mlua_derive/src/{userdata.rs => userdata/mod.rs} (98%) rename mlua_derive/src/{ => userdata}/userdata_impl.rs (99%) diff --git a/mlua_derive/src/chunk.rs b/mlua_derive/src/chunk/mod.rs similarity index 54% rename from mlua_derive/src/chunk.rs rename to mlua_derive/src/chunk/mod.rs index 1b5ea0cd..50306e89 100644 --- a/mlua_derive/src/chunk.rs +++ b/mlua_derive/src/chunk/mod.rs @@ -2,9 +2,11 @@ use std::ops::Deref; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use quote::ToTokens; +use quote::{ToTokens, quote}; -use crate::token::{Pos, Token, Tokens}; +use self::token::{Pos, Token, Tokens}; + +mod token; #[derive(Debug, Clone)] pub(crate) struct Capture(Token); @@ -95,11 +97,64 @@ impl Chunk { } } - pub(crate) fn source(&self) -> &str { - &self.source - } - pub(crate) fn captures(&self) -> &[Capture] { self.caps.captures() } + + pub(crate) fn expand(&self) -> TokenStream2 { + let source = &self.source; + + let caps_len = self.captures().len(); + let caps = self.captures().iter().map(|cap| { + let cap_name = cap.name(); + quote! { env.raw_set(#cap_name, #cap)?; } + }); + + quote! {{ + use mlua::{AsChunk, ChunkMode, Lua, Result, Table}; + use ::std::borrow::Cow; + use ::std::cell::Cell; + use ::std::io::Result as IoResult; + + struct InnerChunk Result
>(Cell>); + + impl AsChunk for InnerChunk + where + F: FnOnce(&Lua) -> Result
, + { + fn environment(&self, lua: &Lua) -> Result> { + if #caps_len > 0 { + if let Some(make_env) = self.0.take() { + return make_env(lua).map(Some); + } + } + Ok(None) + } + + fn mode(&self) -> Option { + Some(ChunkMode::Text) + } + + fn source<'a>(&self) -> IoResult> { + Ok(Cow::Borrowed((#source).as_bytes())) + } + } + + let make_env = move |lua: &Lua| -> Result
{ + let globals = lua.globals(); + let env = lua.create_table()?; + let meta = lua.create_table()?; + meta.raw_set("__index", &globals)?; + meta.raw_set("__newindex", &globals)?; + + // Add captured variables + #(#caps)* + + env.set_metatable(Some(meta))?; + Ok(env) + }; + + InnerChunk(Cell::new(Some(make_env))) + }} + } } diff --git a/mlua_derive/src/token.rs b/mlua_derive/src/chunk/token.rs similarity index 100% rename from mlua_derive/src/token.rs rename to mlua_derive/src/chunk/token.rs diff --git a/mlua_derive/src/from_lua.rs b/mlua_derive/src/from_lua.rs index fbd2f22f..2ca69180 100644 --- a/mlua_derive/src/from_lua.rs +++ b/mlua_derive/src/from_lua.rs @@ -13,19 +13,19 @@ pub fn from_lua(input: TokenStream) -> TokenStream { }; quote! { - impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { - #[inline] - fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { - match value { - ::mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), - _ => Err(::mlua::Error::FromLuaConversionError { - from: value.type_name(), - to: #ident_str.to_string(), - message: None, - }), - } + impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { + #[inline] + fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { + match value { + ::mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => Err(::mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: #ident_str.to_string(), + message: None, + }), + } + } } - } } .into() } diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index e943d9ff..0a4f8af0 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -1,8 +1,6 @@ use proc_macro::TokenStream; -use proc_macro2::{Ident, Span}; -use quote::quote; -use syn::meta::ParseNestedMeta; -use syn::{ItemFn, LitStr, Result, parse_macro_input}; + +mod module; #[cfg(feature = "macros")] use {crate::chunk::Chunk, proc_macro_error2::proc_macro_error}; @@ -17,132 +15,16 @@ macro_rules! try_compile { }; } -#[derive(Default)] -struct ModuleAttributes { - name: Option, - skip_memory_check: bool, -} - -impl ModuleAttributes { - fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { - if meta.path.is_ident("name") { - match meta.value() { - Ok(value) => { - self.name = Some(value.parse::()?.parse()?); - } - Err(_) => { - return Err(meta.error("`name` attribute must have a value")); - } - } - } else if meta.path.is_ident("skip_memory_check") { - if meta.value().is_ok() { - return Err(meta.error("`skip_memory_check` attribute have no values")); - } - self.skip_memory_check = true; - } else { - return Err(meta.error("unsupported module attribute")); - } - Ok(()) - } -} - #[proc_macro_attribute] pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { - let mut args = ModuleAttributes::default(); - if !attr.is_empty() { - let args_parser = syn::meta::parser(|meta| args.parse(meta)); - parse_macro_input!(attr with args_parser); - } - - let func = parse_macro_input!(item as ItemFn); - let func_name = &func.sig.ident; - let module_name = args.name.unwrap_or_else(|| func_name.clone()); - let ext_entrypoint_name = Ident::new(&format!("luaopen_{module_name}"), Span::call_site()); - let skip_memory_check = if args.skip_memory_check { - quote! { lua.skip_memory_check(true); } - } else { - quote! {} - }; - - let wrapped = quote! { - mlua::require_module_feature!(); - - #func - - #[unsafe(no_mangle)] - unsafe extern "C-unwind" fn #ext_entrypoint_name(state: *mut mlua::lua_State) -> ::std::os::raw::c_int { - mlua::Lua::entrypoint1(state, move |lua| { - #skip_memory_check - #func_name(lua) - }) - } - }; - - wrapped.into() + module::lua_module(attr, item) } #[cfg(feature = "macros")] #[proc_macro] #[proc_macro_error] pub fn chunk(input: TokenStream) -> TokenStream { - let chunk = Chunk::new(input); - - let source = chunk.source(); - - let caps_len = chunk.captures().len(); - let caps = chunk.captures().iter().map(|cap| { - let cap_name = cap.name(); - quote! { env.raw_set(#cap_name, #cap)?; } - }); - - let wrapped_code = quote! {{ - use mlua::{AsChunk, ChunkMode, Lua, Result, Table}; - use ::std::borrow::Cow; - use ::std::cell::Cell; - use ::std::io::Result as IoResult; - - struct InnerChunk Result
>(Cell>); - - impl AsChunk for InnerChunk - where - F: FnOnce(&Lua) -> Result
, - { - fn environment(&self, lua: &Lua) -> Result> { - if #caps_len > 0 { - if let Some(make_env) = self.0.take() { - return make_env(lua).map(Some); - } - } - Ok(None) - } - - fn mode(&self) -> Option { - Some(ChunkMode::Text) - } - - fn source<'a>(&self) -> IoResult> { - Ok(Cow::Borrowed((#source).as_bytes())) - } - } - - let make_env = move |lua: &Lua| -> Result
{ - let globals = lua.globals(); - let env = lua.create_table()?; - let meta = lua.create_table()?; - meta.raw_set("__index", &globals)?; - meta.raw_set("__newindex", &globals)?; - - // Add captured variables - #(#caps)* - - env.set_metatable(Some(meta))?; - Ok(env) - }; - - InnerChunk(Cell::new(Some(make_env))) - }}; - - wrapped_code.into() + Chunk::new(input).expand().into() } #[cfg(feature = "macros")] @@ -162,18 +44,12 @@ pub fn userdata(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(feature = "macros")] #[proc_macro_attribute] pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { - userdata_impl::userdata_impl(attr, item) + userdata::userdata_impl::userdata_impl(attr, item) } -#[cfg(feature = "macros")] -mod attr; #[cfg(feature = "macros")] mod chunk; #[cfg(feature = "macros")] mod from_lua; #[cfg(feature = "macros")] -mod token; -#[cfg(feature = "macros")] mod userdata; -#[cfg(feature = "macros")] -mod userdata_impl; diff --git a/mlua_derive/src/module.rs b/mlua_derive/src/module.rs new file mode 100644 index 00000000..bad7bf04 --- /dev/null +++ b/mlua_derive/src/module.rs @@ -0,0 +1,68 @@ +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::quote; +use syn::meta::ParseNestedMeta; +use syn::{ItemFn, LitStr, Result, parse_macro_input}; + +#[derive(Default)] +struct ModuleAttributes { + name: Option, + skip_memory_check: bool, +} + +impl ModuleAttributes { + fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { + if meta.path.is_ident("name") { + match meta.value() { + Ok(value) => { + self.name = Some(value.parse::()?.parse()?); + } + Err(_) => { + return Err(meta.error("`name` attribute must have a value")); + } + } + } else if meta.path.is_ident("skip_memory_check") { + if meta.value().is_ok() { + return Err(meta.error("`skip_memory_check` attribute have no values")); + } + self.skip_memory_check = true; + } else { + return Err(meta.error("unsupported module attribute")); + } + Ok(()) + } +} + +pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { + let mut args = ModuleAttributes::default(); + if !attr.is_empty() { + let args_parser = syn::meta::parser(|meta| args.parse(meta)); + parse_macro_input!(attr with args_parser); + } + + let func = parse_macro_input!(item as ItemFn); + let func_name = &func.sig.ident; + let module_name = args.name.unwrap_or_else(|| func_name.clone()); + let ext_entrypoint_name = Ident::new(&format!("luaopen_{module_name}"), Span::call_site()); + let skip_memory_check = if args.skip_memory_check { + quote! { lua.skip_memory_check(true); } + } else { + quote! {} + }; + + let wrapped = quote! { + mlua::require_module_feature!(); + + #func + + #[unsafe(no_mangle)] + unsafe extern "C-unwind" fn #ext_entrypoint_name(state: *mut mlua::lua_State) -> ::std::os::raw::c_int { + mlua::Lua::entrypoint1(state, move |lua| { + #skip_memory_check + #func_name(lua) + }) + } + }; + + wrapped.into() +} diff --git a/mlua_derive/src/attr.rs b/mlua_derive/src/userdata/attr.rs similarity index 100% rename from mlua_derive/src/attr.rs rename to mlua_derive/src/userdata/attr.rs diff --git a/mlua_derive/src/userdata.rs b/mlua_derive/src/userdata/mod.rs similarity index 98% rename from mlua_derive/src/userdata.rs rename to mlua_derive/src/userdata/mod.rs index 393f3b62..0e578e0a 100644 --- a/mlua_derive/src/userdata.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -1,8 +1,11 @@ +mod attr; +pub(crate) mod userdata_impl; + use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; -use crate::attr::LuaAttr; +use self::attr::LuaAttr; /// Parse all `#[lua(...)]` attributes on a field, merging them into one `LuaAttr`. fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { diff --git a/mlua_derive/src/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs similarity index 99% rename from mlua_derive/src/userdata_impl.rs rename to mlua_derive/src/userdata/userdata_impl.rs index c640378e..ca299f1e 100644 --- a/mlua_derive/src/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -7,7 +7,7 @@ use syn::{ Attribute, FnArg, Ident, ImplItem, ItemImpl, Meta, Signature, Type, parse_macro_input, parse_quote, }; -use crate::attr::LuaAttr; +use super::attr::LuaAttr; /// `&T` reference types that mlua provides as wrapper types via `FromLua`. static BORROW_WRAPPERS: &[(&str, &str)] = &[ From 1d4a7564360929b3655cbacdce3286fe266010af Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 29 May 2026 00:54:56 +0100 Subject: [PATCH 070/138] mlua_derive: Support async userdata methods in macro --- mlua_derive/src/userdata/mod.rs | 24 ++- mlua_derive/src/userdata/userdata_impl.rs | 172 ++++++++++++++++-- tests/compile.rs | 7 + .../compile/async_any_userdata_method.stderr | 20 +- tests/compile/lua_norefunwindsafe.stderr | 10 + tests/compile/ref_nounwindsafe.stderr | 10 + tests/compile/userdata_field_async.rs | 15 ++ tests/compile/userdata_field_async.stderr | 13 ++ tests/compile/userdata_getter_async.rs | 15 ++ tests/compile/userdata_getter_async.stderr | 13 ++ tests/compile/userdata_setter_async.rs | 16 ++ tests/compile/userdata_setter_async.stderr | 13 ++ tests/userdata_macro.rs | 115 ++++++++++++ 13 files changed, 418 insertions(+), 25 deletions(-) create mode 100644 tests/compile/userdata_field_async.rs create mode 100644 tests/compile/userdata_field_async.stderr create mode 100644 tests/compile/userdata_getter_async.rs create mode 100644 tests/compile/userdata_getter_async.stderr create mode 100644 tests/compile/userdata_setter_async.rs create mode 100644 tests/compile/userdata_setter_async.stderr diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index 0e578e0a..ff87bfc4 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 ca299f1e..4dbadc63 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 50f70f1a..64d2fffd 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 3e01c45b..116b64f7 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 2b3e6648..e2807c4e 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 07613de8..06990592 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 00000000..004b4128 --- /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 00000000..13d7c686 --- /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 00000000..3591f0bd --- /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 00000000..8b8866ee --- /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 00000000..4d50e96d --- /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 00000000..46ac54bb --- /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 843da7fe..b0c5e85e 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(); + } +} From 1f3dafa564ac05146b0dc874ba0bb8eed3c97797 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 30 May 2026 12:46:31 +0100 Subject: [PATCH 071/138] mlua_derive: Reject static field functions with args --- mlua_derive/src/userdata/userdata_impl.rs | 8 ++++++++ tests/compile.rs | 1 + tests/compile/userdata_field_with_args.rs | 15 +++++++++++++++ tests/compile/userdata_field_with_args.stderr | 5 +++++ 4 files changed, 29 insertions(+) create mode 100644 tests/compile/userdata_field_with_args.rs create mode 100644 tests/compile/userdata_field_with_args.stderr diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 4dbadc63..a63eeded 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -335,6 +335,14 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { .to_compile_error() .into(); } + if !info.args.is_empty() { + return syn::Error::new_spanned( + &method.sig, + "field function must not take arguments", + ) + .to_compile_error() + .into(); + } let lua_name = lua_attr.name(fn_name); if lua_attr.meta { let tokens = quote! { diff --git a/tests/compile.rs b/tests/compile.rs index 64d2fffd..6bd84083 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -35,6 +35,7 @@ fn test_compilation() { t.compile_fail("tests/compile/userdata_static_with_self.rs"); t.compile_fail("tests/compile/userdata_meta_owned_self.rs"); t.compile_fail("tests/compile/userdata_const_getter.rs"); + t.compile_fail("tests/compile/userdata_field_with_args.rs"); } #[cfg(all(feature = "macros", feature = "async"))] diff --git a/tests/compile/userdata_field_with_args.rs b/tests/compile/userdata_field_with_args.rs new file mode 100644 index 00000000..5394eafd --- /dev/null +++ b/tests/compile/userdata_field_with_args.rs @@ -0,0 +1,15 @@ +#[derive(Default)] +#[mlua::userdata] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + fn as_name(name: &str) -> String { + name.to_string() + } +} + +fn main() {} diff --git a/tests/compile/userdata_field_with_args.stderr b/tests/compile/userdata_field_with_args.stderr new file mode 100644 index 00000000..c7ccf45c --- /dev/null +++ b/tests/compile/userdata_field_with_args.stderr @@ -0,0 +1,5 @@ +error: field function must not take arguments + --> tests/compile/userdata_field_with_args.rs:10:5 + | +10 | fn as_name(name: &str) -> String { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From b7c98ad9bbb6a95f29ef2dff16433d4dfbbd24ad Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 30 May 2026 13:11:26 +0100 Subject: [PATCH 072/138] mlua_derive: Switch from `#[userdata]` to `[derive(UserData)]` --- examples/userdata.rs | 5 +-- mlua_derive/src/lib.rs | 8 ++-- mlua_derive/src/userdata/mod.rs | 38 ++++--------------- src/lib.rs | 8 ++-- tests/compile/userdata_const_getter.rs | 3 +- tests/compile/userdata_const_getter.stderr | 4 +- tests/compile/userdata_field_async.rs | 3 +- tests/compile/userdata_field_async.stderr | 8 ++-- tests/compile/userdata_field_with_args.rs | 3 +- tests/compile/userdata_field_with_args.stderr | 8 ++-- tests/compile/userdata_getter_and_meta.rs | 3 +- tests/compile/userdata_getter_and_meta.stderr | 4 +- tests/compile/userdata_getter_and_setter.rs | 3 +- .../compile/userdata_getter_and_setter.stderr | 8 ++-- tests/compile/userdata_getter_async.rs | 3 +- tests/compile/userdata_getter_async.stderr | 8 ++-- tests/compile/userdata_getter_extra_arg.rs | 3 +- .../compile/userdata_getter_extra_arg.stderr | 8 ++-- tests/compile/userdata_getter_mut_self.rs | 3 +- tests/compile/userdata_getter_mut_self.stderr | 8 ++-- tests/compile/userdata_meta_owned_self.rs | 3 +- tests/compile/userdata_meta_owned_self.stderr | 4 +- tests/compile/userdata_mut_slice_arg.rs | 3 +- tests/compile/userdata_mut_slice_arg.stderr | 4 +- tests/compile/userdata_setter_async.rs | 3 +- tests/compile/userdata_setter_async.stderr | 8 ++-- tests/compile/userdata_setter_no_value.rs | 3 +- tests/compile/userdata_setter_no_value.stderr | 8 ++-- tests/compile/userdata_setter_ref_self.rs | 3 +- tests/compile/userdata_setter_ref_self.stderr | 8 ++-- tests/compile/userdata_static_with_self.rs | 3 +- .../compile/userdata_static_with_self.stderr | 8 ++-- tests/userdata_macro.rs | 19 ++++------ 33 files changed, 85 insertions(+), 131 deletions(-) diff --git a/examples/userdata.rs b/examples/userdata.rs index 986e546f..25b70a8d 100644 --- a/examples/userdata.rs +++ b/examples/userdata.rs @@ -1,7 +1,6 @@ -use mlua::{Lua, Result, chunk}; +use mlua::{Lua, Result, UserData, chunk}; -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, UserData)] struct Rectangle { length: u32, width: u32, diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index 0a4f8af0..8fc6dd49 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -33,11 +33,11 @@ pub fn from_lua(input: TokenStream) -> TokenStream { from_lua::from_lua(input) } -/// Attribute macro for exposing a Rust type as a Lua userdata. +/// Derive macro for implementing `UserData` for a Rust type. #[cfg(feature = "macros")] -#[proc_macro_attribute] -pub fn userdata(attr: TokenStream, item: TokenStream) -> TokenStream { - userdata::userdata_type(attr, item) +#[proc_macro_derive(UserData, attributes(lua))] +pub fn userdata(item: TokenStream) -> TokenStream { + userdata::userdata_type(item) } /// Attribute macro for exposing impl block methods to Lua userdata. diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index ff87bfc4..83d91e3a 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -34,35 +34,18 @@ fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { Ok(lua_attr) } -/// Strip `#[lua(...)]` attributes from a field, keeping all others. -fn strip_lua_attrs(attrs: &[Attribute]) -> Vec { - (attrs.iter()) - .filter(|attr| !attr.path().is_ident("lua")) - .cloned() - .collect() -} - -pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream { - if !attr.is_empty() { - return Error::new_spanned( - proc_macro2::TokenStream::from(attr), - "`#[userdata]` does not accept arguments", - ) - .to_compile_error() - .into(); - } - - let mut input = parse_macro_input!(item as DeriveInput); +pub fn userdata_type(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); let type_name = &input.ident; - let mut named_fields: Option<&mut FieldsNamed> = match &mut input.data { - Data::Struct(data) => match &mut data.fields { + let named_fields: Option<&FieldsNamed> = match &input.data { + Data::Struct(data) => match &data.fields { Fields::Named(fields) => Some(fields), Fields::Unnamed(_) | Fields::Unit => None, }, Data::Enum(_) => None, Data::Union(_) => { - return Error::new_spanned(&input, "`#[userdata]` cannot be applied to unions") + return Error::new_spanned(&input, "`#[derive(UserData)]` cannot be applied to unions") .to_compile_error() .into(); } @@ -73,14 +56,14 @@ pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream { if has_type_params { return Error::new_spanned( &input.generics, - "`#[userdata]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead." + "`#[derive(UserData)]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead." ) .to_compile_error() .into(); } let mut field_registrations = Vec::new(); - if let Some(fields) = &mut named_fields { + if let Some(fields) = &named_fields { for field in &fields.named { let field_name = field.ident.as_ref().unwrap(); @@ -114,19 +97,12 @@ pub fn userdata_type(attr: TokenStream, item: TokenStream) -> TokenStream { field_registrations.push(with_cfg(tokens, &field.attrs)); } } - - // Strip mlua-specific attributes from fields before re-emitting - for field in &mut fields.named { - field.attrs = strip_lua_attrs(&field.attrs); - } } let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}"); let register_fields_fn_name = format_ident!("__mlua_register_{type_name}_fields"); let output = quote! { - #input - #[doc(hidden)] #[allow(non_camel_case_types)] struct #registration_type_name { diff --git a/src/lib.rs b/src/lib.rs index bf06d472..95e67c74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,16 +233,14 @@ pub use mlua_derive::chunk; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::FromLua; -/// Attribute macro for exposing a struct as Lua userdata. +/// Derive macro for implementing [`UserData`] for a Rust type. /// -/// All fields are auto-exposed as get/set. +/// Named fields are auto-exposed as get/set. /// /// Use `#[lua(...)]` to customize. -/// -/// This generates a [`UserData`] trait implementation. #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] -pub use mlua_derive::userdata; +pub use mlua_derive::UserData; /// Attribute macro for exposing impl block methods to Lua userdata. /// diff --git a/tests/compile/userdata_const_getter.rs b/tests/compile/userdata_const_getter.rs index 6394ed87..12584106 100644 --- a/tests/compile/userdata_const_getter.rs +++ b/tests/compile/userdata_const_getter.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo; #[mlua::userdata_impl] diff --git a/tests/compile/userdata_const_getter.stderr b/tests/compile/userdata_const_getter.stderr index 6b7ef6ad..b29f21a8 100644 --- a/tests/compile/userdata_const_getter.stderr +++ b/tests/compile/userdata_const_getter.stderr @@ -1,5 +1,5 @@ error: const items do not support `getter` or `setter` - --> tests/compile/userdata_const_getter.rs:8:11 + --> tests/compile/userdata_const_getter.rs:7:11 | -8 | const X: u32 = 42; +7 | const X: u32 = 42; | ^ diff --git a/tests/compile/userdata_field_async.rs b/tests/compile/userdata_field_async.rs index 004b4128..fd2b5332 100644 --- a/tests/compile/userdata_field_async.rs +++ b/tests/compile/userdata_field_async.rs @@ -1,7 +1,6 @@ use mlua::Result; -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, mlua::UserData)] struct Foo; #[mlua::userdata_impl] diff --git a/tests/compile/userdata_field_async.stderr b/tests/compile/userdata_field_async.stderr index 13d7c686..a41d49fb 100644 --- a/tests/compile/userdata_field_async.stderr +++ b/tests/compile/userdata_field_async.stderr @@ -1,8 +1,8 @@ error: async field function is not supported - --> tests/compile/userdata_field_async.rs:10:5 - | -10 | async fn description() -> Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_field_async.rs:9:5 + | +9 | async fn description() -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused import: `mlua::Result` --> tests/compile/userdata_field_async.rs:1:5 diff --git a/tests/compile/userdata_field_with_args.rs b/tests/compile/userdata_field_with_args.rs index 5394eafd..8757548c 100644 --- a/tests/compile/userdata_field_with_args.rs +++ b/tests/compile/userdata_field_with_args.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_field_with_args.stderr b/tests/compile/userdata_field_with_args.stderr index c7ccf45c..a7501385 100644 --- a/tests/compile/userdata_field_with_args.stderr +++ b/tests/compile/userdata_field_with_args.stderr @@ -1,5 +1,5 @@ error: field function must not take arguments - --> tests/compile/userdata_field_with_args.rs:10:5 - | -10 | fn as_name(name: &str) -> String { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_field_with_args.rs:9:5 + | +9 | fn as_name(name: &str) -> String { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_meta.rs b/tests/compile/userdata_getter_and_meta.rs index 1c78f30f..4da36885 100644 --- a/tests/compile/userdata_getter_and_meta.rs +++ b/tests/compile/userdata_getter_and_meta.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo; #[mlua::userdata_impl] diff --git a/tests/compile/userdata_getter_and_meta.stderr b/tests/compile/userdata_getter_and_meta.stderr index 9b7e2192..68bd1bda 100644 --- a/tests/compile/userdata_getter_and_meta.stderr +++ b/tests/compile/userdata_getter_and_meta.stderr @@ -1,5 +1,5 @@ error: `meta` can only be combined with `field` - --> tests/compile/userdata_getter_and_meta.rs:8:5 + --> tests/compile/userdata_getter_and_meta.rs:7:5 | -8 | fn bar(&self) -> mlua::Result { +7 | fn bar(&self) -> mlua::Result { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_setter.rs b/tests/compile/userdata_getter_and_setter.rs index d6f33da9..2232dedf 100644 --- a/tests/compile/userdata_getter_and_setter.rs +++ b/tests/compile/userdata_getter_and_setter.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_getter_and_setter.stderr b/tests/compile/userdata_getter_and_setter.stderr index 910ebc55..1f15a33c 100644 --- a/tests/compile/userdata_getter_and_setter.stderr +++ b/tests/compile/userdata_getter_and_setter.stderr @@ -1,5 +1,5 @@ error: at most one of `getter`, `setter`, `field` can be specified - --> tests/compile/userdata_getter_and_setter.rs:10:5 - | -10 | fn x(&self) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_getter_and_setter.rs:9:5 + | +9 | fn x(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_async.rs b/tests/compile/userdata_getter_async.rs index 3591f0bd..8d99f81d 100644 --- a/tests/compile/userdata_getter_async.rs +++ b/tests/compile/userdata_getter_async.rs @@ -1,7 +1,6 @@ use mlua::Result; -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, mlua::UserData)] struct Foo(u64); #[mlua::userdata_impl] diff --git a/tests/compile/userdata_getter_async.stderr b/tests/compile/userdata_getter_async.stderr index 8b8866ee..fb8184d4 100644 --- a/tests/compile/userdata_getter_async.stderr +++ b/tests/compile/userdata_getter_async.stderr @@ -1,8 +1,8 @@ error: async field getter is not supported - --> tests/compile/userdata_getter_async.rs:10:5 - | -10 | async fn value(&self) -> Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_getter_async.rs:9:5 + | +9 | async fn value(&self) -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused import: `mlua::Result` --> tests/compile/userdata_getter_async.rs:1:5 diff --git a/tests/compile/userdata_getter_extra_arg.rs b/tests/compile/userdata_getter_extra_arg.rs index 5e61ebc0..fd218f0d 100644 --- a/tests/compile/userdata_getter_extra_arg.rs +++ b/tests/compile/userdata_getter_extra_arg.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_getter_extra_arg.stderr b/tests/compile/userdata_getter_extra_arg.stderr index 34adb6ea..0e1b2060 100644 --- a/tests/compile/userdata_getter_extra_arg.stderr +++ b/tests/compile/userdata_getter_extra_arg.stderr @@ -1,5 +1,5 @@ error: field getter must not take additional arguments - --> tests/compile/userdata_getter_extra_arg.rs:10:5 - | -10 | fn x(&self, extra: u32) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_getter_extra_arg.rs:9:5 + | +9 | fn x(&self, extra: u32) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_mut_self.rs b/tests/compile/userdata_getter_mut_self.rs index b48c2e69..96d977c7 100644 --- a/tests/compile/userdata_getter_mut_self.rs +++ b/tests/compile/userdata_getter_mut_self.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_getter_mut_self.stderr b/tests/compile/userdata_getter_mut_self.stderr index 79aa08f5..db160943 100644 --- a/tests/compile/userdata_getter_mut_self.stderr +++ b/tests/compile/userdata_getter_mut_self.stderr @@ -1,5 +1,5 @@ error: field getter must take `&self` - --> tests/compile/userdata_getter_mut_self.rs:10:5 - | -10 | fn x(&mut self) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_getter_mut_self.rs:9:5 + | +9 | fn x(&mut self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_meta_owned_self.rs b/tests/compile/userdata_meta_owned_self.rs index ef85b801..438d9172 100644 --- a/tests/compile/userdata_meta_owned_self.rs +++ b/tests/compile/userdata_meta_owned_self.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo; #[mlua::userdata_impl] diff --git a/tests/compile/userdata_meta_owned_self.stderr b/tests/compile/userdata_meta_owned_self.stderr index 5cb10a18..b0157f2f 100644 --- a/tests/compile/userdata_meta_owned_self.stderr +++ b/tests/compile/userdata_meta_owned_self.stderr @@ -1,5 +1,5 @@ error: meta methods cannot take `self`, use `&[mut] self` instead - --> tests/compile/userdata_meta_owned_self.rs:8:5 + --> tests/compile/userdata_meta_owned_self.rs:7:5 | -8 | fn __gc(self) -> mlua::Result<()> { +7 | fn __gc(self) -> mlua::Result<()> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_mut_slice_arg.rs b/tests/compile/userdata_mut_slice_arg.rs index 1ecc5055..4ef8ec79 100644 --- a/tests/compile/userdata_mut_slice_arg.rs +++ b/tests/compile/userdata_mut_slice_arg.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo(Vec); #[mlua::userdata_impl] diff --git a/tests/compile/userdata_mut_slice_arg.stderr b/tests/compile/userdata_mut_slice_arg.stderr index 43780fc9..2b763cd6 100644 --- a/tests/compile/userdata_mut_slice_arg.stderr +++ b/tests/compile/userdata_mut_slice_arg.stderr @@ -1,5 +1,5 @@ error: this reference type is not supported as a callback parameter - --> tests/compile/userdata_mut_slice_arg.rs:7:27 + --> tests/compile/userdata_mut_slice_arg.rs:6:27 | -7 | fn first(&self, data: &mut [u8]) -> mlua::Result { +6 | fn first(&self, data: &mut [u8]) -> mlua::Result { | ^^^^^^^^^ diff --git a/tests/compile/userdata_setter_async.rs b/tests/compile/userdata_setter_async.rs index 4d50e96d..180f425d 100644 --- a/tests/compile/userdata_setter_async.rs +++ b/tests/compile/userdata_setter_async.rs @@ -1,7 +1,6 @@ use mlua::Result; -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, mlua::UserData)] struct Foo(u64); #[mlua::userdata_impl] diff --git a/tests/compile/userdata_setter_async.stderr b/tests/compile/userdata_setter_async.stderr index 46ac54bb..3125c1a0 100644 --- a/tests/compile/userdata_setter_async.stderr +++ b/tests/compile/userdata_setter_async.stderr @@ -1,8 +1,8 @@ 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<()> { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_setter_async.rs:9:5 + | +9 | async fn set_value(&mut self, val: u64) -> Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: unused import: `mlua::Result` --> tests/compile/userdata_setter_async.rs:1:5 diff --git a/tests/compile/userdata_setter_no_value.rs b/tests/compile/userdata_setter_no_value.rs index 8334dc78..5218c8d6 100644 --- a/tests/compile/userdata_setter_no_value.rs +++ b/tests/compile/userdata_setter_no_value.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_setter_no_value.stderr b/tests/compile/userdata_setter_no_value.stderr index b6ce7dc0..4b521419 100644 --- a/tests/compile/userdata_setter_no_value.stderr +++ b/tests/compile/userdata_setter_no_value.stderr @@ -1,5 +1,5 @@ error: field setter must take exactly one value argument - --> tests/compile/userdata_setter_no_value.rs:10:5 - | -10 | fn set_x(&mut self) -> mlua::Result<()> { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_setter_no_value.rs:9:5 + | +9 | fn set_x(&mut self) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_setter_ref_self.rs b/tests/compile/userdata_setter_ref_self.rs index afc7572a..b88ea1e1 100644 --- a/tests/compile/userdata_setter_ref_self.rs +++ b/tests/compile/userdata_setter_ref_self.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_setter_ref_self.stderr b/tests/compile/userdata_setter_ref_self.stderr index 49d85cf9..d15604f8 100644 --- a/tests/compile/userdata_setter_ref_self.stderr +++ b/tests/compile/userdata_setter_ref_self.stderr @@ -1,5 +1,5 @@ error: field setter must take `&[mut] self` - --> tests/compile/userdata_setter_ref_self.rs:10:5 - | -10 | fn set_x(self, val: u32) -> mlua::Result<()> { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_setter_ref_self.rs:9:5 + | +9 | fn set_x(self, val: u32) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_static_with_self.rs b/tests/compile/userdata_static_with_self.rs index c97fa4de..ad2ea1dc 100644 --- a/tests/compile/userdata_static_with_self.rs +++ b/tests/compile/userdata_static_with_self.rs @@ -1,5 +1,4 @@ -#[derive(Default)] -#[mlua::userdata] +#[derive(Default, mlua::UserData)] struct Foo { x: u32, } diff --git a/tests/compile/userdata_static_with_self.stderr b/tests/compile/userdata_static_with_self.stderr index adc552e8..56b4b419 100644 --- a/tests/compile/userdata_static_with_self.stderr +++ b/tests/compile/userdata_static_with_self.stderr @@ -1,5 +1,5 @@ error: field function must not take `self` - --> tests/compile/userdata_static_with_self.rs:10:5 - | -10 | fn get_x(&self) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> tests/compile/userdata_static_with_self.rs:9:5 + | +9 | fn get_x(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index b0c5e85e..169c9247 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -1,9 +1,8 @@ #![cfg(feature = "macros")] -use mlua::{Lua, Result}; +use mlua::{Lua, Result, UserData}; -#[derive(Default, Clone, Debug)] -#[mlua::userdata] +#[derive(Default, Clone, Debug, UserData)] struct Rectangle { length: u32, #[lua] @@ -208,8 +207,7 @@ fn test_rectangle() { .unwrap(); } -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, UserData)] enum Color { Red, Green, @@ -274,8 +272,7 @@ fn test_color() { .unwrap(); } -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, UserData)] struct Point(i32, i32); fn make_lua_point() -> Lua { @@ -328,8 +325,7 @@ fn test_point() { .unwrap(); } -#[derive(Clone, Debug)] -#[mlua::userdata] +#[derive(Clone, Debug, UserData)] struct Bytes(Vec); #[mlua::userdata_impl] @@ -375,10 +371,9 @@ fn test_known_borrow_wrappers() -> Result<()> { #[cfg(feature = "async")] mod async_tests { - use mlua::{Lua, Result}; + use mlua::{Lua, Result, UserData}; - #[derive(Clone, Debug)] - #[mlua::userdata] + #[derive(Clone, Debug, UserData)] struct AsyncCounter(u64); #[mlua::userdata_impl] From a7c5a24a7b725b8eb395aa28b0e3838c64ff2dbb Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 30 May 2026 22:23:03 +0100 Subject: [PATCH 073/138] mlua_derive: Deny `#[lua = "..."]` syntax --- mlua_derive/src/userdata/mod.rs | 18 ++++++++++++++---- mlua_derive/src/userdata/userdata_impl.rs | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index 83d91e3a..4cbfd4ec 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -25,10 +25,20 @@ pub(crate) fn with_cfg(tokens: proc_macro2::TokenStream, attrs: &[Attribute]) -> fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { let mut lua_attr = LuaAttr::default(); for attr in attrs { - if attr.path().is_ident("lua") - && let Meta::List(_) = &attr.meta - { - attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + if !attr.path().is_ident("lua") { + continue; + } + match &attr.meta { + Meta::List(_) => { + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + } + Meta::Path(_) => {} + Meta::NameValue(_) => { + return Err(syn::Error::new_spanned( + attr, + "`#[lua = \"...\"]` is not supported: use `#[lua(attr = \"...\")]`", + )); + } } } Ok(lua_attr) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index a63eeded..d082bb49 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -177,10 +177,20 @@ fn strip_item_attrs(attrs: &[Attribute]) -> Vec { fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { let mut lua_attr = LuaAttr::default(); for attr in attrs { - if attr.path().is_ident("lua") - && let Meta::List(_) = &attr.meta - { - attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + if !attr.path().is_ident("lua") { + continue; + } + match &attr.meta { + Meta::List(_) => { + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + } + Meta::Path(_) => {} + Meta::NameValue(_) => { + return Err(syn::Error::new_spanned( + attr, + "`#[lua = \"...\"]` is not supported: use `#[lua(attr = \"...\")]`", + )); + } } } Ok(lua_attr) From ca360f90192a9c88446677951037d02d16900643 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 11:01:15 +0100 Subject: [PATCH 074/138] Move inlined `chunk!` macro doc to docs/chunk.md --- docs/chunk.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 49 +----------------------------------------------- 2 files changed, 53 insertions(+), 48 deletions(-) create mode 100644 docs/chunk.md diff --git a/docs/chunk.md b/docs/chunk.md new file mode 100644 index 00000000..43c06ae2 --- /dev/null +++ b/docs/chunk.md @@ -0,0 +1,52 @@ +Create a type that implements [`AsChunk`] and can capture Rust variables. + +This macro allows to write Lua code directly in Rust code. + +Rust variables can be referenced from Lua using `$` prefix, as shown in the example below. +User's Rust types needs to implement [`UserData`] or [`IntoLua`] traits. + +Captured variables are **moved** into the chunk. + +``` +use mlua::{Lua, Result, chunk}; + +fn main() -> Result<()> { + let lua = Lua::new(); + let name = "Rustacean"; + lua.load(chunk! { + print("hello, " .. $name) + }).exec() +} +``` + +## Syntax issues + +Since the Rust tokenizer will tokenize Lua code, this imposes some restrictions. +The main thing to remember is: + +- Use double quoted strings (`""`) instead of single quoted strings (`''`). + + (Single quoted strings only work if they contain a single character, since in Rust, + `'a'` is a character literal). + +- Using Lua comments `--` is not desirable in **stable** Rust and can have bad side effects. + + This is because procedural macros have Line/Column information available only in + **nightly** Rust. Instead, Lua chunks represented as a big single line of code in stable Rust. + + As workaround, Rust comments `//` can be used. + +Other minor limitations: + +- Certain escape codes in string literals don't work. (Specifically: `\a`, `\b`, `\f`, `\v`, + `\123` (octal escape codes), `\u`, and `\U`). + + These are accepted: : `\\`, `\n`, `\t`, `\r`, `\xAB` (hex escape codes), and `\0`. + +- The `//` (floor division) operator is unusable, as its start a comment. + +Everything else should work. + +[`AsChunk`]: crate::chunk::AsChunk +[`UserData`]: crate::UserData +[`IntoLua`]: crate::IntoLua diff --git a/src/lib.rs b/src/lib.rs index 95e67c74..d18e8460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,54 +173,7 @@ pub mod serde; #[macro_use] extern crate mlua_derive; -/// Create a type that implements [`AsChunk`] and can capture Rust variables. -/// -/// This macro allows to write Lua code directly in Rust code. -/// -/// Rust variables can be referenced from Lua using `$` prefix, as shown in the example below. -/// User's Rust types needs to implement [`UserData`] or [`IntoLua`] traits. -/// -/// Captured variables are **moved** into the chunk. -/// -/// ``` -/// use mlua::{Lua, Result, chunk}; -/// -/// fn main() -> Result<()> { -/// let lua = Lua::new(); -/// let name = "Rustacean"; -/// lua.load(chunk! { -/// print("hello, " .. $name) -/// }).exec() -/// } -/// ``` -/// -/// ## Syntax issues -/// -/// Since the Rust tokenizer will tokenize Lua code, this imposes some restrictions. -/// The main thing to remember is: -/// -/// - Use double quoted strings (`""`) instead of single quoted strings (`''`). -/// -/// (Single quoted strings only work if they contain a single character, since in Rust, -/// `'a'` is a character literal). -/// -/// - Using Lua comments `--` is not desirable in **stable** Rust and can have bad side effects. -/// -/// This is because procedural macros have Line/Column information available only in -/// **nightly** Rust. Instead, Lua chunks represented as a big single line of code in stable Rust. -/// -/// As workaround, Rust comments `//` can be used. -/// -/// Other minor limitations: -/// -/// - Certain escape codes in string literals don't work. (Specifically: `\a`, `\b`, `\f`, `\v`, -/// `\123` (octal escape codes), `\u`, and `\U`). -/// -/// These are accepted: : `\\`, `\n`, `\t`, `\r`, `\xAB` (hex escape codes), and `\0`. -/// -/// - The `//` (floor division) operator is unusable, as its start a comment. -/// -/// Everything else should work. +#[doc = include_str!("../docs/chunk.md")] #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::chunk; From 208a70f4077b78b707dfde48b7a8cc80c25f1129 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 11:17:51 +0100 Subject: [PATCH 075/138] Move inlined `#[lua_module]` proc macro doc to docs/lua_module.md --- docs/lua_module.md | 41 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 42 +----------------------------------------- 2 files changed, 42 insertions(+), 41 deletions(-) create mode 100644 docs/lua_module.md diff --git a/docs/lua_module.md b/docs/lua_module.md new file mode 100644 index 00000000..9156df99 --- /dev/null +++ b/docs/lua_module.md @@ -0,0 +1,41 @@ +Registers Lua module entrypoint. + +You can register multiple entrypoints as required. + +```ignore +use mlua::{Lua, Result, Table}; + +#[mlua::lua_module] +fn my_module(lua: &Lua) -> Result
{ + let exports = lua.create_table()?; + exports.set("hello", "world")?; + Ok(exports) +} +``` + +Internally in the code above the compiler defines C function `luaopen_my_module`. + +You can also pass options to the attribute: + +* name - name of the module, defaults to the name of the function + +```ignore +#[mlua::lua_module(name = "alt_module")] +fn my_module(lua: &Lua) -> Result
{ + ... +} +``` + +* skip_memory_check - skip memory allocation checks for some operations. + +In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory +limits or not. As a result, some operations that require memory allocation run in protected +mode. Setting this attribute will improve performance of such operations with risk of having +uncaught exceptions and memory leaks. + +```ignore +#[mlua::lua_module(skip_memory_check)] +fn my_module(lua: &Lua) -> Result
{ + ... +} +``` diff --git a/src/lib.rs b/src/lib.rs index d18e8460..2b079f32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,47 +204,7 @@ pub use mlua_derive::UserData; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::userdata_impl; -/// Registers Lua module entrypoint. -/// -/// You can register multiple entrypoints as required. -/// -/// ```ignore -/// use mlua::{Lua, Result, Table}; -/// -/// #[mlua::lua_module] -/// fn my_module(lua: &Lua) -> Result
{ -/// let exports = lua.create_table()?; -/// exports.set("hello", "world")?; -/// Ok(exports) -/// } -/// ``` -/// -/// Internally in the code above the compiler defines C function `luaopen_my_module`. -/// -/// You can also pass options to the attribute: -/// -/// * name - name of the module, defaults to the name of the function -/// -/// ```ignore -/// #[mlua::lua_module(name = "alt_module")] -/// fn my_module(lua: &Lua) -> Result
{ -/// ... -/// } -/// ``` -/// -/// * skip_memory_check - skip memory allocation checks for some operations. -/// -/// In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory -/// limits or not. As a result, some operations that require memory allocation run in protected -/// mode. Setting this attribute will improve performance of such operations with risk of having -/// uncaught exceptions and memory leaks. -/// -/// ```ignore -/// #[mlua::lua_module(skip_memory_check)] -/// fn my_module(lua: &Lua) -> Result
{ -/// ... -/// } -/// ``` +#[doc = include_str!("../docs/lua_module.md")] #[cfg(all(feature = "mlua_derive", any(feature = "module", doc)))] #[cfg_attr(docsrs, doc(cfg(feature = "module")))] pub use mlua_derive::lua_module; From fcab60bac449d1a86302d1ca2b54b1c58b59cf2b Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 12:28:06 +0100 Subject: [PATCH 076/138] mlua_derive: Fix `#[lua(meta, field)]` case --- mlua_derive/src/userdata/userdata_impl.rs | 2 +- tests/userdata_macro.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index d082bb49..88f0d5a1 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -356,7 +356,7 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let lua_name = lua_attr.name(fn_name); if lua_attr.meta { let tokens = quote! { - registry.add_meta_field(#lua_name, #type_path::#fn_name); + registry.add_meta_field(#lua_name, #type_path::#fn_name()); }; registration_calls.push(with_cfg(tokens, &method.attrs)); } else { diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index 169c9247..ec802f34 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -79,6 +79,11 @@ impl Rectangle { } } + #[lua(meta, field, name = "__answer")] + fn answer() -> u32 { + 42 + } + #[lua(skip)] #[allow(unused)] fn helper() -> u32 { @@ -124,7 +129,7 @@ impl Rectangle { } fn make_lua() -> Lua { - let lua = Lua::new(); + let lua = unsafe { Lua::unsafe_new() }; lua.globals() .set("Rectangle", lua.create_proxy::().unwrap()) .unwrap(); @@ -193,6 +198,12 @@ fn test_rectangle() { assert(other.length == 7, "other length should be 7 after transfer") assert(other:double_length() == 14, "double_length should be 14") + -- meta field + if _VERSION:match("Lua ") then + local mt = debug.getmetatable(rect) + assert(mt.__answer == 42, "__answer meta field should be 42") + end + assert(rect.lua_version == _VERSION, "lua_version should match Lua's _VERSION") -- Consuming method From ae88e8acf8cd0daf1c5b4f7598b9e8408cf50194 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 12:31:35 +0100 Subject: [PATCH 077/138] mlua_derive: Detect (and reject) all generic type parameters in UserData derive --- mlua_derive/src/userdata/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index 4cbfd4ec..a4a0d3dd 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -61,9 +61,9 @@ pub fn userdata_type(item: TokenStream) -> TokenStream { } }; - // Check for generic type parameters (not supported) - let has_type_params = input.generics.type_params().next().is_some(); - if has_type_params { + // Check for generic parameters (not supported) + let has_generics = !input.generics.params.is_empty(); + if has_generics { return Error::new_spanned( &input.generics, "`#[derive(UserData)]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead." From e6d16815d75d0a5d836709b00f69515d118d21d0 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 12:56:15 +0100 Subject: [PATCH 078/138] mlua_derive: Improve context-aware validation for `#[lua]` attr --- mlua_derive/src/userdata/mod.rs | 21 +++++++++++++++++++++ mlua_derive/src/userdata/userdata_impl.rs | 16 +++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index a4a0d3dd..dd010fb0 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -2,7 +2,9 @@ mod attr; pub(crate) mod userdata_impl; use proc_macro::TokenStream; +use proc_macro2::Span; use quote::{format_ident, quote}; +use syn::spanned::Spanned; use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; use self::attr::LuaAttr; @@ -31,6 +33,7 @@ fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { match &attr.meta { Meta::List(_) => { attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + validate_field_lua_attr(&lua_attr, attr.span())?; } Meta::Path(_) => {} Meta::NameValue(_) => { @@ -44,6 +47,24 @@ fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { Ok(lua_attr) } +fn validate_field_lua_attr(attr: &LuaAttr, span: Span) -> syn::Result<()> { + for (set, name) in [ + (attr.getter, "getter"), + (attr.setter, "setter"), + (attr.field, "field"), + (attr.meta, "meta"), + (attr.infallible, "infallible"), + ] { + if set { + return Err(syn::Error::new( + span, + format!("`{name}` is not valid for struct fields"), + )); + } + } + Ok(()) +} + pub fn userdata_type(item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); let type_name = &input.ident; diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 88f0d5a1..87d6704c 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -1,8 +1,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; +use syn::spanned::Spanned; use syn::{ Attribute, FnArg, Ident, ImplItem, ItemImpl, Meta, Signature, Type, parse_macro_input, parse_quote, }; @@ -183,6 +184,7 @@ fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { match &attr.meta { Meta::List(_) => { attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + validate_lua_attr(&lua_attr, attr.span())?; } Meta::Path(_) => {} Meta::NameValue(_) => { @@ -196,6 +198,18 @@ fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { Ok(lua_attr) } +fn validate_lua_attr(attr: &LuaAttr, span: Span) -> syn::Result<()> { + for (set, name) in [(attr.get, "get"), (attr.set, "set")] { + if set { + return Err(syn::Error::new( + span, + format!("`{name}` is not valid for methods"), + )); + } + } + Ok(()) +} + pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { if !attr.is_empty() { return syn::Error::new_spanned( From e263220fb3a2dee0be16d38e6474e73e60259ab5 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 13:05:03 +0100 Subject: [PATCH 079/138] Add documentation for `#derive(UserData)` --- docs/UserData.md | 149 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 +--- 2 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 docs/UserData.md diff --git a/docs/UserData.md b/docs/UserData.md new file mode 100644 index 00000000..20bf1673 --- /dev/null +++ b/docs/UserData.md @@ -0,0 +1,149 @@ +Implements the [`UserData`] trait for a Rust type. + +This derive macro generates an implementation of [`UserData`] that exposes +struct fields to Lua and integrates with `#[mlua::userdata_impl]` for +registering methods. + +Named fields are exposed as readable and writable fields in Lua by default. +Use `#[lua(...)]` on individual fields or methods to control how they are +registered. + +```rust +use mlua::{Lua, Result, UserData}; + +#[derive(UserData)] +struct Rectangle { + length: u32, + width: u32, +} + +#[mlua::userdata_impl] +impl Rectangle { + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Self { length, width } + } + + #[lua(getter, name = "area", infallible)] + fn calculate_area(&self) -> u32 { + self.length * self.width + } + + fn diagonal(&self) -> Result { + Ok(((self.length.pow(2) + self.width.pow(2)) as f64).sqrt()) + } +} +``` + +# Struct field attributes + +Each named field can be annotated with `#[lua(...)]`: + +| Attribute | Description | +| -------------- | ----------------------------------------------------- | +| `get` | Expose a getter. The field becomes readable from Lua. | +| `set` | Expose a setter. The field becomes writable from Lua. | +| `skip` | Do not expose this field. | +| `name = "..."` | Override the Lua-facing name for the field. | + +If neither `get` nor `set` is specified, both are enabled. + +Fields exposed as readable (via `get` or by default) must implement `Clone`. +The generated getter clones the field value when accessed from Lua. + +# Methods registration + +Use `#[mlua::userdata_impl]` on an `impl` block to register methods, +metamethods, and constants. All public items in the block are registered +automatically. + +## Method detection + +The receiver type determines how a method is registered: + +| Receiver | Registration | +| ----------- | ----------------- | +| `&self` | `add_method` | +| `&mut self` | `add_method_mut` | +| `self` | `add_method_once` | +| None | `add_function` | + +A first parameter of type `&Lua` (or `&mlua::Lua`) is treated as the +Lua state reference and passed automatically. + +## Method and constant attributes + +Each item in the impl block can be annotated with `#[lua(...)]`: + +| Attribute | Applies to | Description | +| -------------- | ------------------ | -------------------------------------------------------------------------------------- | +| `skip` | Methods, constants | Exclude this item from registration. | +| `name = "..."` | Methods, constants | Override the Lua-facing name. | +| `infallible` | Methods | Wrap the return value in `Ok(...)`. | +| `getter` | Methods | Register as a field getter. Must take `&self` and no Lua-facing arguments. | +| `setter` | Methods | Register as a field setter. Must take `&[mut] self` and one value argument. | +| `field` | Methods, constants | Register as a static field. Methods must take no receiver and no Lua-facing arguments. | +| `meta` | Methods, constants | Register as a metamethod. May be combined with `field` for meta static fields. | + +At most one of `getter`, `setter`, `field` may be specified on a method. + +## Constants + +Constants in an `#[mlua::userdata_impl]` block are registered as static +fields: + +```rust +#[mlua::userdata_impl] +impl MyType { + const VERSION: &str = "1.0"; + const COUNT: u32 = 42; +} +``` + +Use `#[lua(meta)]` on a constant to register it as a meta static field. + +## Metamethods + +Annotate a method with `#[lua(meta)]` to register it as a Lua metamethod. +The metamethod name is inferred from the function name when it starts with +`__`. Use `name = "..."` to specify the name explicitly. + +```rust +#[mlua::userdata_impl] +impl MyType { + #[lua(meta, infallible)] + fn __add(&self, other: &Self) -> Self { ... } + + #[lua(meta, name = "__call", infallible)] + fn construct(lua: &Lua, value: u32) -> Self { ... } +} +``` + +## Reference parameters + +Reference parameters in method signatures are automatically mapped to +the appropriate callback wrapper types: + +| Parameter type | Callback type | +| -------------- | ------------------- | +| `&str` | `BorrowedStr` | +| `&[u8]` | `BorrowedBytes` | +| `&T` | `UserDataRef` | +| `&mut T` | `UserDataRefMut` | + +## Async methods + +Async methods are supported and registered via the corresponding async +variants (`add_async_method`, `add_async_method_mut`, etc.). + +# Limitations + +Generics are not supported. Wrap a generic type in a concrete newtype +instead. + +Union types cannot derive `UserData`. + +Enum types are accepted but generate no field registrations. All method +registration must be done via `#[mlua::userdata_impl]`. + +[`UserData`]: crate::UserData diff --git a/src/lib.rs b/src/lib.rs index 2b079f32..02197144 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,20 +186,12 @@ pub use mlua_derive::chunk; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::FromLua; -/// Derive macro for implementing [`UserData`] for a Rust type. -/// -/// Named fields are auto-exposed as get/set. -/// -/// Use `#[lua(...)]` to customize. +#[doc = include_str!("../docs/UserData.md")] #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::UserData; -/// Attribute macro for exposing impl block methods to Lua userdata. -/// -/// All methods and constants are auto-exposed. -/// -/// Use `#[lua(...)]` to customize. +#[doc(hidden)] #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::userdata_impl; From e9271d2e32e53d9700464976515482ee881a3474 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 13:17:40 +0100 Subject: [PATCH 080/138] mlua_derive: Use correct Span in metamethod name validation error --- mlua_derive/src/userdata/attr.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mlua_derive/src/userdata/attr.rs b/mlua_derive/src/userdata/attr.rs index b6457810..67ae9d72 100644 --- a/mlua_derive/src/userdata/attr.rs +++ b/mlua_derive/src/userdata/attr.rs @@ -1,4 +1,3 @@ -use proc_macro2::Span; use syn::meta::ParseNestedMeta; use syn::{Ident, LitStr, Result}; @@ -69,16 +68,16 @@ impl LuaAttr { /// /// If `name` is set via attribute, use it. Otherwise, if the function name /// starts with `__`, use that. Returns an error if neither is available. - pub(crate) fn effective_meta_name(&self, fn_name: &Ident) -> Result { + pub(crate) fn effective_meta_name(&self, fn_ident: &Ident) -> Result { if let Some(ref name) = self.name { return Ok(name.clone()); } - let fn_name = fn_name.to_string(); + let fn_name = fn_ident.to_string(); if fn_name.starts_with("__") { return Ok(fn_name); } Err(syn::Error::new( - Span::call_site(), + fn_ident.span(), format!( "could not infer metamethod name from `{fn_name}`, either add `name = \"...\"` to `#[lua(meta, ...)]` or prefix the function with `__`" ), From 8d1841f8cfc8c310b8124eaaa527088716257ccb Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 13:22:50 +0100 Subject: [PATCH 081/138] Fix tests (Luau 0.723 integer type) --- tests/luau.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/luau.rs b/tests/luau.rs index 6b5c9296..979a79d4 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -539,7 +539,7 @@ fn test_heap_dump() -> Result<()> { fn test_integer64_type() -> Result<()> { let lua = Lua::new(); - _ = Lua::set_fflag("LuauIntegerType", true); + _ = Lua::set_fflag("LuauIntegerType2", true); let integer_lib = lua.globals().get::
("integer")?; let n = integer_lib.call_function::("create", 42)?; From 38c05b850e9438a9500c52e0daad84ed89d1e1f6 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 13:32:33 +0100 Subject: [PATCH 082/138] mlua_derive: Improve error reporting for #[lua] attributes --- mlua_derive/src/userdata/attr.rs | 7 +++++++ mlua_derive/src/userdata/mod.rs | 8 ++++---- mlua_derive/src/userdata/userdata_impl.rs | 19 ++++++++++--------- tests/compile/userdata_const_getter.stderr | 6 +++--- tests/compile/userdata_getter_and_meta.stderr | 6 +++--- .../compile/userdata_getter_and_setter.stderr | 6 +++--- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/mlua_derive/src/userdata/attr.rs b/mlua_derive/src/userdata/attr.rs index 67ae9d72..c6ad4f30 100644 --- a/mlua_derive/src/userdata/attr.rs +++ b/mlua_derive/src/userdata/attr.rs @@ -1,3 +1,4 @@ +use proc_macro2::Span; use syn::meta::ParseNestedMeta; use syn::{Ident, LitStr, Result}; @@ -8,6 +9,7 @@ use syn::{Ident, LitStr, Result}; /// - Impl methods: `getter`, `setter`, `field`, `meta`, `infallible`, `name`, `skip` #[derive(Default)] pub(crate) struct LuaAttr { + pub(crate) span: Option, pub(crate) name: Option, pub(crate) infallible: bool, pub(crate) skip: bool, @@ -64,6 +66,11 @@ impl LuaAttr { self.name.clone().unwrap_or_else(|| ident.to_string()) } + /// Returns the span to use for error reporting. + pub(crate) fn span(&self) -> Span { + self.span.unwrap_or_else(Span::call_site) + } + /// Returns the effective Lua metamethod name. /// /// If `name` is set via attribute, use it. Otherwise, if the function name diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index dd010fb0..be155bca 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -2,7 +2,6 @@ mod attr; pub(crate) mod userdata_impl; use proc_macro::TokenStream; -use proc_macro2::Span; use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; @@ -32,8 +31,9 @@ fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { } match &attr.meta { Meta::List(_) => { + lua_attr.span = Some(attr.span()); attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; - validate_field_lua_attr(&lua_attr, attr.span())?; + validate_field_lua_attr(&lua_attr)?; } Meta::Path(_) => {} Meta::NameValue(_) => { @@ -47,7 +47,7 @@ fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { Ok(lua_attr) } -fn validate_field_lua_attr(attr: &LuaAttr, span: Span) -> syn::Result<()> { +fn validate_field_lua_attr(attr: &LuaAttr) -> syn::Result<()> { for (set, name) in [ (attr.getter, "getter"), (attr.setter, "setter"), @@ -57,7 +57,7 @@ fn validate_field_lua_attr(attr: &LuaAttr, span: Span) -> syn::Result<()> { ] { if set { return Err(syn::Error::new( - span, + attr.span(), format!("`{name}` is not valid for struct fields"), )); } diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 87d6704c..eae51980 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{ @@ -183,8 +183,9 @@ fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { } match &attr.meta { Meta::List(_) => { + lua_attr.span = Some(attr.span()); attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; - validate_lua_attr(&lua_attr, attr.span())?; + validate_lua_attr(&lua_attr)?; } Meta::Path(_) => {} Meta::NameValue(_) => { @@ -198,11 +199,11 @@ fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { Ok(lua_attr) } -fn validate_lua_attr(attr: &LuaAttr, span: Span) -> syn::Result<()> { +fn validate_lua_attr(attr: &LuaAttr) -> syn::Result<()> { for (set, name) in [(attr.get, "get"), (attr.set, "set")] { if set { return Err(syn::Error::new( - span, + attr.span(), format!("`{name}` is not valid for methods"), )); } @@ -250,8 +251,8 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { continue; } if lua_attr.getter || lua_attr.setter { - return syn::Error::new_spanned( - &const_item.ident, + return syn::Error::new( + lua_attr.span(), "const items do not support `getter` or `setter`", ) .to_compile_error() @@ -285,15 +286,15 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let primary = [lua_attr.getter, lua_attr.setter, lua_attr.field]; let primary_count = primary.iter().filter(|&&x| x).count(); if primary_count > 1 { - return syn::Error::new_spanned( - &method.sig, + return syn::Error::new( + lua_attr.span(), "at most one of `getter`, `setter`, `field` can be specified", ) .to_compile_error() .into(); } if lua_attr.meta && primary_count == 1 && !lua_attr.field { - return syn::Error::new_spanned(&method.sig, "`meta` can only be combined with `field`") + return syn::Error::new(lua_attr.span(), "`meta` can only be combined with `field`") .to_compile_error() .into(); } diff --git a/tests/compile/userdata_const_getter.stderr b/tests/compile/userdata_const_getter.stderr index b29f21a8..f005b009 100644 --- a/tests/compile/userdata_const_getter.stderr +++ b/tests/compile/userdata_const_getter.stderr @@ -1,5 +1,5 @@ error: const items do not support `getter` or `setter` - --> tests/compile/userdata_const_getter.rs:7:11 + --> tests/compile/userdata_const_getter.rs:6:5 | -7 | const X: u32 = 42; - | ^ +6 | #[lua(getter)] + | ^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_meta.stderr b/tests/compile/userdata_getter_and_meta.stderr index 68bd1bda..e8a0af7d 100644 --- a/tests/compile/userdata_getter_and_meta.stderr +++ b/tests/compile/userdata_getter_and_meta.stderr @@ -1,5 +1,5 @@ error: `meta` can only be combined with `field` - --> tests/compile/userdata_getter_and_meta.rs:7:5 + --> tests/compile/userdata_getter_and_meta.rs:6:5 | -7 | fn bar(&self) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +6 | #[lua(getter, meta)] + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_setter.stderr b/tests/compile/userdata_getter_and_setter.stderr index 1f15a33c..8f3709e4 100644 --- a/tests/compile/userdata_getter_and_setter.stderr +++ b/tests/compile/userdata_getter_and_setter.stderr @@ -1,5 +1,5 @@ error: at most one of `getter`, `setter`, `field` can be specified - --> tests/compile/userdata_getter_and_setter.rs:9:5 + --> tests/compile/userdata_getter_and_setter.rs:8:5 | -9 | fn x(&self) -> mlua::Result { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +8 | #[lua(getter, setter)] + | ^^^^^^^^^^^^^^^^^^^^^^ From 0849d05c83eb8f72cc4cf2834c80ab85f93bdff6 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 31 May 2026 13:44:24 +0100 Subject: [PATCH 083/138] mlua_derive: Update docs --- docs/UserData.md | 6 +++--- docs/chunk.md | 2 +- docs/lua_module.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/UserData.md b/docs/UserData.md index 20bf1673..f46f2144 100644 --- a/docs/UserData.md +++ b/docs/UserData.md @@ -8,7 +8,7 @@ Named fields are exposed as readable and writable fields in Lua by default. Use `#[lua(...)]` on individual fields or methods to control how they are registered. -```rust +```rust,ignore use mlua::{Lua, Result, UserData}; #[derive(UserData)] @@ -92,7 +92,7 @@ At most one of `getter`, `setter`, `field` may be specified on a method. Constants in an `#[mlua::userdata_impl]` block are registered as static fields: -```rust +```rust,ignore #[mlua::userdata_impl] impl MyType { const VERSION: &str = "1.0"; @@ -108,7 +108,7 @@ Annotate a method with `#[lua(meta)]` to register it as a Lua metamethod. The metamethod name is inferred from the function name when it starts with `__`. Use `name = "..."` to specify the name explicitly. -```rust +```rust,ignore #[mlua::userdata_impl] impl MyType { #[lua(meta, infallible)] diff --git a/docs/chunk.md b/docs/chunk.md index 43c06ae2..972048ef 100644 --- a/docs/chunk.md +++ b/docs/chunk.md @@ -7,7 +7,7 @@ User's Rust types needs to implement [`UserData`] or [`IntoLua`] traits. Captured variables are **moved** into the chunk. -``` +```rust use mlua::{Lua, Result, chunk}; fn main() -> Result<()> { diff --git a/docs/lua_module.md b/docs/lua_module.md index 9156df99..5c5e84e6 100644 --- a/docs/lua_module.md +++ b/docs/lua_module.md @@ -2,7 +2,7 @@ Registers Lua module entrypoint. You can register multiple entrypoints as required. -```ignore +```rust,ignore use mlua::{Lua, Result, Table}; #[mlua::lua_module] @@ -19,7 +19,7 @@ You can also pass options to the attribute: * name - name of the module, defaults to the name of the function -```ignore +```rust,ignore #[mlua::lua_module(name = "alt_module")] fn my_module(lua: &Lua) -> Result
{ ... @@ -33,7 +33,7 @@ limits or not. As a result, some operations that require memory allocation run i mode. Setting this attribute will improve performance of such operations with risk of having uncaught exceptions and memory leaks. -```ignore +```rust,ignore #[mlua::lua_module(skip_memory_check)] fn my_module(lua: &Lua) -> Result
{ ... From 15fb63b2a24be7dd3cdeb55b1f762a9d213f0a69 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 1 Jun 2026 00:25:12 +0100 Subject: [PATCH 084/138] Support thread create/resume/yield callbacks for all Lua/Luau versions Introduce new `Lua::set_thread_event_callback` function to register callback for thread events. Thread collection callback is removed as unlikely very usefil and has too many limitations. --- src/lib.rs | 2 +- src/state.rs | 133 ++++++++++++++-------------- src/state/extra.rs | 15 ++-- src/state/raw.rs | 29 +++++- src/thread.rs | 154 +++++++++++++++++++++++++++++++- src/types.rs | 14 +-- tests/luau.rs | 85 +----------------- tests/thread.rs | 216 ++++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 467 insertions(+), 181 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 02197144..5472e28f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,7 +142,7 @@ pub use crate::string::LuaString as String; #[doc(hidden)] pub use crate::table::{TablePairs, TableSequence}; #[doc(hidden)] -pub use crate::thread::ThreadStatus; +pub use crate::thread::{ThreadEvent, ThreadStatus, ThreadTriggers}; #[doc(hidden)] pub use crate::userdata::{ MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataOwned, UserDataRef, diff --git a/src/state.rs b/src/state.rs index 7e624ec2..7c3fcfd8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -22,7 +22,7 @@ use crate::scope::Scope; use crate::stdlib::StdLib; use crate::string::LuaString; use crate::table::Table; -use crate::thread::Thread; +use crate::thread::{Thread, ThreadEvent, ThreadTriggers}; use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti}; use crate::types::{ AppDataRef, AppDataRefMut, ArcReentrantMutexGuard, Integer, LuaType, MaybeSend, MaybeSync, Number, @@ -842,92 +842,87 @@ impl Lua { } } - /// Sets a thread creation callback that will be called when a thread is created. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn set_thread_creation_callback(&self, callback: F) + /// Sets a callback invoked when thread lifecycle events occur. + /// + /// `triggers` controls which events trigger the callback, see [`ThreadTriggers`] for more + /// details. + /// + /// Only one callback can be registered at a time. Calling this again replaces the previous + /// callback and its triggers. + /// + /// # Example + /// + /// Subscribe only to yield events: + /// + /// ``` + /// # use mlua::{Lua, Result, ThreadTriggers, ThreadEvent}; + /// # fn main() -> Result<()> { + /// let lua = Lua::new(); + /// lua.set_thread_event_callback( + /// ThreadTriggers::ON_YIELD, + /// |_lua, event| { + /// if let ThreadEvent::Yield(thread) = event { + /// println!("thread yielded"); + /// } + /// Ok(()) + /// }, + /// ); + /// # Ok(()) + /// # } + /// ``` + pub fn set_thread_event_callback(&self, triggers: ThreadTriggers, callback: F) where - F: Fn(&Lua, Thread) -> Result<()> + MaybeSend + 'static, + F: Fn(&Lua, ThreadEvent) -> Result<()> + MaybeSend + 'static, { let lua = self.lock(); unsafe { - (*lua.extra.get()).thread_creation_callback = Some(XRc::new(callback)); - (*ffi::lua_callbacks(lua.main_state())).userthread = Some(Self::userthread_proc); + (*lua.extra.get()).thread_triggers = triggers; + (*lua.extra.get()).thread_event_callback = Some(XRc::new(callback)); + #[cfg(feature = "luau")] + { + let proc = Self::userthread_proc as _; + (*ffi::lua_callbacks(lua.main_state())).userthread = triggers.on_create.then(|| proc); + } } } - /// Sets a thread collection callback that will be called when a thread is destroyed. + /// Removes the thread event callback previously set by [`Lua::set_thread_event_callback`]. /// - /// Luau GC does not support exceptions during collection, so the callback must be - /// non-panicking. If the callback panics, the program will be aborted. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn set_thread_collection_callback(&self, callback: F) - where - F: Fn(crate::LightUserData) + MaybeSend + 'static, - { + /// This function has no effect if a callback was not previously set. + pub fn remove_thread_event_callback(&self) { let lua = self.lock(); + let extra = lua.extra.get(); unsafe { - (*lua.extra.get()).thread_collection_callback = Some(XRc::new(callback)); - (*ffi::lua_callbacks(lua.main_state())).userthread = Some(Self::userthread_proc); + (*extra).thread_triggers = ThreadTriggers::new(); + (*extra).thread_event_callback = None; + #[cfg(feature = "luau")] + { + (*ffi::lua_callbacks(lua.main_state())).userthread = None; + } } } #[cfg(feature = "luau")] unsafe extern "C-unwind" fn userthread_proc(parent: *mut ffi::lua_State, child: *mut ffi::lua_State) { - let extra = ExtraData::get(child); - if !parent.is_null() { - // Thread is created - let callback = match (*extra).thread_creation_callback { - Some(ref cb) => cb.clone(), - None => return, - }; - if XRc::strong_count(&callback) > 2 { - return; // Don't allow recursion - } - ffi::lua_pushthread(child); - ffi::lua_xmove(child, (*extra).ref_thread, 1); - let value = Thread((*extra).raw_lua().pop_ref_thread(), child); - callback_error_ext(parent, extra, false, move |extra, _| { - callback((*extra).lua(), value) - }) - } else { - // Thread is about to be collected - let callback = match (*extra).thread_collection_callback { - Some(ref cb) => cb.clone(), - None => return, - }; - - // We need to wrap the callback call in non-unwind function as it's not safe to unwind when - // Luau GC is running. - // This will trigger `abort()` if the callback panics. - unsafe extern "C" fn run_callback( - callback: *const crate::types::ThreadCollectionCallback, - value: *mut ffi::lua_State, - ) { - (*callback)(crate::LightUserData(value as _)); - } - - (*extra).running_gc = true; - run_callback(&callback, child); - (*extra).running_gc = false; + // Only handle thread creation + if parent.is_null() { + return; } - } - /// Removes any thread creation or collection callbacks previously set by - /// [`Lua::set_thread_creation_callback`] or [`Lua::set_thread_collection_callback`]. - /// - /// This function has no effect if a thread callbacks were not previously set. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn remove_thread_callbacks(&self) { - let lua = self.lock(); - unsafe { - let extra = lua.extra.get(); - (*extra).thread_creation_callback = None; - (*extra).thread_collection_callback = None; - (*ffi::lua_callbacks(lua.main_state())).userthread = None; + let extra = ExtraData::get(child); + if !(*extra).thread_triggers.on_create { + return; } + let callback = match &(*extra).thread_event_callback { + Some(cb) if XRc::strong_count(cb) == 1 => cb.clone(), + _ => return, + }; + ffi::lua_pushthread(child); + ffi::lua_xmove(child, (*extra).ref_thread, 1); + let thread = Thread((*extra).raw_lua().pop_ref_thread(), child); + callback_error_ext(parent, extra, false, move |extra, _| { + callback((*extra).lua(), ThreadEvent::Create(thread)) + }) } /// Sets the warning function to be used by Lua to emit warnings. diff --git a/src/state/extra.rs b/src/state/extra.rs index d761c9cb..9889e63c 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -12,7 +12,8 @@ use rustc_hash::FxHashMap; use crate::error::Result; use crate::state::RawLua; use crate::stdlib::StdLib; -use crate::types::{AppData, ReentrantMutex, XRc}; +use crate::thread::ThreadTriggers; +use crate::types::{AppData, ReentrantMutex, ThreadEventCallback, XRc}; use crate::userdata::RawUserDataRegistry; use crate::util::{TypeKey, WrappedFailure, get_internal_metatable, push_internal_userdata}; @@ -81,10 +82,8 @@ pub(crate) struct ExtraData { pub(super) warn_callback: Option, #[cfg(feature = "luau")] pub(super) interrupt_callback: Option, - #[cfg(feature = "luau")] - pub(super) thread_creation_callback: Option, - #[cfg(feature = "luau")] - pub(super) thread_collection_callback: Option, + pub(super) thread_triggers: ThreadTriggers, + pub(super) thread_event_callback: Option, #[cfg(feature = "luau")] pub(crate) running_gc: bool, @@ -186,10 +185,8 @@ impl ExtraData { warn_callback: None, #[cfg(feature = "luau")] interrupt_callback: None, - #[cfg(feature = "luau")] - thread_creation_callback: None, - #[cfg(feature = "luau")] - thread_collection_callback: None, + thread_triggers: ThreadTriggers::default(), + thread_event_callback: None, #[cfg(feature = "luau")] sandboxed: false, #[cfg(feature = "luau")] diff --git a/src/state/raw.rs b/src/state/raw.rs index 06b9bfae..c6a6f9a9 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -15,11 +15,11 @@ use crate::state::util::callback_error_ext; use crate::stdlib::StdLib; use crate::string::LuaString; use crate::table::Table; -use crate::thread::Thread; +use crate::thread::{Thread, ThreadTriggers}; use crate::traits::{FromLua, IntoLua}; use crate::types::{ AppDataRef, AppDataRefMut, Callback, CallbackUpvalue, DestructedUserdata, Integer, LightUserData, - LuaType, MaybeSend, ReentrantMutex, RegistryKey, ValueRef, XRc, + LuaType, MaybeSend, ReentrantMutex, RegistryKey, ThreadEventCallback, ValueRef, XRc, }; use crate::userdata::{ AnyUserData, MetaMethod, RawUserDataRegistry, UserData, UserDataRegistry, UserDataStorage, @@ -643,7 +643,7 @@ impl RawLua { let protect = !self.unlikely_memory_error(); #[cfg(feature = "luau")] - let protect = protect || (*self.extra.get()).thread_creation_callback.is_some(); + let protect = protect || self.thread_event_triggers().on_create; let thread_state = if !protect { ffi::lua_newthread(state) @@ -656,6 +656,19 @@ impl RawLua { self.set_thread_hook(thread_state, HookKind::Global)?; let thread = Thread(self.pop_ref(), thread_state); + + // Exec creation callback for non-Luau (Luau handles this via `userthread_proc`) + #[cfg(not(feature = "luau"))] + if self.thread_event_triggers().on_create { + let extra = self.extra.get(); + if let Some(ref cb) = (*extra).thread_event_callback + && XRc::strong_count(cb) == 1 + { + let cb = cb.clone(); + cb((*extra).lua(), crate::thread::ThreadEvent::Create(thread.clone()))?; + } + } + ffi::lua_xpush(self.ref_thread(), thread_state, func.0.index); Ok(thread) } @@ -691,6 +704,16 @@ impl RawLua { } } + #[inline(always)] + pub(crate) unsafe fn thread_event_triggers(&self) -> ThreadTriggers { + (*self.extra.get()).thread_triggers + } + + #[inline(always)] + pub(crate) unsafe fn thread_event_callback(&self) -> Option { + (*self.extra.get()).thread_event_callback.clone() + } + /// Pushes a primitive type value onto the Lua stack. pub(crate) unsafe fn push_primitive_type(&self) -> bool { match T::TYPE_ID { diff --git a/src/thread.rs b/src/thread.rs index 65d209ed..da7c9881 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -42,7 +42,7 @@ use crate::error::{Error, Result}; use crate::function::Function; use crate::state::RawLua; use crate::traits::{FromLuaMulti, IntoLuaMulti}; -use crate::types::{LuaType, ValueRef}; +use crate::types::{LuaType, ValueRef, XRc}; use crate::util::{StackGuard, check_stack, error_traceback_thread, pop_error}; #[cfg(not(feature = "luau"))] @@ -63,6 +63,85 @@ use { }, }; +/// Controls which thread lifecycle events trigger the callback. +#[derive(Clone, Copy, Debug, Default)] +#[non_exhaustive] +pub struct ThreadTriggers { + /// Trigger the callback when a new thread is created. + pub on_create: bool, + /// Trigger the callback before a thread is resumed (via [`Thread::resume`]). + pub on_resume: bool, + /// Trigger the callback after a thread yields. + pub on_yield: bool, +} + +impl ThreadTriggers { + /// An instance of [`ThreadTriggers`] with `on_create` trigger set. + pub const ON_CREATE: Self = Self::new().on_create(); + + /// An instance of [`ThreadTriggers`] with `on_resume` trigger set. + pub const ON_RESUME: Self = Self::new().on_resume(); + + /// An instance of [`ThreadTriggers`] with `on_yield` trigger set. + pub const ON_YIELD: Self = Self::new().on_yield(); + + /// Returns a new instance of `ThreadTriggers` with all triggers disabled. + pub const fn new() -> Self { + Self { + on_create: false, + on_resume: false, + on_yield: false, + } + } + + /// Returns an instance of `ThreadTriggers` with `on_create` trigger set. + pub const fn on_create(mut self) -> Self { + self.on_create = true; + self + } + + /// Returns an instance of `ThreadTriggers` with `on_resume` trigger set. + pub const fn on_resume(mut self) -> Self { + self.on_resume = true; + self + } + + /// Returns an instance of `ThreadTriggers` with `on_yield` trigger set. + pub const fn on_yield(mut self) -> Self { + self.on_yield = true; + self + } +} + +impl std::ops::BitOr for ThreadTriggers { + type Output = Self; + + fn bitor(mut self, rhs: Self) -> Self::Output { + self.on_create |= rhs.on_create; + self.on_resume |= rhs.on_resume; + self.on_yield |= rhs.on_yield; + self + } +} + +impl std::ops::BitOrAssign for ThreadTriggers { + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +/// Represents a thread (coroutine) event. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ThreadEvent { + /// A new thread was created. + Create(Thread), + /// A thread is about to be resumed via [`Thread::resume`]. + Resume(Thread), + /// A thread has just yielded. + Yield(Thread), +} + /// Status of a Lua thread (coroutine). #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ThreadStatus { @@ -98,7 +177,6 @@ impl ThreadStatusInner { matches!(self, ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_)) } - #[cfg(feature = "async")] #[inline(always)] fn is_yielded(self) -> bool { matches!(self, ThreadStatusInner::Yielded(_)) @@ -193,6 +271,14 @@ impl Thread { unsafe { let _sg = StackGuard::new(state); + // Exec thread resume callback + if lua.thread_event_triggers().on_resume + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Resume(self.clone()))?; + } + let nargs = args.push_into_stack_multi(&lua)?; if nargs > 0 { check_stack(thread_state, nargs)?; @@ -201,7 +287,17 @@ impl Thread { } let _thread_sg = StackGuard::with_top(thread_state, 0); - let (_, nresults) = self.resume_inner(&lua, pushed_nargs)?; + let (status, nresults) = self.resume_inner(&lua, pushed_nargs)?; + + // Exec thread yield callback + if lua.thread_event_triggers().on_yield + && status.is_yielded() + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Yield(self.clone()))?; + } + check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); @@ -229,12 +325,30 @@ impl Thread { unsafe { let _sg = StackGuard::new(state); + // Exec thread resume callback + if lua.thread_event_triggers().on_resume + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Resume(self.clone()))?; + } + check_stack(state, 1)?; error.push_into_stack(&lua)?; ffi::lua_xmove(state, thread_state, 1); let _thread_sg = StackGuard::with_top(thread_state, 0); - let (_, nresults) = self.resume_inner(&lua, ffi::LUA_RESUMEERROR)?; + let (status, nresults) = self.resume_inner(&lua, ffi::LUA_RESUMEERROR)?; + + // Exec thread yield callback + if lua.thread_event_triggers().on_yield + && status.is_yielded() + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Yield(self.clone()))?; + } + check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); @@ -622,9 +736,25 @@ impl Stream for AsyncThread { let _thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); + // Exec thread resume callback + if lua.thread_event_triggers().on_resume + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Resume(self.thread.clone()))?; + } + let (status, nresults) = (self.thread).resume_inner(&lua, nargs)?; if status.is_yielded() { + // Exec thread yield callback + if lua.thread_event_triggers().on_yield + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Yield(self.thread.clone()))?; + } + if nresults == 1 && is_poll_pending(thread_state) { return Poll::Pending; } @@ -658,9 +788,25 @@ impl Future for AsyncThread { let _thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); + // Exec thread resume callback + if lua.thread_event_triggers().on_resume + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Resume(self.thread.clone()))?; + } + let (status, nresults) = self.thread.resume_inner(&lua, nargs)?; if status.is_yielded() { + // Exec thread yield callback + if lua.thread_event_triggers().on_yield + && let Some(cb) = lua.thread_event_callback() + && XRc::strong_count(&cb) <= 2 + { + cb(lua.lua(), ThreadEvent::Yield(self.thread.clone()))?; + } + if !(nresults == 1 && is_poll_pending(thread_state)) { // Ignore values returned via yield() cx.waker().wake_by_ref(); diff --git a/src/types.rs b/src/types.rs index a6a91030..06ecf277 100644 --- a/src/types.rs +++ b/src/types.rs @@ -96,17 +96,11 @@ pub(crate) type InterruptCallback = XRc Result + Send>; #[cfg(all(not(feature = "send"), feature = "luau"))] pub(crate) type InterruptCallback = XRc Result>; -#[cfg(all(feature = "send", feature = "luau"))] -pub(crate) type ThreadCreationCallback = XRc Result<()> + Send>; - -#[cfg(all(not(feature = "send"), feature = "luau"))] -pub(crate) type ThreadCreationCallback = XRc Result<()>>; - -#[cfg(all(feature = "send", feature = "luau"))] -pub(crate) type ThreadCollectionCallback = XRc; +#[cfg(feature = "send")] +pub(crate) type ThreadEventCallback = XRc Result<()> + Send>; -#[cfg(all(not(feature = "send"), feature = "luau"))] -pub(crate) type ThreadCollectionCallback = XRc; +#[cfg(not(feature = "send"))] +pub(crate) type ThreadEventCallback = XRc Result<()>>; #[cfg(feature = "send")] #[cfg(any(feature = "lua55", feature = "lua54"))] diff --git a/tests/luau.rs b/tests/luau.rs index 979a79d4..770640b9 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -1,10 +1,8 @@ #![cfg(feature = "luau")] -use std::cell::Cell; use std::fmt::Debug; -use std::os::raw::c_void; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; use mlua::{ Compiler, Error, Function, Lua, LuaOptions, ObjectLike, Result, StdLib, Table, Value, Vector, VmState, @@ -359,87 +357,6 @@ fn test_fflags() { assert!(Lua::set_fflag("UnknownFlag", true).is_err()); } -#[test] -fn test_thread_events() -> Result<()> { - let lua = Lua::new(); - - let count = Arc::new(AtomicU64::new(0)); - let thread_data: Arc<(AtomicPtr, AtomicBool)> = Arc::new(Default::default()); - - let (count2, thread_data2) = (count.clone(), thread_data.clone()); - lua.set_thread_creation_callback(move |_, thread| { - count2.fetch_add(1, Ordering::Relaxed); - (thread_data2.0).store(thread.to_pointer() as *mut _, Ordering::Relaxed); - thread_data2.1.store(false, Ordering::Relaxed); - Ok(()) - }); - let (count3, thread_data3) = (count.clone(), thread_data.clone()); - lua.set_thread_collection_callback(move |thread_ptr| { - count3.fetch_add(1, Ordering::Relaxed); - if thread_data3.0.load(Ordering::Relaxed) == thread_ptr.0 { - thread_data3.1.store(true, Ordering::Relaxed); - } - }); - - let t = lua.create_thread(lua.load("return 123").into_function()?)?; - assert_eq!(count.load(Ordering::Relaxed), 1); - let t_ptr = t.to_pointer(); - assert_eq!(t_ptr, thread_data.0.load(Ordering::Relaxed)); - assert!(!thread_data.1.load(Ordering::Relaxed)); - - // Thead will be destroyed after GC cycle - drop(t); - lua.gc_collect()?; - assert_eq!(count.load(Ordering::Relaxed), 2); - assert_eq!(t_ptr, thread_data.0.load(Ordering::Relaxed)); - assert!(thread_data.1.load(Ordering::Relaxed)); - - // Check that recursion is not allowed - let count4 = count.clone(); - lua.set_thread_creation_callback(move |lua, _value| { - count4.fetch_add(1, Ordering::Relaxed); - let _ = lua.create_thread(lua.load("return 123").into_function().unwrap())?; - Ok(()) - }); - let t = lua.create_thread(lua.load("return 123").into_function()?)?; - assert_eq!(count.load(Ordering::Relaxed), 3); - - lua.remove_thread_callbacks(); - drop(t); - lua.gc_collect()?; - assert_eq!(count.load(Ordering::Relaxed), 3); - - // Test error inside callback - lua.set_thread_creation_callback(move |_, _| Err(Error::runtime("error when processing thread event"))); - let result = lua.create_thread(lua.load("return 123").into_function()?); - assert!(result.is_err()); - assert!( - matches!(result, Err(Error::RuntimeError(err)) if err.contains("error when processing thread event")) - ); - - // Test context switch when running Lua script - let count = Cell::new(0); - lua.set_thread_creation_callback(move |_, _| { - count.set(count.get() + 1); - if count.get() == 2 { - return Err(Error::runtime("thread limit exceeded")); - } - Ok(()) - }); - let result = lua - .load( - r#" - local co = coroutine.wrap(function() return coroutine.create(print) end) - co() - "#, - ) - .exec(); - assert!(result.is_err()); - assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("thread limit exceeded"))); - - Ok(()) -} - #[test] fn test_loadstring() -> Result<()> { let lua = Lua::new(); diff --git a/tests/thread.rs b/tests/thread.rs index 98b861f8..d0afe433 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -1,6 +1,8 @@ use std::panic::catch_unwind; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use mlua::{Error, Function, IntoLua, Lua, Result, Thread, Value}; +use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadEvent, ThreadTriggers, Value}; #[test] fn test_thread() -> Result<()> { @@ -275,3 +277,215 @@ fn test_thread_resume_bad_arg() -> Result<()> { Ok(()) } + +#[test] +fn test_thread_event_create() -> Result<()> { + let lua = Lua::new(); + + let created = Arc::new(AtomicBool::new(false)); + let created2 = created.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Create(_))); + created2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let _thread = lua.create_thread(lua.create_function(|_, ()| Ok(()))?)?; + assert!(created.load(Ordering::Relaxed)); + + Ok(()) +} + +#[test] +fn test_thread_event_create_recursive() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicU32::new(0)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |lua, event| { + assert!(matches!(event, ThreadEvent::Create(_))); + count2.fetch_add(1, Ordering::Relaxed); + // Creating a thread inside the callback + let _ = lua.create_thread(lua.load("return 321").into_function().unwrap())?; + Ok(()) + }); + + let _t = lua.create_thread(lua.load("return 123").into_function()?)?; + assert_eq!(count.load(Ordering::Relaxed), 1); + + Ok(()) +} + +#[test] +fn test_thread_event_create_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_, _| Err(Error::runtime("blah"))); + + let result = lua.create_thread(lua.load("return 123").into_function()?); + assert!(result.is_err()); + assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("blah"))); + + Ok(()) +} + +#[test] +fn test_thread_event_resume() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicBool::new(false)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Resume(_))); + count2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + thread.resume::<()>(())?; + + assert!(count.load(Ordering::Relaxed)); + Ok(()) +} + +#[test] +fn test_thread_event_resume_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, _event| { + Err(Error::runtime("abort resume")) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + let err = thread.resume::<()>(()).unwrap_err(); + assert!(matches!(err, Error::RuntimeError(msg) if msg == "abort resume")); + assert!(thread.is_resumable()); + + Ok(()) +} + +#[test] +fn test_thread_event_yield() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicBool::new(false)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Yield(_))); + count2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1) return 2").into_function()?)?; + let val = thread.resume::(())?; + assert_eq!(val, 1); + assert!(count.load(Ordering::Relaxed)); + + // Reset flag and resume to completion + count.store(false, Ordering::Relaxed); + let val = thread.resume::(())?; + assert_eq!(val, 2); + // Yield hook should not fire on the final return + assert!(!count.load(Ordering::Relaxed)); + assert!(thread.is_finished()); + + Ok(()) +} + +#[test] +fn test_thread_event_yield_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, _event| { + Err(Error::runtime("yield error")) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1)").into_function()?)?; + let err = thread.resume::<()>(()).unwrap_err(); + assert!(matches!(err, Error::RuntimeError(msg) if msg == "yield error")); + + Ok(()) +} + +#[test] +fn test_thread_event_swap() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicU32::new(0)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, _event| { + count2.fetch_add(1, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1) return 2").into_function()?)?; + thread.resume::(())?; + assert_eq!(count.load(Ordering::Relaxed), 1); + + // Replace callback with a new one + let count3 = Arc::new(AtomicU32::new(0)); + let count4 = count3.clone(); + lua.set_thread_event_callback(ThreadTriggers::new().on_resume(), move |_lua, _event| { + count4.fetch_add(10, Ordering::Relaxed); + Ok(()) + }); + + thread.resume::(())?; + assert_eq!(count.load(Ordering::Relaxed), 1); + assert_eq!(count3.load(Ordering::Relaxed), 10); + + // Remove callback + lua.remove_thread_event_callback(); + thread.reset(lua.load("return 0").into_function()?)?; + thread.resume::<()>(())?; + assert_eq!(count3.load(Ordering::Relaxed), 10); // unchanged + + Ok(()) +} + +#[cfg(feature = "luau")] +#[test] +fn test_thread_event_luau_resume_error() -> Result<()> { + let lua = Lua::new(); + + let fired = Arc::new(AtomicBool::new(false)); + let fired2 = fired.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Resume(_))); + fired2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + let _ = thread.resume_error::<()>("test error"); + assert!(fired.load(Ordering::Relaxed)); + + Ok(()) +} + +#[cfg(feature = "luau")] +#[test] +fn test_thread_event_create_from_lua() -> Result<()> { + let lua = Lua::new(); + + let count = std::cell::Cell::new(0); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_, _| { + count.set(count.get() + 1); + if count.get() == 2 { + return Err(Error::runtime("thread limit exceeded")); + } + Ok(()) + }); + let result = lua + .load( + r#" + local co = coroutine.wrap(function() return coroutine.create(print) end) + co() + "#, + ) + .exec(); + assert!(result.is_err()); + assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("thread limit exceeded"))); + + Ok(()) +} From 0b365e92a96bdc3b80c3c34286d14ba7fb83ce20 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 6 Jun 2026 13:18:55 +0100 Subject: [PATCH 085/138] Support `to_alias_override`/`to_alias_fallback` in `Require` trait (Luau) --- src/luau/require.rs | 46 ++++++++++++- tests/luau/require.rs | 146 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/luau/require.rs b/src/luau/require.rs index 3aee6f27..e6902a54 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -66,6 +66,24 @@ pub trait Require { /// configuration file. fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError>; + /// Provides an initial alias override opportunity prior to searching for + /// configuration files. + /// + /// If `Ok(())` is returned, alias resolution stops here and the internal state + /// must point at the aliased location. + fn to_alias_override(&mut self, _alias: &str) -> StdResult<(), NavigateError> { + Err(NavigateError::NotFound) + } + + /// Provides a final opportunity to resolve an alias if it cannot be found in + /// configuration files. + /// + /// If `Ok(())` is returned, alias resolution stops here and the internal state + /// must point at the aliased location. + fn to_alias_fallback(&mut self, _alias: &str) -> StdResult<(), NavigateError> { + Err(NavigateError::NotFound) + } + // Navigate to parent directory fn to_parent(&mut self) -> StdResult<(), NavigateError>; @@ -192,6 +210,30 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ }) } + unsafe extern "C-unwind" fn to_alias_override( + state: *mut ffi::lua_State, + ctx: *mut c_void, + alias_unprefixed: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let mut this = try_borrow_mut!(state, ctx); + let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy(); + callback_error_ext(state, ptr::null_mut(), true, move |_, _| { + this.to_alias_override(&alias).into_nav_result() + }) + } + + unsafe extern "C-unwind" fn to_alias_fallback( + state: *mut ffi::lua_State, + ctx: *mut c_void, + alias_unprefixed: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let mut this = try_borrow_mut!(state, ctx); + let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy(); + callback_error_ext(state, ptr::null_mut(), true, move |_, _| { + this.to_alias_fallback(&alias).into_nav_result() + }) + } + unsafe extern "C-unwind" fn to_parent( state: *mut ffi::lua_State, ctx: *mut c_void, @@ -298,8 +340,8 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ (*config).is_require_allowed = is_require_allowed; (*config).reset = reset; (*config).jump_to_alias = jump_to_alias; - (*config).to_alias_override = None; - (*config).to_alias_fallback = None; + (*config).to_alias_override = Some(to_alias_override); + (*config).to_alias_fallback = Some(to_alias_fallback); (*config).to_parent = to_parent; (*config).to_child = to_child; (*config).is_module_present = is_module_present; diff --git a/tests/luau/require.rs b/tests/luau/require.rs index eace354c..ebdfcb05 100644 --- a/tests/luau/require.rs +++ b/tests/luau/require.rs @@ -251,6 +251,152 @@ fn test_require_with_config_luau() { test_require_with_config_inner("with_config_luau"); } +#[test] +fn test_alias_override() { + let lua = Lua::new(); + + struct OverrideRequire(FsRequirer); + + impl Require for OverrideRequire { + fn is_require_allowed(&self, chunk_name: &str) -> bool { + self.0.is_require_allowed(chunk_name) + } + + fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError> { + self.0.reset(chunk_name) + } + + fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError> { + self.0.jump_to_alias(path) + } + + fn to_alias_override(&mut self, alias: &str) -> StdResult<(), NavigateError> { + if alias == "testoverride" { + self.0.jump_to_alias("./tests/luau/require/without_config") + } else { + Err(NavigateError::NotFound) + } + } + + fn to_parent(&mut self) -> StdResult<(), NavigateError> { + self.0.to_parent() + } + + fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError> { + self.0.to_child(name) + } + + fn has_module(&self) -> bool { + self.0.has_module() + } + + fn cache_key(&self) -> String { + self.0.cache_key() + } + + fn has_config(&self) -> bool { + self.0.has_config() + } + + fn config(&self) -> IoResult> { + self.0.config() + } + + fn loader(&self, lua: &Lua) -> Result { + self.0.loader(lua) + } + } + + let require_fn = lua + .create_require_function(OverrideRequire(FsRequirer::new())) + .unwrap(); + lua.globals().set("require", require_fn).unwrap(); + + // to_alias_override intercepts before config-file search + let res = run_require(&lua, "@testoverride/dependency").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // Different sub-path through the same alias + let res = run_require(&lua, "@testoverride/module").unwrap(); + assert_eq!("required into module", get_str(&res, 2)); + + // Aliases not handled by the override still fail normally + let res = run_require(&lua, "@unknown_alias_xyz/anything"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@unknown_alias_xyz is not a valid alias")); +} + +#[test] +fn test_alias_fallback() { + let lua = Lua::new(); + + struct FallbackRequire(FsRequirer); + + impl Require for FallbackRequire { + fn is_require_allowed(&self, chunk_name: &str) -> bool { + self.0.is_require_allowed(chunk_name) + } + + fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError> { + self.0.reset(chunk_name) + } + + fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError> { + self.0.jump_to_alias(path) + } + + fn to_alias_fallback(&mut self, alias: &str) -> StdResult<(), NavigateError> { + if alias == "testfallback" { + self.0.jump_to_alias("./tests/luau/require/without_config") + } else { + Err(NavigateError::NotFound) + } + } + + fn to_parent(&mut self) -> StdResult<(), NavigateError> { + self.0.to_parent() + } + + fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError> { + self.0.to_child(name) + } + + fn has_module(&self) -> bool { + self.0.has_module() + } + + fn cache_key(&self) -> String { + self.0.cache_key() + } + + fn has_config(&self) -> bool { + self.0.has_config() + } + + fn config(&self) -> IoResult> { + self.0.config() + } + + fn loader(&self, lua: &Lua) -> Result { + self.0.loader(lua) + } + } + + let require_fn = lua + .create_require_function(FallbackRequire(FsRequirer::new())) + .unwrap(); + lua.globals().set("require", require_fn).unwrap(); + + // to_alias_fallback catches after config-file search misses + let res = run_require(&lua, "@testfallback/dependency").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // Aliases not handled by the fallback still fail + let res = run_require(&lua, "@unknown_alias_xyz/anything"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@unknown_alias_xyz is not a valid alias")); +} + #[cfg(all(feature = "async", not(windows)))] #[tokio::test] async fn test_async_require() -> Result<()> { From 743325f7d65a46ba8a74bd5ad1bc4e23c178ba99 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 6 Jun 2026 14:28:17 +0100 Subject: [PATCH 086/138] clippy --- mlua_derive/src/userdata/userdata_impl.rs | 2 +- src/error.rs | 2 +- src/state.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index eae51980..4e995aa6 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -537,7 +537,7 @@ fn gen_field_getter( lua_attr: &LuaAttr, info: &MethodInfo, ) -> TokenStream2 { - let lua_name = lua_attr.name(&fn_name); + let lua_name = lua_attr.name(fn_name); let call_args = gen_call_args(info); if lua_attr.infallible { diff --git a/src/error.rs b/src/error.rs index 0664618a..6fd21ca6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -290,7 +290,7 @@ impl fmt::Display for Error { // Try to find local traceback within the full traceback if let Some(pos) = full_traceback.find(traceback) { write!(fmt, "{}", &full_traceback[..pos])?; - writeln!(fmt, ">{}", &full_traceback[pos..].trim_end())?; + writeln!(fmt, ">{}", full_traceback[pos..].trim_end())?; } else { writeln!(fmt, "{}", full_traceback.trim_end())?; } diff --git a/src/state.rs b/src/state.rs index 7c3fcfd8..eb177df1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -881,7 +881,7 @@ impl Lua { #[cfg(feature = "luau")] { let proc = Self::userthread_proc as _; - (*ffi::lua_callbacks(lua.main_state())).userthread = triggers.on_create.then(|| proc); + (*ffi::lua_callbacks(lua.main_state())).userthread = triggers.on_create.then_some(proc); } } } From 0711c614c74b333f01f83461b94f105835fbe27d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 6 Jun 2026 14:25:57 +0100 Subject: [PATCH 087/138] v0.12.0-rc.2 --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 4 ++-- mlua_derive/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8e8562..0719f119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v0.12.0-rc.2 (Jun 06, 2026) + +- Add `#[derive(UserData)]` and `#[mlua::userdata_impl]` macros +- Support thread create/resume/yield callbacks for all Lua versions (including Luau) +- Support `to_alias_override`/`to_alias_fallback` in `Require` trait (Luau) +- Prevent `XRc` overflow when dropping `RawLua` with foreign Lua state +- implement `Not` for `StdLib` (#699) +- Fix `String::to_pointer` return NULL in Lua <5.4 + ## v0.12.0-rc.1 (Apr 21, 2026) - Rust 2024 edition diff --git a/Cargo.toml b/Cargo.toml index 6d8f44a3..5646618f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua" -version = "0.12.0-rc.1" # remember to update mlua_derive +version = "0.12.0-rc.2" # remember to update mlua_derive authors = ["Aleksandr Orlenko ", "kyren "] rust-version = "1.88" edition = "2024" @@ -50,7 +50,7 @@ userdata-wrappers = ["parking_lot/send_guard"] serialize = ["serde"] [dependencies] -mlua_derive = { version = "=0.11.0", optional = true, path = "mlua_derive" } +mlua_derive = { version = "=0.12.0-rc.1", optional = true, path = "mlua_derive" } bstr = { version = "1.0", features = ["std"], default-features = false } either = "1.0" num-traits = { version = "0.2.14" } diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 7ba306a9..c49d7d39 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua_derive" -version = "0.11.0" +version = "0.12.0-rc.1" authors = ["Aleksandr Orlenko "] rust-version = "1.88" edition = "2024" From 94adff7b453303521238aeb03379d0f9a4793347 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 9 Jun 2026 17:07:28 -0400 Subject: [PATCH 088/138] Remove unmaintained proc-macro-error2 dependency (RUSTSEC-2026-0173) (#707) * Remove unmaintained proc-macro-error2 dependency (RUSTSEC-2026-0173) * Use syn::Error compile error instead of unreachable! for span_pos failure --- mlua_derive/Cargo.toml | 3 +- mlua_derive/src/chunk/mod.rs | 8 +-- mlua_derive/src/chunk/token.rs | 103 ++++++++++++++++++--------------- mlua_derive/src/lib.rs | 8 ++- 4 files changed, 67 insertions(+), 55 deletions(-) diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index c49d7d39..b7b4f20f 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -13,11 +13,10 @@ license = "MIT" proc-macro = true [features] -macros = ["proc-macro-error2", "itertools"] +macros = ["itertools"] [dependencies] quote = "1.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } -proc-macro-error2 = { version = "2.0.1", optional = true } syn = { version = "2.0", features = ["full"] } itertools = { version = "0.14", optional = true } diff --git a/mlua_derive/src/chunk/mod.rs b/mlua_derive/src/chunk/mod.rs index 50306e89..df618f16 100644 --- a/mlua_derive/src/chunk/mod.rs +++ b/mlua_derive/src/chunk/mod.rs @@ -63,8 +63,8 @@ pub(crate) struct Chunk { } impl Chunk { - pub(crate) fn new(tokens: TokenStream) -> Self { - let tokens = Tokens::retokenize(tokens); + pub(crate) fn new(tokens: TokenStream) -> Result { + let tokens = Tokens::retokenize(tokens)?; let mut source = String::new(); let mut caps = Captures::new(); @@ -91,10 +91,10 @@ impl Chunk { prev_end = Some(t.end()); } - Self { + Ok(Self { source: source.trim_end().to_string(), caps, - } + }) } pub(crate) fn captures(&self) -> &[Capture] { diff --git a/mlua_derive/src/chunk/token.rs b/mlua_derive/src/chunk/token.rs index 51c565c3..ab580414 100644 --- a/mlua_derive/src/chunk/token.rs +++ b/mlua_derive/src/chunk/token.rs @@ -1,10 +1,11 @@ use std::cmp::{Eq, PartialEq}; +use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; use std::vec::IntoIter; -use itertools::Itertools; use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; -use proc_macro2::Span as Span2; +use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; +use syn; #[derive(Clone, Copy, Debug)] pub(crate) struct Pos { @@ -32,19 +33,21 @@ impl Pos { } } -fn span_pos(span: &Span) -> (Pos, Pos) { +fn span_pos(span: &Span) -> Result<(Pos, Pos), TokenStream2> { let span2: Span2 = (*span).into(); let start = span2.start(); let end = span2.end(); // Rust 1.88 stabilized Span APIs, so this branch must be unreachable if start.line == 0 || end.line == 0 { - proc_macro_error2::abort_call_site!( - "cannot retrieve span location information; mlua requires nightly Rust or stable >= 1.88" - ); + return Err(syn::Error::new( + Span2::call_site(), + "cannot retrieve span location information; mlua requires nightly Rust or stable >= 1.88", + ) + .to_compile_error()); } - (Pos::new(start.line, start.column), Pos::new(end.line, end.column)) + Ok((Pos::new(start.line, start.column), Pos::new(end.line, end.column))) } /// Attribute of token. @@ -74,33 +77,32 @@ impl PartialEq for Token { impl Eq for Token {} impl Token { - fn new(tree: TokenTree) -> Self { - let (start, end) = span_pos(&tree.span()); + fn new(tree: TokenTree) -> Result { + let (start, end) = span_pos(&tree.span())?; let source = tree.span().source_text().unwrap_or_else(|| tree.to_string()); - Self { + Ok(Self { source, start, end, tree, attr: TokenAttr::None, - } + }) } - fn new_delim(source: String, tree: TokenTree, open: bool) -> Self { - let (start, end) = span_pos(&tree.span()); + fn new_delim(source: String, tree: TokenTree, open: bool) -> Result { + let (start, end) = span_pos(&tree.span())?; let (start, end) = if open { (start, start.right()) } else { (end.left(), end) }; - - Self { + Ok(Self { source, tree, start, end, attr: TokenAttr::None, - } + }) } pub(crate) fn tree(&self) -> &TokenTree { @@ -133,27 +135,33 @@ impl Token { pub(crate) struct Tokens(pub(crate) Vec); impl Tokens { - pub(crate) fn retokenize(tt: TokenStream) -> Tokens { - Tokens( - tt.into_iter() - .flat_map(Tokens::from) - .batching(|iter| { - // Find variable tokens: `$` + `ident` => `$ident` - let t = iter.next()?; - if t.is("$") { - if let Some(next) = iter.next() - && matches!(next.tree, TokenTree::Ident(_)) - { - Some(next.attr(TokenAttr::Cap)) - } else { - proc_macro_error2::abort!(t.tree.span(), "`$` must be followed by an identifier"); - } - } else { - Some(t) - } - }) - .collect(), - ) + pub(crate) fn retokenize(tt: TokenStream) -> Result { + let mut flat = Vec::new(); + for tree in tt.into_iter() { + flat.extend(Tokens::try_from(tree)?.0); + } + + let mut tokens = Vec::new(); + let mut iter = flat.into_iter(); + while let Some(t) = iter.next() { + // Find variable tokens: `$` + `ident` => `$ident` + if t.is("$") { + if let Some(next) = iter.next() + && matches!(next.tree, TokenTree::Ident(_)) + { + tokens.push(next.attr(TokenAttr::Cap)); + } else { + return Err(syn::Error::new( + t.tree.span().into(), + "`$` must be followed by an identifier", + ) + .to_compile_error()); + } + } else { + tokens.push(t); + } + } + Ok(Tokens(tokens)) } } @@ -166,8 +174,10 @@ impl IntoIterator for Tokens { } } -impl From for Tokens { - fn from(tt: TokenTree) -> Self { +impl TryFrom for Tokens { + type Error = TokenStream2; + + fn try_from(tt: TokenTree) -> Result { let tts = match tt.clone() { TokenTree::Group(g) => { let (b, e) = match g.delimiter() { @@ -178,15 +188,16 @@ impl From for Tokens { }; let (b, e) = (b.into(), e.into()); - vec![Token::new_delim(b, tt.clone(), true)] - .into_iter() - .chain(g.stream().into_iter().flat_map(Tokens::from)) - .chain(vec![Token::new_delim(e, tt, false)]) - .collect() + let mut result = vec![Token::new_delim(b, tt.clone(), true)?]; + for inner in g.stream().into_iter() { + result.extend(Tokens::try_from(inner)?.0); + } + result.push(Token::new_delim(e, tt, false)?); + result } - _ => vec![Token::new(tt)], + _ => vec![Token::new(tt)?], }; - Tokens(tts) + Ok(Tokens(tts)) } } diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index 8fc6dd49..1031e13c 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -3,7 +3,7 @@ use proc_macro::TokenStream; mod module; #[cfg(feature = "macros")] -use {crate::chunk::Chunk, proc_macro_error2::proc_macro_error}; +use crate::chunk::Chunk; #[cfg(feature = "macros")] macro_rules! try_compile { @@ -22,9 +22,11 @@ pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { #[cfg(feature = "macros")] #[proc_macro] -#[proc_macro_error] pub fn chunk(input: TokenStream) -> TokenStream { - Chunk::new(input).expand().into() + match Chunk::new(input) { + Ok(chunk) => chunk.expand().into(), + Err(err) => err.into(), + } } #[cfg(feature = "macros")] From ed4376d42a057353f79cf0190e0ab9409e164f8d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 14 Jun 2026 14:18:19 +0100 Subject: [PATCH 089/138] mlua_derive: Drop itertools dependency Plus minor codestyle changes --- mlua_derive/Cargo.toml | 3 +-- mlua_derive/src/chunk/mod.rs | 6 ++---- mlua_derive/src/chunk/token.rs | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index b7b4f20f..7f0f6548 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -13,10 +13,9 @@ license = "MIT" proc-macro = true [features] -macros = ["itertools"] +macros = [] [dependencies] quote = "1.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } syn = { version = "2.0", features = ["full"] } -itertools = { version = "0.14", optional = true } diff --git a/mlua_derive/src/chunk/mod.rs b/mlua_derive/src/chunk/mod.rs index df618f16..f89372c1 100644 --- a/mlua_derive/src/chunk/mod.rs +++ b/mlua_derive/src/chunk/mod.rs @@ -91,10 +91,8 @@ impl Chunk { prev_end = Some(t.end()); } - Ok(Self { - source: source.trim_end().to_string(), - caps, - }) + let source = source.trim_end().to_string(); + Ok(Self { source, caps }) } pub(crate) fn captures(&self) -> &[Capture] { diff --git a/mlua_derive/src/chunk/token.rs b/mlua_derive/src/chunk/token.rs index ab580414..501f8bfb 100644 --- a/mlua_derive/src/chunk/token.rs +++ b/mlua_derive/src/chunk/token.rs @@ -137,8 +137,8 @@ pub(crate) struct Tokens(pub(crate) Vec); impl Tokens { pub(crate) fn retokenize(tt: TokenStream) -> Result { let mut flat = Vec::new(); - for tree in tt.into_iter() { - flat.extend(Tokens::try_from(tree)?.0); + for tree in tt { + flat.extend(Tokens::try_from(tree)?); } let mut tokens = Vec::new(); @@ -189,8 +189,8 @@ impl TryFrom for Tokens { let (b, e) = (b.into(), e.into()); let mut result = vec![Token::new_delim(b, tt.clone(), true)?]; - for inner in g.stream().into_iter() { - result.extend(Tokens::try_from(inner)?.0); + for inner in g.stream() { + result.extend(Tokens::try_from(inner)?); } result.push(Token::new_delim(e, tt, false)?); result From 7d2fc110ec1807ee25cfb66e5cc517f788a7558e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 14 Jun 2026 14:30:53 +0100 Subject: [PATCH 090/138] Map implicit async threads to their root owner `Lua::current_thread()` now returns a stable handle instead of a temporary coroutine. Closes #706 --- src/function.rs | 1 + src/state.rs | 11 +++++++++++ src/state/extra.rs | 5 +++++ src/state/raw.rs | 18 ++++++++++++++++++ src/thread.rs | 6 ++++++ tests/async.rs | 16 +++++++++++++++- 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/function.rs b/src/function.rs index 234bbe23..0eac4496 100644 --- a/src/function.rs +++ b/src/function.rs @@ -252,6 +252,7 @@ impl Function { lua.create_recycled_thread(self).and_then(|th| { let mut th = th.into_async(args)?; th.set_recyclable(true); + lua.update_thread_ownership(th.thread(), Some(lua.state())); Ok(th) }) }) diff --git a/src/state.rs b/src/state.rs index eb177df1..75259536 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1764,7 +1764,18 @@ impl Lua { pub fn current_thread(&self) -> Thread { let lua = self.lock(); let state = lua.state(); + let extra = lua.extra.get(); unsafe { + // If this thread is implicit (created by `call_async`), return the root user-owned thread + // instead. + #[cfg(feature = "async")] + if let Some(&owner) = (*extra).thread_ownership_map.get(&state) { + assert_stack(owner, 1); + ffi::lua_pushthread(owner); + ffi::lua_xmove(owner, lua.ref_thread(), 1); + return Thread(lua.pop_ref_thread(), owner); + } + let _sg = StackGuard::new(state); assert_stack(state, 1); ffi::lua_pushthread(state); diff --git a/src/state/extra.rs b/src/state/extra.rs index 9889e63c..187a71d7 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -66,6 +66,9 @@ pub(crate) struct ExtraData { // Pool of `Thread`s (coroutines) for async execution #[cfg(feature = "async")] pub(super) thread_pool: Vec, + // Map for implicit threads to root user-owned Thread + #[cfg(feature = "async")] + pub(super) thread_ownership_map: FxHashMap<*mut ffi::lua_State, *mut ffi::lua_State>, // Address of `WrappedFailure` metatable pub(super) wrapped_failure_mt_ptr: *const c_void, @@ -174,6 +177,8 @@ impl ExtraData { wrapped_failure_top: 0, #[cfg(feature = "async")] thread_pool: Vec::new(), + #[cfg(feature = "async")] + thread_ownership_map: FxHashMap::default(), wrapped_failure_mt_ptr, #[cfg(feature = "async")] waker: NonNull::from(noop_waker_ref()), diff --git a/src/state/raw.rs b/src/state/raw.rs index c6a6f9a9..8012b192 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -693,6 +693,24 @@ impl RawLua { self.create_thread(func) } + /// Updates the ownership of the given implicit thread to the root user-owned thread. + /// + /// If `owner` is `None`, the thread is removed from the ownership map. + #[cfg(feature = "async")] + pub(crate) unsafe fn update_thread_ownership(&self, th: &Thread, owner: Option<*mut ffi::lua_State>) { + let extra = &mut *self.extra.get(); + let th_state = th.state(); + match owner { + Some(owner) => { + let new_owner = (extra.thread_ownership_map).get(&owner).copied().unwrap_or(owner); + extra.thread_ownership_map.insert(th_state, new_owner); + } + None => { + extra.thread_ownership_map.remove(&th_state); + } + } + } + /// Returns the thread to the pool for later use. #[cfg(feature = "async")] pub(crate) unsafe fn recycle_thread(&self, thread: &mut Thread) { diff --git a/src/thread.rs b/src/thread.rs index da7c9881..29246faf 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -689,6 +689,11 @@ impl AsyncThread { pub(crate) fn set_recyclable(&mut self, recyclable: bool) { self.recycle = recyclable; } + + #[inline(always)] + pub(crate) fn thread(&self) -> &Thread { + &self.thread + } } #[cfg(feature = "async")] @@ -712,6 +717,7 @@ impl Drop for AsyncThread { if self.thread.reset_inner(status).is_ok() { lua.recycle_thread(&mut self.thread); } + lua.update_thread_ownership(&self.thread, None); } } } diff --git a/tests/async.rs b/tests/async.rs index 55e41593..fb5a2bd7 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -7,7 +7,7 @@ use futures_util::stream::TryStreamExt; use tokio::sync::Mutex; use mlua::{ - Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, UserData, + Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, Thread, UserData, UserDataMethods, UserDataRef, Value, }; @@ -718,3 +718,17 @@ fn test_async_yield_with() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_async_current_thread() -> Result<()> { + let lua = Lua::new(); + + let get_inner_thread = lua.create_async_function(move |lua, ()| async move { + let f = lua.create_async_function(move |lua, ()| async move { Ok(lua.current_thread()) })?; + f.call_async::(()).await + })?; + let inner_thread = get_inner_thread.call_async::(()).await?; + assert_eq!(inner_thread, lua.current_thread()); + + Ok(()) +} From 304f31ded6079a19bc94dd5781599c62a95876ff Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 14 Jun 2026 17:11:58 +0100 Subject: [PATCH 091/138] Fix module tests on windows --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b67adccb..9fcaecee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -203,7 +203,7 @@ jobs: needs: build strategy: matrix: - lua: [lua54, luajit] + lua: [lua55, lua54, luajit] defaults: run: shell: msys2 {0} @@ -212,7 +212,7 @@ jobs: - uses: actions/checkout@main - name: Install Rust & Lua run: | - pacman -S --noconfirm mingw-w64-x86_64-rust mingw-w64-x86_64-lua mingw-w64-x86_64-luajit mingw-w64-x86_64-pkg-config + pacman -S --noconfirm mingw-w64-x86_64-rust mingw-w64-x86_64-lua54 mingw-w64-x86_64-lua mingw-w64-x86_64-luajit mingw-w64-x86_64-pkg-config - name: Run ${{ matrix.lua }} module tests run: | (cd tests/module && cargo build --release --features "${{ matrix.lua }}") From 48820114376e14ace05503259cfed784293b3299 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 14 Jun 2026 17:12:15 +0100 Subject: [PATCH 092/138] clippy --- src/state.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/state.rs b/src/state.rs index 75259536..1ae90636 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1764,12 +1764,11 @@ impl Lua { pub fn current_thread(&self) -> Thread { let lua = self.lock(); let state = lua.state(); - let extra = lua.extra.get(); unsafe { // If this thread is implicit (created by `call_async`), return the root user-owned thread // instead. #[cfg(feature = "async")] - if let Some(&owner) = (*extra).thread_ownership_map.get(&state) { + if let Some(&owner) = (*lua.extra.get()).thread_ownership_map.get(&state) { assert_stack(owner, 1); ffi::lua_pushthread(owner); ffi::lua_xmove(owner, lua.ref_thread(), 1); From 3f4a741c23877ecddaa0139acb765a0581ae4f21 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 14 Jun 2026 22:31:21 +0100 Subject: [PATCH 093/138] mlua_derive: Support `Option<&[mut] T>` callback parameters in userdata_impl macro Closes #709 --- mlua_derive/src/chunk/token.rs | 1 - mlua_derive/src/userdata/userdata_impl.rs | 91 +++++++++++++++++------ tests/userdata_macro.rs | 32 ++++++++ 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/mlua_derive/src/chunk/token.rs b/mlua_derive/src/chunk/token.rs index 501f8bfb..da7a14c8 100644 --- a/mlua_derive/src/chunk/token.rs +++ b/mlua_derive/src/chunk/token.rs @@ -5,7 +5,6 @@ use std::vec::IntoIter; use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; -use syn; #[derive(Clone, Copy, Debug)] pub(crate) struct Pos { diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 4e995aa6..6066671b 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -26,6 +26,8 @@ enum SelfKind { enum RefKind { Ref, Mut, + OptionRef, + OptionMut, } struct ArgInfo { @@ -104,6 +106,25 @@ fn classify_ref_type(ty: &Type) -> Option { } } +/// If `ty` is `Option`, return the inner type. +fn try_unwrap_option(ty: &Type) -> Option<&Type> { + let Type::Path(type_path) = ty else { return None }; + let segment = type_path.path.segments.last()?; + if segment.ident != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &segment.arguments else { + return None; + }; + if args.args.len() != 1 { + return None; + } + let syn::GenericArgument::Type(inner) = &args.args[0] else { + return None; + }; + Some(inner) +} + /// Analyze method signature. /// /// Determine `self` kind and collect the callback arguments. @@ -134,12 +155,32 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result { check_first_typed = false; if let syn::Pat::Ident(pat_ident) = &*typed.pat { let arg_type = &*typed.ty; + let mut option_inner = None; let ref_kind = match arg_type { Type::Reference(r) if r.mutability.is_some() => Some(RefKind::Mut), Type::Reference(_) => Some(RefKind::Ref), - _ => None, + _ => { + // Check if it's `Option<&T>` or `Option<&mut T>` + option_inner = try_unwrap_option(arg_type); + option_inner.and_then(|inner| match inner { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::OptionMut), + Type::Reference(_) => Some(RefKind::OptionRef), + _ => None, + }) + } }; let callback_type = match &ref_kind { + Some(RefKind::OptionRef | RefKind::OptionMut) => { + match classify_ref_type(option_inner.unwrap()) { + Some(ty) => parse_quote! { Option<#ty> }, + None => { + return Err(syn::Error::new_spanned( + arg_type, + "this reference type is not supported as a callback parameter", + )); + } + } + } Some(_) => match classify_ref_type(arg_type) { Some(ty) => ty, None => { @@ -449,7 +490,7 @@ fn gen_closure_destructure(info: &MethodInfo) -> TokenStream2 { .iter() .map(|a| { let ident = &a.ident; - if matches!(a.userdata_ref, Some(RefKind::Mut)) { + if matches!(a.userdata_ref, Some(RefKind::Mut | RefKind::OptionMut)) { quote! { mut #ident } } else { quote! { #ident } @@ -460,6 +501,18 @@ fn gen_closure_destructure(info: &MethodInfo) -> TokenStream2 { quote! { (#(#idents),*): (#(#types),*) } } +/// Generate the call-site expression for a single argument. +fn gen_arg_token(arg: &ArgInfo) -> TokenStream2 { + let ident = &arg.ident; + match arg.userdata_ref { + Some(RefKind::Ref) => quote! { &*#ident }, + Some(RefKind::Mut) => quote! { &mut *#ident }, + Some(RefKind::OptionRef) => quote! { #ident.as_ref().map(|r| &**r) }, + Some(RefKind::OptionMut) => quote! { #ident.as_mut().map(|r| &mut **r) }, + None => quote! { #ident }, + } +} + /// Generate call arguments for invoking the original method. fn gen_call_args(info: &MethodInfo) -> TokenStream2 { let mut call_args: Vec = Vec::new(); @@ -474,12 +527,7 @@ fn gen_call_args(info: &MethodInfo) -> TokenStream2 { } 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 }), - } + call_args.push(gen_arg_token(arg)); } quote! { #(#call_args),* } @@ -491,8 +539,8 @@ fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { 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::Ref(RefKind::Mut | RefKind::OptionMut) => call_args.push(quote! { &mut this }), + SelfKind::Ref(_) => call_args.push(quote! { &this }), SelfKind::Owned => call_args.push(quote! { this }), } @@ -501,12 +549,7 @@ fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { } 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 }), - } + call_args.push(gen_arg_token(arg)); } quote! { #(#call_args),* } @@ -640,12 +683,12 @@ fn gen_regular_method( quote! { #fn_path(#call_args) } }; match info.self_kind { - SelfKind::Ref(RefKind::Ref) => quote! { - registry.add_method(#lua_name, #closure_params { #body }); - }, - SelfKind::Ref(RefKind::Mut) => quote! { + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => quote! { registry.add_method_mut(#lua_name, #closure_params { #body }); }, + SelfKind::Ref(_) => quote! { + registry.add_method(#lua_name, #closure_params { #body }); + }, SelfKind::Owned => quote! { registry.add_method_once(#lua_name, #closure_params { #body }); }, @@ -672,12 +715,12 @@ fn gen_async_regular_method( 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! { + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => quote! { registry.add_async_method_mut(#lua_name, #closure_params #body); }, + SelfKind::Ref(_) => quote! { + registry.add_async_method(#lua_name, #closure_params #body); + }, SelfKind::Owned => quote! { registry.add_async_method_once(#lua_name, #closure_params #body); }, diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index ec802f34..61c305dc 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -79,6 +79,18 @@ impl Rectangle { } } + #[lua(infallible)] + fn maybe_add(&self, other: Option<&Rectangle>) -> Rectangle { + match other { + Some(other) => Rectangle { + length: self.length + other.length, + width: self.width + other.width, + ..Default::default() + }, + None => self.clone(), + } + } + #[lua(meta, field, name = "__answer")] fn answer() -> u32 { 42 @@ -113,6 +125,13 @@ impl Rectangle { Ok(format!("Hello, {name}!")) } + fn maybe_greet(&self, name: Option<&str>) -> Result { + match name { + Some(name) => Ok(format!("Hello, {name}!")), + None => Ok("Hello!".to_string()), + } + } + fn transfer_length(&mut self, other: &mut Rectangle) -> Result<()> { other.length += self.length; self.length = 0; @@ -190,6 +209,14 @@ fn test_rectangle() { assert(r4.length == 8, "__add length should be 5 + 3 = 8") assert(r4.width == 14, "__add width should be 10 + 4 = 14") + -- Option<&T> wrapped parameter + local r5 = r1:maybe_add(r3) + assert(r5.length == 8, "maybe_add with arg length should be 5 + 3 = 8") + assert(r5.width == 14, "maybe_add with arg width should be 10 + 4 = 14") + local r6 = r1:maybe_add() + assert(r6.length == 5, "maybe_add with nil is no-op") + assert(r6.width == 10, "maybe_add with nil is no-op") + -- method with &mut self and &mut Rectangle param rect = Rectangle.new(5, 10, 3) other = Rectangle.new(2, 3, 0) @@ -212,6 +239,11 @@ fn test_rectangle() { assert(h == 10, "into_tuple height should be 7") local ok, err = pcall(function() rect:area() end) assert(not ok and tostring(err):match("userdata has been destructed"), "rect should be consumed and unusable after into_tuple") + + -- Custom methods + assert(other:greet("User") == "Hello, User!", "greet should return 'Hello, User!'") + assert(other:maybe_greet("User") == "Hello, User!", "maybe_greet with arg should return 'Hello, User!'") + assert(other:maybe_greet() == "Hello!", "maybe_greet with nil should return 'Hello!'") "#, ) .exec() From fdfe322621fec81bd476b7a36ebcb4025862e95a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 19 Jun 2026 23:42:01 +0100 Subject: [PATCH 094/138] Don't free external string on memory error (Lua 5.5) In https://www.lua.org/source/5.5/lstring.c.html#luaS_newextlstr Lua already do this on memory error. --- src/util/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/util/mod.rs b/src/util/mod.rs index 0741f12c..a5058734 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -121,8 +121,7 @@ pub(crate) unsafe fn push_external_string( ffi::lua_pushexternalstring(state, s_ptr, s_len, Some(dealloc), bytes_ud as *mut _); }); if res.is_err() { - // Deallocate on error - drop(Box::from_raw(bytes_ud)); + // Lua free external string on error return res; } } else { From f3765d190c51ca2b3322570583493e261c773645 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 19 Jun 2026 23:44:35 +0100 Subject: [PATCH 095/138] Add StackGuard in `Table::clear` (non-Luau) --- src/table.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/table.rs b/src/table.rs index e938000e..b36cb2a0 100644 --- a/src/table.rs +++ b/src/table.rs @@ -564,6 +564,7 @@ impl Table { #[cfg(not(feature = "luau"))] { let state = lua.state(); + let _sg = StackGuard::new(state); check_stack(state, 4)?; lua.push_ref(&self.0); From 705a677a7584edcfb7bd5516871f31fbc779513d Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:09:37 +0100 Subject: [PATCH 096/138] Don't leak `T` if `FromLua` for `[T; N]` fails --- src/conversion.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 1fdacb03..c3fc12ed 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -902,17 +902,21 @@ where fn from_lua(value: Value, _lua: &Lua) -> Result { match value { #[cfg(feature = "luau")] - #[rustfmt::skip] - Value::Vector(v) if N == crate::Vector::SIZE => unsafe { - use std::{mem, ptr}; - let mut arr: [mem::MaybeUninit; N] = mem::MaybeUninit::uninit().assume_init(); - ptr::write(arr[0].as_mut_ptr() , T::from_lua(Value::Number(v.x() as _), _lua)?); - ptr::write(arr[1].as_mut_ptr(), T::from_lua(Value::Number(v.y() as _), _lua)?); - ptr::write(arr[2].as_mut_ptr(), T::from_lua(Value::Number(v.z() as _), _lua)?); + Value::Vector(v) if N == crate::Vector::SIZE => { + use std::mem::MaybeUninit; + let x = T::from_lua(Value::Number(v.x() as _), _lua)?; + let y = T::from_lua(Value::Number(v.y() as _), _lua)?; + let z = T::from_lua(Value::Number(v.z() as _), _lua)?; #[cfg(feature = "luau-vector4")] - ptr::write(arr[3].as_mut_ptr(), T::from_lua(Value::Number(v.w() as _), _lua)?); - Ok(mem::transmute_copy(&arr)) - }, + let w = T::from_lua(Value::Number(v.w() as _), _lua)?; + let mut arr: [MaybeUninit; N] = [const { MaybeUninit::uninit() }; N]; + arr[0].write(x); + arr[1].write(y); + arr[2].write(z); + #[cfg(feature = "luau-vector4")] + arr[3].write(w); + Ok(arr.map(|e| unsafe { e.assume_init() })) + } Value::Table(table) => { let vec = table.sequence_values().collect::>>()?; vec.try_into().map_err(|vec: Vec| { From e9bb98f4a20d48b7bf9422a64bce25d23b4bc369 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:15:16 +0100 Subject: [PATCH 097/138] mlua-sys: Fix wrong `cfg` flags in luaL_makeseed (Lua 5.5) --- mlua-sys/Cargo.toml | 2 +- mlua-sys/src/lua55/lauxlib.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index e1bb93ce..0340ded4 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -46,4 +46,4 @@ luajit-src = { version = ">= 210.7.0, < 210.8.0", optional = true } luau0-src = { version = "0.20.0", optional = true } [lints.rust] -unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/src/lua55/lauxlib.rs b/mlua-sys/src/lua55/lauxlib.rs index 13a8524f..03de0f03 100644 --- a/mlua-sys/src/lua55/lauxlib.rs +++ b/mlua-sys/src/lua55/lauxlib.rs @@ -190,10 +190,11 @@ pub unsafe fn luaL_loadbufferenv( status } +#[allow(unused_variables, unreachable_code)] pub unsafe fn luaL_makeseed(L: *mut lua_State) -> c_uint { - #[cfg(macos)] + #[cfg(target_os = "macos")] return libc::arc4random(); - #[cfg(linux)] + #[cfg(target_os = "linux")] { let mut seed = 0u32; let buf = &mut seed as *mut _ as *mut c_void; From 135d44ba968207436f5d25fe997884b4233c4899 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:26:49 +0100 Subject: [PATCH 098/138] Fix MemoryState::set_memory_limit usize->isize conversion --- src/memory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/memory.rs b/src/memory.rs index a484e277..bdea1e06 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -50,7 +50,7 @@ impl MemoryState { #[inline] pub(crate) fn set_memory_limit(&mut self, limit: usize) -> usize { let prev_limit = self.memory_limit; - self.memory_limit = limit as isize; + self.memory_limit = limit.min(isize::MAX as usize) as isize; prev_limit as usize } From 3b558a014c333b2751f00c9a02a6d46b7c6cfdf1 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:28:15 +0100 Subject: [PATCH 099/138] Fix loading Luau chunks starting with `\t` --- src/chunk.rs | 2 +- tests/tests.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index 5c25f773..9b36465a 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -758,7 +758,7 @@ impl Chunk<'_> { return ChunkMode::Binary; } #[cfg(feature = "luau")] - if *source.first().unwrap_or(&u8::MAX) < b'\n' { + if *source.first().unwrap_or(&u8::MAX) < b'\t' { return ChunkMode::Binary; } } diff --git a/tests/tests.rs b/tests/tests.rs index 99716f63..ac438741 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -132,9 +132,9 @@ fn test_exec() -> Result<()> { fn test_eval() -> Result<()> { let lua = Lua::new(); - assert_eq!(lua.load("1 + 1").eval::()?, 2); + assert_eq!(lua.load("\t1 + 1").eval::()?, 2); assert_eq!(lua.load("false == false").eval::()?, true); - assert_eq!(lua.load("return 1 + 2").eval::()?, 3); + assert_eq!(lua.load("\nreturn 1 + 2").eval::()?, 3); match lua.load("if true then").eval::<()>() { Err(Error::SyntaxError { incomplete_input: true, From 85d707d63a7074270147db03d7f7fa4e357a177e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:36:31 +0100 Subject: [PATCH 100/138] Fix reading uninitialized memory in Luau `HeapDump::new` --- src/luau/heap_dump.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/luau/heap_dump.rs b/src/luau/heap_dump.rs index 0189845c..e8b7a266 100644 --- a/src/luau/heap_dump.rs +++ b/src/luau/heap_dump.rs @@ -34,12 +34,13 @@ impl HeapDump { } ffi::lua_gcdump(state, file as *mut _, Some(category_name)); libc::fseek(file, 0, libc::SEEK_END); - let len = libc::ftell(file) as usize; + let len = libc::ftell(file); libc::rewind(file); if len > 0 { + let len = len as usize; buf.reserve(len); - libc::fread(buf.as_mut_ptr() as *mut _, 1, len, file); - buf.set_len(len); + let n = libc::fread(buf.as_mut_ptr() as *mut _, 1, len, file); + buf.set_len(n); } libc::fclose(file); } From 29e59cfcbd8bd111b71282b0e3142e287fa02272 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 00:43:09 +0100 Subject: [PATCH 101/138] mlua_derive: Fix `FromLua` derive for where-clauses with a trailing comma --- mlua_derive/src/from_lua.rs | 16 +++++++++------- tests/userdata.rs | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/mlua_derive/src/from_lua.rs b/mlua_derive/src/from_lua.rs index 2ca69180..4a0df519 100644 --- a/mlua_derive/src/from_lua.rs +++ b/mlua_derive/src/from_lua.rs @@ -1,16 +1,18 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{DeriveInput, parse_macro_input}; +use syn::{DeriveInput, parse_macro_input, parse_quote}; pub fn from_lua(input: TokenStream) -> TokenStream { - let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput); + let DeriveInput { + ident, mut generics, .. + } = parse_macro_input!(input as DeriveInput); let ident_str = ident.to_string(); - let (impl_generics, ty_generics, _) = generics.split_for_impl(); - let where_clause = match &generics.where_clause { - Some(where_clause) => quote! { #where_clause, Self: 'static + Clone }, - None => quote! { where Self: 'static + Clone }, - }; + generics + .make_where_clause() + .predicates + .push(parse_quote!(Self: 'static + Clone)); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { diff --git a/tests/userdata.rs b/tests/userdata.rs index c5233557..69bf1912 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -986,10 +986,11 @@ fn test_userdata_derive() -> Result<()> { // More complex struct where generics and where clause + #[rustfmt::skip] #[derive(Clone, Copy, mlua::FromLua)] struct MyUserData2<'a, T: ?Sized>(&'a T) where - T: Copy; + T: Copy,; // trailing comma is needed for testing lua.register_userdata_type::>(|reg| { reg.add_function("val", |_, this: MyUserData2<'static, i32>| Ok(*this.0)); From 9a512ae1aaa52b3bef63c60f2ee990963540252e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 13:55:58 +0100 Subject: [PATCH 102/138] Refactor: simplify and deduplicate code across modules --- mlua_derive/src/userdata/userdata_impl.rs | 30 ++++++++--------- src/chunk.rs | 15 +++------ src/conversion.rs | 41 ++++++++--------------- src/debug.rs | 9 ++--- src/function.rs | 16 ++------- src/luau/json.rs | 6 ++-- src/luau/require.rs | 2 +- src/luau/require/fs.rs | 10 +++--- src/multi.rs | 7 ++-- src/state.rs | 14 ++++---- src/state/raw.rs | 4 +-- src/thread.rs | 33 +++++++++--------- src/userdata.rs | 11 +++--- src/util/mod.rs | 7 ++-- src/value.rs | 16 +++------ 15 files changed, 85 insertions(+), 136 deletions(-) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 6066671b..5c9d184b 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -301,17 +301,16 @@ 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 { - let tokens = quote! { + let tokens = if lua_attr.meta { + quote! { registry.add_meta_field(#lua_name, #type_path::#const_name); - }; - registration_calls.push(with_cfg(tokens, &const_item.attrs)); + } } else { - let tokens = quote! { + quote! { registry.add_field(#lua_name, #type_path::#const_name); - }; - registration_calls.push(with_cfg(tokens, &const_item.attrs)); - } + } + }; + registration_calls.push(with_cfg(tokens, &const_item.attrs)); } ImplItem::Fn(method) => { let lua_attr = try_compile!(parse_lua_attr(&method.attrs)); @@ -410,17 +409,16 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { .into(); } let lua_name = lua_attr.name(fn_name); - if lua_attr.meta { - let tokens = quote! { + let tokens = if lua_attr.meta { + quote! { registry.add_meta_field(#lua_name, #type_path::#fn_name()); - }; - registration_calls.push(with_cfg(tokens, &method.attrs)); + } } else { - let tokens = quote! { + quote! { registry.add_field(#lua_name, #type_path::#fn_name()); - }; - registration_calls.push(with_cfg(tokens, &method.attrs)); - } + } + }; + registration_calls.push(with_cfg(tokens, &method.attrs)); continue; } diff --git a/src/chunk.rs b/src/chunk.rs index 9b36465a..b3c84652 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -401,14 +401,11 @@ impl Compiler { use std::os::raw::{c_char, c_int}; use std::ptr; - let vector_lib = self.vector_lib.clone(); - let vector_lib = vector_lib.and_then(|lib| CString::new(lib).ok()); + let vector_lib = (self.vector_lib.as_deref()).and_then(|lib| CString::new(lib).ok()); let vector_lib = vector_lib.as_ref(); - let vector_ctor = self.vector_ctor.clone(); - let vector_ctor = vector_ctor.and_then(|ctor| CString::new(ctor).ok()); + let vector_ctor = (self.vector_ctor.as_deref()).and_then(|ctor| CString::new(ctor).ok()); let vector_ctor = vector_ctor.as_ref(); - let vector_type = self.vector_type.clone(); - let vector_type = vector_type.and_then(|t| CString::new(t).ok()); + let vector_type = (self.vector_type.as_deref()).and_then(|t| CString::new(t).ok()); let vector_type = vector_type.as_ref(); macro_rules! vec2cstring_ptr { @@ -740,11 +737,7 @@ impl Chunk<'_> { .unwrap_or(source); let name = Self::convert_name(self.name.clone())?; - let env = match &self.env { - Ok(Some(env)) => Some(env), - Ok(None) => None, - Err(err) => return Err(err.clone()), - }; + let env = self.env.as_ref().map_err(Error::clone)?.as_ref(); self.lua.lock().load_chunk(Some(&name), env, None, &source) } diff --git a/src/conversion.rs b/src/conversion.rs index c3fc12ed..3ee8a1f0 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1117,36 +1117,23 @@ impl FromLua for Either { #[inline] fn from_lua(value: Value, lua: &Lua) -> Result { let value_type_name = value.type_name(); - // Try the left type first - match L::from_lua(value.clone(), lua) { - Ok(l) => Ok(Either::Left(l)), - // Try the right type - Err(_) => match R::from_lua(value, lua).map(Either::Right) { - Ok(r) => Ok(r), - Err(_) => Err(Error::from_lua_conversion( - value_type_name, - Self::type_name(), - None, - )), - }, - } + L::from_lua(value.clone(), lua) + .map(Either::Left) + .or_else(|_| R::from_lua(value, lua).map(Either::Right)) + .map_err(|_| Error::from_lua_conversion(value_type_name, Self::type_name(), None)) } #[inline] unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { - match L::from_stack(idx, lua) { - Ok(l) => Ok(Either::Left(l)), - Err(_) => match R::from_stack(idx, lua).map(Either::Right) { - Ok(r) => Ok(r), - Err(_) => { - let state = lua.state(); - let from_type_name = CStr::from_ptr(ffi::lua_typename(state, ffi::lua_type(state, idx))) - .to_str() - .unwrap_or("unknown"); - let err = Error::from_lua_conversion(from_type_name, Self::type_name(), None); - Err(err) - } - }, - } + L::from_stack(idx, lua) + .map(Either::Left) + .or_else(|_| R::from_stack(idx, lua).map(Either::Right)) + .map_err(|_| { + let state = lua.state(); + let from_type_name = CStr::from_ptr(ffi::lua_typename(state, ffi::lua_type(state, idx))) + .to_str() + .unwrap_or("unknown"); + Error::from_lua_conversion(from_type_name, Self::type_name(), None) + }) } } diff --git a/src/debug.rs b/src/debug.rs index 89d8c501..6d4eb77e 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -99,10 +99,7 @@ impl<'a> Debug<'a> { DebugNames { name: ptr_to_lossy_str((*self.ar).name), #[cfg(not(feature = "luau"))] - name_what: match ptr_to_str((*self.ar).namewhat) { - Some("") => None, - val => val, - }, + name_what: ptr_to_str((*self.ar).namewhat).filter(|s| !s.is_empty()), #[cfg(feature = "luau")] name_what: None, } @@ -379,9 +376,7 @@ impl std::ops::BitOr for HookTriggers { self.on_calls |= rhs.on_calls; self.on_returns |= rhs.on_returns; self.every_line |= rhs.every_line; - if self.every_nth_instruction.is_none() && rhs.every_nth_instruction.is_some() { - self.every_nth_instruction = rhs.every_nth_instruction; - } + self.every_nth_instruction = self.every_nth_instruction.or(rhs.every_nth_instruction); self } } diff --git a/src/function.rs b/src/function.rs index 0eac4496..7da31b7e 100644 --- a/src/function.rs +++ b/src/function.rs @@ -457,10 +457,7 @@ impl Function { FunctionInfo { name: ptr_to_lossy_str(ar.name).map(|s| s.into_owned()), #[cfg(not(feature = "luau"))] - name_what: match ptr_to_str(ar.namewhat) { - Some("") => None, - val => val, - }, + name_what: ptr_to_str(ar.namewhat).filter(|s| !s.is_empty()), #[cfg(feature = "luau")] name_what: None, what: ptr_to_str(ar.what).unwrap_or("main"), @@ -542,22 +539,15 @@ impl Function { where F: FnMut(CoverageInfo), { - use std::ffi::CStr; - use std::os::raw::c_char; - unsafe extern "C-unwind" fn callback( data: *mut c_void, - function: *const c_char, + function: *const std::os::raw::c_char, line_defined: c_int, depth: c_int, hits: *const c_int, size: usize, ) { - let function = if !function.is_null() { - Some(CStr::from_ptr(function).to_string_lossy().to_string()) - } else { - None - }; + let function = ptr_to_lossy_str(function).map(|s| s.into_owned()); let rust_callback = &*(data as *const RefCell); if let Ok(mut rust_callback) = rust_callback.try_borrow_mut() { // Call the Rust callback with CoverageInfo diff --git a/src/luau/json.rs b/src/luau/json.rs index ce17a20e..b8892aec 100644 --- a/src/luau/json.rs +++ b/src/luau/json.rs @@ -52,8 +52,7 @@ impl<'a> Json<'a> { } pub(crate) fn as_u64(&self) -> Option { - self.as_i64() - .and_then(|i| if i >= 0 { Some(i as u64) } else { None }) + self.as_i64().and_then(|i| u64::try_from(i).ok()) } pub(crate) fn as_array(&self) -> Option<&[Json<'a>]> { @@ -74,8 +73,7 @@ impl<'a> Json<'a> { pub(crate) fn parse<'a>(s: &'a str) -> Result, &'static str> { let s = s.trim_ascii(); let mut chars = s.char_indices().peekable(); - let value = parse_value(s, &mut chars)?; - Ok(value) + parse_value(s, &mut chars) } fn parse_value<'a>(s: &'a str, chars: &mut Peekable) -> Result, &'static str> { diff --git a/src/luau/require.rs b/src/luau/require.rs index e6902a54..7e8db695 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -359,7 +359,7 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ fn detect_config_format(data: &[u8]) -> ConfigStatus { let data = data.trim_ascii(); if data.starts_with(b"{") { - let data = &data[1..].trim_ascii_start(); + let data = data[1..].trim_ascii_start(); if data.starts_with(b"\"") || data == b"}" { return ConfigStatus::PresentJson; } diff --git a/src/luau/require/fs.rs b/src/luau/require/fs.rs index f6373434..61250485 100644 --- a/src/luau/require/fs.rs +++ b/src/luau/require/fs.rs @@ -208,13 +208,15 @@ impl Require for FsRequirer { } fn has_config(&self) -> bool { - self.abs_path.is_dir() && self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() - || self.abs_path.is_dir() && self.abs_path.join(Self::LUAU_CONFIG_FILENAME).is_file() + self.abs_path.is_dir() + && (self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() + || self.abs_path.join(Self::LUAU_CONFIG_FILENAME).is_file()) } fn config(&self) -> IoResult> { - if self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() { - return fs::read(self.abs_path.join(Self::LUAURC_CONFIG_FILENAME)); + let path = self.abs_path.join(Self::LUAURC_CONFIG_FILENAME); + if path.is_file() { + return fs::read(path); } fs::read(self.abs_path.join(Self::LUAU_CONFIG_FILENAME)) } diff --git a/src/multi.rs b/src/multi.rs index f82f4cb5..10b16046 100644 --- a/src/multi.rs +++ b/src/multi.rs @@ -1,6 +1,5 @@ use std::collections::{VecDeque, vec_deque}; use std::iter::FromIterator; -use std::mem; use std::ops::{Deref, DerefMut}; use std::os::raw::c_int; use std::result::Result as StdResult; @@ -180,10 +179,8 @@ impl IntoIterator for MultiValue { type IntoIter = vec_deque::IntoIter; #[inline] - fn into_iter(mut self) -> Self::IntoIter { - let deque = mem::take(&mut self.0); - mem::forget(self); - deque.into_iter() + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() } } diff --git a/src/state.rs b/src/state.rs index 1ae90636..2183cee5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -975,11 +975,11 @@ impl Lua { #[cfg(any(feature = "lua55", feature = "lua54"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] pub fn warning(&self, msg: impl AsRef, incomplete: bool) { - let msg = msg.as_ref(); - let mut bytes = vec![0; msg.len() + 1]; - bytes[..msg.len()].copy_from_slice(msg.as_bytes()); - let real_len = bytes.iter().position(|&c| c == 0).unwrap(); - bytes.truncate(real_len); + let msg = msg.as_ref().as_bytes(); + let end = msg.iter().position(|&c| c == 0).unwrap_or(msg.len()); + let mut bytes = Vec::with_capacity(end + 1); + bytes.extend_from_slice(&msg[..end]); + bytes.push(0); let lua = self.lock(); unsafe { ffi::lua_warning(lua.state(), bytes.as_ptr() as *const _, incomplete as c_int); @@ -1848,7 +1848,7 @@ impl Lua { lua.push_value(&v)?; let mut isint = 0; let i = ffi::lua_tointegerx(state, -1, &mut isint); - if isint == 0 { None } else { Some(i) } + (isint != 0).then_some(i) }, }) } @@ -1870,7 +1870,7 @@ impl Lua { lua.push_value(&v)?; let mut isnum = 0; let n = ffi::lua_tonumberx(state, -1, &mut isnum); - if isnum == 0 { None } else { Some(n) } + (isnum != 0).then_some(n) }, }) } diff --git a/src/state/raw.rs b/src/state/raw.rs index 8012b192..098a3bec 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -304,13 +304,13 @@ impl RawLua { #[cfg(not(feature = "luau"))] if is_safe { let curr_libs = (*self.extra.get()).libs; - if (curr_libs ^ (curr_libs | libs)).contains(StdLib::PACKAGE) { + if libs.contains(StdLib::PACKAGE) && !curr_libs.contains(StdLib::PACKAGE) { mlua_expect!(self.lua().disable_c_modules(), "Error disabling C modules"); } } #[cfg(feature = "luau")] let _ = is_safe; - unsafe { (*self.extra.get()).libs |= libs }; + (*self.extra.get()).libs |= libs; res } diff --git a/src/thread.rs b/src/thread.rs index 29246faf..b1e209ca 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -699,26 +699,25 @@ impl AsyncThread { #[cfg(feature = "async")] impl Drop for AsyncThread { fn drop(&mut self) { - #[allow(clippy::collapsible_if)] - if self.recycle { - if let Some(lua) = self.thread.0.lua.try_lock() { - unsafe { - let mut status = self.thread.status_inner(&lua); - if matches!(status, ThreadStatusInner::Yielded(0)) { - // The thread is dropped while yielded, resume it with the "terminate" signal - ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0); - if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) { - // `new_status` should always be `ThreadStatusInner::Yielded(0)` - status = new_status; - } + if self.recycle + && let Some(lua) = self.thread.0.lua.try_lock() + { + unsafe { + let mut status = self.thread.status_inner(&lua); + if matches!(status, ThreadStatusInner::Yielded(0)) { + // The thread is dropped while yielded, resume it with the "terminate" signal + ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0); + if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) { + // `new_status` should always be `ThreadStatusInner::Yielded(0)` + status = new_status; } + } - // For Lua 5.4 this also closes all pending to-be-closed variables - if self.thread.reset_inner(status).is_ok() { - lua.recycle_thread(&mut self.thread); - } - lua.update_thread_ownership(&self.thread, None); + // For Lua 5.4 this also closes all pending to-be-closed variables + if self.thread.reset_inner(status).is_ok() { + lua.recycle_thread(&mut self.thread); } + lua.update_thread_ownership(&self.thread, None); } } } diff --git a/src/userdata.rs b/src/userdata.rs index 7749c9b0..41c166a1 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -275,8 +275,7 @@ impl MetaMethod { pub(crate) fn validate(name: &str) -> Result<&str> { match name { - "__gc" => Err(Error::MetaMethodRestricted(name.to_string())), - "__metatable" => Err(Error::MetaMethodRestricted(name.to_string())), + "__gc" | "__metatable" => Err(Error::MetaMethodRestricted(name.to_string())), _ if name.starts_with("__mlua") => Err(Error::MetaMethodRestricted(name.to_string())), name => Ok(name), } @@ -923,7 +922,7 @@ impl AnyUserData { } ffi::lua_rawgeti(state, -1, n as ffi::lua_Integer); - V::from_lua(lua.pop_value(), lua.lua()) + V::from_stack(-1, &lua) } } @@ -1064,8 +1063,8 @@ impl AnyUserData { return Ok(false); } - if mt.contains_key("__eq")? { - return mt.get::("__eq")?.call((self, other)); + if let Some(eq) = mt.get::>("__eq")? { + return eq.call((self, other)); } Ok(false) @@ -1080,7 +1079,7 @@ impl AnyUserData { // Userdata must be registered and not destructed let _ = lua.get_userdata_ref_type_id(&self.0)?; let ud = &*get_userdata::>(lua.ref_thread(), self.0.index); - Ok::<_, Error>((*ud).is_serializable()) + Ok::<_, Error>(ud.is_serializable()) }; is_serializable().unwrap_or(false) } diff --git a/src/util/mod.rs b/src/util/mod.rs index a5058734..3efdc7c0 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -281,7 +281,7 @@ pub(crate) unsafe fn to_string(state: *mut ffi::lua_State, index: c_int) -> Stri } ffi::LUA_TNUMBER => { let mut isint = 0; - let i = ffi::lua_tointegerx(state, -1, &mut isint); + let i = ffi::lua_tointegerx(state, index, &mut isint); if isint == 0 { ffi::lua_tonumber(state, index).to_string() } else { @@ -348,10 +348,7 @@ pub(crate) unsafe fn ptr_to_lossy_str<'a>(input: *const c_char) -> Option Option { - match n { - n if n < 0 => None, - n => Some(n as usize), - } + usize::try_from(n).ok() } mod error; diff --git a/src/value.rs b/src/value.rs index b4891e73..19023180 100644 --- a/src/value.rs +++ b/src/value.rs @@ -493,14 +493,6 @@ impl Value { // Compares two values. // Used to sort values for Debug printing. pub(crate) fn sort_cmp(&self, other: &Self) -> Ordering { - fn cmp_num(a: Number, b: Number) -> Ordering { - match (a, b) { - _ if a < b => Ordering::Less, - _ if a > b => Ordering::Greater, - _ => Ordering::Equal, - } - } - match (self, other) { // Nil (Value::Nil, Value::Nil) => Ordering::Equal, @@ -516,9 +508,11 @@ impl Value { (_, Value::Boolean(_)) => Ordering::Greater, // Integer && Number (Value::Integer(a), Value::Integer(b)) => a.cmp(b), - (Value::Integer(a), Value::Number(b)) => cmp_num(*a as Number, *b), - (Value::Number(a), Value::Integer(b)) => cmp_num(*a, *b as Number), - (Value::Number(a), Value::Number(b)) => cmp_num(*a, *b), + (Value::Integer(a), Value::Number(b)) => (*a as Number).partial_cmp(b).unwrap_or(Ordering::Equal), + (Value::Number(a), Value::Integer(b)) => { + a.partial_cmp(&(*b as Number)).unwrap_or(Ordering::Equal) + } + (Value::Number(a), Value::Number(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal), (Value::Integer(_) | Value::Number(_), _) => Ordering::Less, (_, Value::Integer(_) | Value::Number(_)) => Ordering::Greater, // Vector (Luau) From 07cf801aa37367132d500da133c0c9ef13ac3269 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 13:59:20 +0100 Subject: [PATCH 103/138] Reuse `Scope::create_any_userdata` in `Scope::create_userdata`. The methods are identical. --- src/scope.rs | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/scope.rs b/src/scope.rs index fa8cafaf..af0621fa 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -156,38 +156,7 @@ impl<'scope, 'env: 'scope> Scope<'scope, 'env> { where T: UserData + 'env, { - let state = self.lua.state(); - unsafe { - let _sg = StackGuard::new(state); - check_stack(state, 3)?; - - // We don't write the data to the userdata until pushing the metatable - let protect = !self.lua.unlikely_memory_error(); - #[cfg(feature = "luau")] - let ud_ptr = { - let data = UserDataStorage::new_scoped(data); - util::push_userdata(state, data, protect)? - }; - #[cfg(not(feature = "luau"))] - let ud_ptr = util::push_uninit_userdata::>(state, protect)?; - - // Push the metatable and register it with no TypeId - let mut registry = UserDataRegistry::new_unique(self.lua.lua(), ud_ptr as *mut _); - T::register(&mut registry); - self.lua.push_userdata_metatable(registry.into_raw())?; - let mt_ptr = ffi::lua_topointer(state, -1); - self.lua.register_userdata_metatable(mt_ptr, None); - - // Write data to the pointer and attach metatable - #[cfg(not(feature = "luau"))] - std::ptr::write(ud_ptr, UserDataStorage::new_scoped(data)); - ffi::lua_setmetatable(state, -2); - - let ud = AnyUserData(self.lua.pop_ref()); - self.seal_userdata::(&ud); - - Ok(ud) - } + self.create_any_userdata(data, T::register) } /// Creates a Lua userdata object from a custom Rust type. From ef4d7003c013a78a4118b4d820007b44dc1c05c2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 19:20:24 +0100 Subject: [PATCH 104/138] Optimize `ValueRef` (heap allocate only on clone) Before this change, by default `ValueRef` allocate memory on heap, even for unique non-shared slots. After we allocate only when `ValueRef` is cloned and the slot becoming shared. --- src/state/extra.rs | 2 +- src/state/raw.rs | 4 +- src/types.rs | 3 - src/types/value_ref.rs | 208 +++++++++++++++++++++++++++++++++++------ 4 files changed, 182 insertions(+), 35 deletions(-) diff --git a/src/state/extra.rs b/src/state/extra.rs index 187a71d7..0064065c 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -65,7 +65,7 @@ pub(crate) struct ExtraData { pub(super) wrapped_failure_top: usize, // Pool of `Thread`s (coroutines) for async execution #[cfg(feature = "async")] - pub(super) thread_pool: Vec, + pub(super) thread_pool: Vec, // Map for implicit threads to root user-owned Thread #[cfg(feature = "async")] pub(super) thread_ownership_map: FxHashMap<*mut ffi::lua_State, *mut ffi::lua_State>, diff --git a/src/state/raw.rs b/src/state/raw.rs index 098a3bec..a84e5c16 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -677,7 +677,7 @@ impl RawLua { #[cfg(feature = "async")] pub(crate) unsafe fn create_recycled_thread(&self, func: &Function) -> Result { if let Some(index) = (*self.extra.get()).thread_pool.pop() { - let thread_state = ffi::lua_tothread(self.ref_thread(), *index.0); + let thread_state = ffi::lua_tothread(self.ref_thread(), index); ffi::lua_xpush(self.ref_thread(), thread_state, func.0.index); #[cfg(feature = "luau")] @@ -716,7 +716,7 @@ impl RawLua { pub(crate) unsafe fn recycle_thread(&self, thread: &mut Thread) { let extra = &mut *self.extra.get(); if extra.thread_pool.len() < extra.thread_pool.capacity() - && let Some(index) = thread.0.index_count.take() + && let Some(index) = thread.0.take_index() { extra.thread_pool.push(index); } diff --git a/src/types.rs b/src/types.rs index 06ecf277..0c512635 100644 --- a/src/types.rs +++ b/src/types.rs @@ -20,9 +20,6 @@ pub use either::Either; pub use registry_key::RegistryKey; pub(crate) use value_ref::ValueRef; -#[cfg(feature = "async")] -pub(crate) use value_ref::ValueRefIndex; - /// Type of Lua integer numbers. pub type Integer = ffi::lua_Integer; /// Type of Lua floating point numbers. diff --git a/src/types/value_ref.rs b/src/types/value_ref.rs index 564c28f0..a6551ebc 100644 --- a/src/types/value_ref.rs +++ b/src/types/value_ref.rs @@ -1,42 +1,34 @@ -use std::fmt; use std::os::raw::{c_int, c_void}; +use std::{fmt, ptr}; use super::XRc; use crate::state::{RawLua, WeakLua}; +use self::ref_count::RefCount; + /// A reference to a Lua (complex) value stored in the Lua auxiliary thread. -#[derive(Clone)] pub struct ValueRef { pub(crate) lua: WeakLua, - // Keep index separate to avoid additional indirection when accessing it. pub(crate) index: c_int, - // If `index_count` is `None`, the value does not need to be destroyed. - pub(crate) index_count: Option, -} - -/// A reference to a Lua value index in the auxiliary thread. -/// It's cheap to clone and can be used to track the number of references to a value. -#[derive(Clone)] -pub(crate) struct ValueRefIndex(pub(crate) XRc); - -impl From for ValueRefIndex { - #[inline] - fn from(index: c_int) -> Self { - ValueRefIndex(XRc::new(index)) - } + count: RefCount, } impl ValueRef { #[inline] - pub(crate) fn new(lua: &RawLua, index: impl Into) -> Self { - let index = index.into(); + pub(crate) fn new(lua: &RawLua, index: c_int) -> Self { ValueRef { lua: lua.weak().clone(), - index: *index.0, - index_count: Some(index), + index, + count: RefCount::unique(), } } + #[cfg(feature = "async")] + #[inline] + pub(crate) fn take_index(&mut self) -> Option { + self.count.take(self.index) + } + #[inline] pub(crate) fn to_pointer(&self) -> *const c_void { let lua = self.lua.lock(); @@ -44,6 +36,17 @@ impl ValueRef { } } +impl Clone for ValueRef { + #[inline] + fn clone(&self) -> Self { + ValueRef { + lua: self.lua.clone(), + index: self.index, + count: self.count.clone_shared(), + } + } +} + impl fmt::Debug for ValueRef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Ref({:p})", self.to_pointer()) @@ -52,14 +55,10 @@ impl fmt::Debug for ValueRef { impl Drop for ValueRef { fn drop(&mut self) { - if let Some(ValueRefIndex(index)) = self.index_count.take() { - // It's guaranteed that the inner value returns exactly once. - // This means in particular that the value is not dropped. - if XRc::into_inner(index).is_some() - && let Some(lua) = self.lua.try_lock() - { - unsafe { lua.drop_ref(self) } - } + if self.count.drop_is_last() + && let Some(lua) = self.lua.try_lock() + { + unsafe { lua.drop_ref(self) } } } } @@ -74,3 +73,154 @@ impl PartialEq for ValueRef { unsafe { ffi::lua_rawequal(lua.ref_thread(), self.index, other.index) == 1 } } } + +// The counter is a pure refcount token. +type Unit = (); +const UNIQUE: *mut Unit = ptr::without_provenance_mut(1); +const NONE: *mut Unit = ptr::null_mut(); + +impl RefCount { + #[inline] + fn unique() -> Self { + Self::from_raw(UNIQUE) + } + + #[inline] + fn clone_shared(&self) -> RefCount { + let mut current = self.load(); + loop { + if current != UNIQUE { + if current != NONE { + unsafe { XRc::increment_strong_count(current as *const Unit) }; + } + return RefCount::from_raw(current); + } + // Lazily allocate the shared counter + let shared = XRc::into_raw(XRc::new(())) as *mut Unit; + match self.promote(shared) { + Ok(()) => { + unsafe { XRc::increment_strong_count(shared as *const Unit) }; + return RefCount::from_raw(shared); + } + Err(actual) => { + unsafe { drop(XRc::from_raw(shared as *const Unit)) }; + current = actual; + } + } + } + } + + /// Takes the slot if it's solely owned by `self`. + /// + /// Returns `None` if the slot is still shared or already non-owning. + #[cfg(feature = "async")] + fn take(&mut self, index: c_int) -> Option { + match self.load() { + current if current == UNIQUE => { + self.swap_none(); + Some(index) + } + current if current == NONE => None, + current => { + // Shared: recyclable only if no other owner remains + let rc = unsafe { XRc::from_raw(current as *const Unit) }; + if XRc::strong_count(&rc) == 1 { + drop(rc); + self.swap_none(); + Some(index) + } else { + let _ = XRc::into_raw(rc); // still shared + None + } + } + } + } + + /// Drops the reference and returns `true` if it was the last owner of the slot + /// (so the slot must be freed). + #[inline] + fn drop_is_last(&mut self) -> bool { + let current = self.load(); + if current == UNIQUE { + true + } else if current == NONE { + false + } else { + unsafe { XRc::into_inner(XRc::from_raw(current as *const Unit)).is_some() } + } + } +} + +#[cfg(feature = "send")] +mod ref_count { + use std::sync::atomic::{AtomicPtr, Ordering}; + + use super::Unit; + + pub(super) struct RefCount(AtomicPtr); + + impl RefCount { + #[inline] + pub(super) fn from_raw(ptr: *mut Unit) -> Self { + RefCount(AtomicPtr::new(ptr)) + } + + #[inline] + pub(super) fn load(&self) -> *mut Unit { + self.0.load(Ordering::Acquire) + } + + /// Replaces the `UNIQUE` tag with the freshly allocated shared counter (`new`). + /// + /// Returns `Err(current)` if another thread promoted first. + #[inline] + pub(super) fn promote(&self, new: *mut Unit) -> Result<(), *mut Unit> { + self.0 + .compare_exchange(super::UNIQUE, new, Ordering::AcqRel, Ordering::Acquire) + .map(|_| ()) + } + + #[cfg(feature = "async")] + #[inline] + pub(super) fn swap_none(&self) -> *mut Unit { + self.0.swap(super::NONE, Ordering::AcqRel) + } + } +} + +#[cfg(not(feature = "send"))] +mod ref_count { + use std::cell::Cell; + + use super::Unit; + + pub(super) struct RefCount(Cell<*mut Unit>); + + impl RefCount { + #[inline] + pub(super) fn from_raw(ptr: *mut Unit) -> Self { + RefCount(Cell::new(ptr)) + } + + #[inline] + pub(super) fn load(&self) -> *mut Unit { + self.0.get() + } + + /// Replaces the `UNIQUE` tag with the freshly allocated shared counter `new`. + /// + /// Never fails. + #[inline] + pub(super) fn promote(&self, new: *mut Unit) -> Result<(), *mut Unit> { + debug_assert_eq!(self.0.get(), super::UNIQUE); + self.0.set(new); + Ok(()) + } + + #[cfg(feature = "async")] + #[inline] + pub(super) fn swap_none(&self) -> *mut Unit { + self.0.replace(super::NONE) + } + } +} From 9a8c7b6f07912ee4521b752a9efe905e2c4c0041 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 22:04:09 +0100 Subject: [PATCH 105/138] clippy --- src/util/mod.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/util/mod.rs b/src/util/mod.rs index 3efdc7c0..899e6964 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -117,13 +117,10 @@ pub(crate) unsafe fn push_external_string( } if protect { - let res = protect_lua!(state, 0, 1, move |state| { + // Lua free external string on error + protect_lua!(state, 0, 1, move |state| { ffi::lua_pushexternalstring(state, s_ptr, s_len, Some(dealloc), bytes_ud as *mut _); - }); - if res.is_err() { - // Lua free external string on error - return res; - } + })?; } else { ffi::lua_pushexternalstring(state, s_ptr, s_len, Some(dealloc), bytes_ud as *mut _); } From 8338b1daac2e71f9a9c24768bd9d2af4270001bf Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 20 Jun 2026 23:10:27 +0100 Subject: [PATCH 106/138] Add `ThreadStatus::Normal` (mimic Lua's coroutine.status). --- src/thread.rs | 35 +++++++++++++++++++++++++++++++++-- tests/thread.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/thread.rs b/src/thread.rs index b1e209ca..885323e3 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -151,6 +151,11 @@ pub enum ThreadStatus { Resumable, /// The thread is currently running. Running, + /// The thread is active but not running. + /// + /// This is the case when the thread has resumed another thread (which has not yet + /// returned or yielded). + Normal, /// The thread has finished executing. Finished, /// The thread has raised a Lua error during execution. @@ -165,6 +170,7 @@ pub enum ThreadStatus { enum ThreadStatusInner { New(c_int), Running, + Normal, Yielded(c_int), Finished, Error, @@ -387,6 +393,7 @@ impl Thread { match self.status_inner(&self.0.lua.lock()) { ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_) => ThreadStatus::Resumable, ThreadStatusInner::Running => ThreadStatus::Running, + ThreadStatusInner::Normal => ThreadStatus::Normal, ThreadStatusInner::Finished => ThreadStatus::Finished, ThreadStatusInner::Error => ThreadStatus::Error, } @@ -403,8 +410,22 @@ impl Thread { let top = unsafe { ffi::lua_gettop(thread_state) }; match status { ffi::LUA_YIELD => ThreadStatusInner::Yielded(top), - ffi::LUA_OK if top > 0 => ThreadStatusInner::New(top - 1), - ffi::LUA_OK => ThreadStatusInner::Finished, + ffi::LUA_OK => { + // Active call frames mean this thread has resumed another (still-running) thread. + // Without frames it's new or finished. + let mut ar = const { unsafe { std::mem::zeroed::() } }; + #[cfg(not(feature = "luau"))] + let has_frames = unsafe { ffi::lua_getstack(thread_state, 0, &mut ar) != 0 }; + #[cfg(feature = "luau")] + let has_frames = unsafe { ffi::lua_getinfo(thread_state, 0, cstr!(""), &mut ar) != 0 }; + if has_frames { + ThreadStatusInner::Normal + } else if top > 0 { + ThreadStatusInner::New(top - 1) + } else { + ThreadStatusInner::Finished + } + } _ => ThreadStatusInner::Error, } } @@ -422,6 +443,15 @@ impl Thread { self.status() == ThreadStatus::Running } + /// Returns `true` if this thread is active but not running. + /// + /// This is the case when the thread has resumed another thread that has not yet returned + /// or yielded. + #[inline(always)] + pub fn is_normal(&self) -> bool { + self.status() == ThreadStatus::Normal + } + /// Returns `true` if this thread has finished executing. #[inline(always)] pub fn is_finished(&self) -> bool { @@ -510,6 +540,7 @@ impl Thread { Ok(()) } ThreadStatusInner::Running => Err(Error::runtime("cannot reset a running thread")), + ThreadStatusInner::Normal => Err(Error::runtime("cannot reset a normal thread")), ThreadStatusInner::Finished => Ok(()), #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "luau")))] ThreadStatusInner::Yielded(_) | ThreadStatusInner::Error => { diff --git a/tests/thread.rs b/tests/thread.rs index d0afe433..da55851c 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -105,6 +105,32 @@ fn test_thread() -> Result<()> { let result = thread.resume::<()>(()); assert!(result.is_ok(), "unexpected result: {result:?}"); + // A thread that has resumed another thread (still running) is "normal". + let check_outer = lua.create_function(|lua, ()| { + let outer: Thread = lua.globals().get("outer")?; + assert!(outer.is_normal()); + assert!( + matches!(outer.resume::<()>(()), Err(Error::CoroutineUnresumable)), + "resuming a `normal` thread must be unresumable", + ); + Ok(()) + })?; + lua.globals().set("check_outer", check_outer)?; + let outer = lua.create_thread( + lua.load( + r#" + function() + local inner = coroutine.create(function() check_outer() end) + assert(coroutine.resume(inner)) + end + "#, + ) + .eval()?, + )?; + lua.globals().set("outer", &outer)?; + outer.resume::<()>(())?; + assert!(outer.is_finished()); + Ok(()) } From 73766a48edc5172e0a690e285a4a5d2bd38d304c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 21 Jun 2026 00:25:16 +0100 Subject: [PATCH 107/138] serde: Array metatable always takes precedence When `detect_mixed_tables` option is enabled, tables with explicit array metatables are always encoded as arrays without running the detection. --- src/table.rs | 15 ++++++++++----- tests/serde.rs | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/table.rs b/src/table.rs index b36cb2a0..92f69583 100644 --- a/src/table.rs +++ b/src/table.rs @@ -936,17 +936,22 @@ impl Table { /// Determines if the table should be encoded as an array or a map. /// /// The algorithm is the following: - /// 1. If `detect_mixed_tables` is enabled, iterate over all keys in the table checking is they + /// 1. If the table has the array metatable attached, always encode it as an array. + /// + /// 2. If `detect_mixed_tables` is enabled, iterate over all keys in the table checking is they /// all are positive integers. If non-array key is found, return `None` (encode as map). /// Otherwise check the sparsity of the array. Too sparse arrays are encoded as maps. /// - /// 2. If `detect_mixed_tables` is disabled, check if the table has a positive length or has the - /// array metatable. If so, encode as array. If the table is empty and - /// `encode_empty_tables_as_array` is enabled, encode as array. + /// 3. If `detect_mixed_tables` is disabled, check if the table has a positive length. If so, + /// encode as array. If the table is empty and `encode_empty_tables_as_array` is enabled, + /// encode as array. /// /// Returns the length of the array if it should be encoded as an array. #[cfg(feature = "serde")] pub(crate) fn encode_as_array(&self, options: crate::serde::de::Options) -> Option { + if self.has_array_metatable() { + return Some(self.raw_len()); + } if options.detect_mixed_tables { if let Some((len, max_idx)) = self.find_array_len() { // If the array is too sparse, serialize it as a map instead @@ -956,7 +961,7 @@ impl Table { } } else { let len = self.raw_len(); - if len > 0 || self.has_array_metatable() { + if len > 0 { return Some(len); } if options.encode_empty_tables_as_array && self.is_empty() { diff --git a/tests/serde.rs b/tests/serde.rs index d3cc00ed..a8c84b6c 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -305,6 +305,12 @@ fn test_serialize_mixed_table() -> LuaResult<()> { let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap(); assert_eq!(json, r#"{"1":1,"2":2,"3":3,"1":"value"}"#); + // Array metatable takes precedence + let table = lua.load(r#"{1,2,3, key="value"}"#).eval::()?; + (table.as_table().unwrap()).set_metatable(Some(lua.array_metatable()))?; + let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap(); + assert_eq!(json, r#"[1,2,3]"#); + Ok(()) } From a3a35cf96317ecf90009eade87f17014e93eacce Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 24 Jun 2026 20:36:24 +0100 Subject: [PATCH 108/138] Add `Value::as_vector`/`Value::is_vector` (Luau) --- src/value.rs | 25 +++++++++++++++++++++++++ tests/luau.rs | 5 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/value.rs b/src/value.rs index 19023180..9a58a2cc 100644 --- a/src/value.rs +++ b/src/value.rs @@ -440,6 +440,31 @@ impl Value { } } + /// Cast the value to a [`Vector`]. + /// + /// If the value is a [`Vector`], returns it or `None` otherwise. + /// + /// [`Vector`]: crate::Vector + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[inline] + pub fn as_vector(&self) -> Option { + match self { + Value::Vector(v) => Some(*v), + _ => None, + } + } + + /// Returns `true` if the value is a [`Vector`]. + /// + /// [`Vector`]: crate::Vector + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[inline] + pub fn is_vector(&self) -> bool { + self.as_vector().is_some() + } + /// Cast the value to a [`Buffer`]. /// /// If the value is [`Buffer`], returns it or `None` otherwise. diff --git a/tests/luau.rs b/tests/luau.rs index 770640b9..b2c8387a 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -60,10 +60,11 @@ fn test_vectors() -> Result<()> { fn test_vectors() -> Result<()> { let lua = Lua::new(); - let v: Vector = lua + let v: Value = lua .load("vector.create(1, 2, 3, 4) + vector.create(4, 3, 2, 1)") .eval()?; - assert_eq!(v, [5.0, 5.0, 5.0, 5.0]); + assert!(v.is_vector()); + assert_eq!(v.as_vector(), Some([5.0, 5.0, 5.0, 5.0])); // Test conversion into Rust array let v: [f64; 4] = lua.load("vector.create(1, 2, 3, 4)").eval()?; From 66557f860c4065f7d661dd30f3553ef458b82897 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 24 Jun 2026 20:37:39 +0100 Subject: [PATCH 109/138] Add Buffer to prelude (Luau) --- src/prelude.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prelude.rs b/src/prelude.rs index fb571e4b..7cb6df72 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -30,7 +30,7 @@ pub use crate::state::GcGenParams as LuaGcGenParams; #[cfg(feature = "luau")] #[doc(no_inline)] pub use crate::{ - Vector as LuaVector, + Buffer as LuaBuffer, Vector as LuaVector, chunk::{CompileConstant as LuaCompileConstant, Compiler as LuaCompiler}, luau::{ FsRequirer as LuaFsRequirer, HeapDump as LuaHeapDump, NavigateError as LuaNavigateError, From a9b4c3e543676e0cdbfe083c62594afdf0020efd Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 24 Jun 2026 20:40:32 +0100 Subject: [PATCH 110/138] impl Hash for BorrowedStr/BorrowedBytes --- src/string.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/string.rs b/src/string.rs index 63ceaf3f..18a40807 100644 --- a/src/string.rs +++ b/src/string.rs @@ -268,6 +268,12 @@ impl AsRef for BorrowedStr { } } +impl Hash for BorrowedStr { + fn hash(&self, state: &mut H) { + self.buf.hash(state); + } +} + impl fmt::Display for BorrowedStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) @@ -349,6 +355,12 @@ impl AsRef<[u8]> for BorrowedBytes { } } +impl Hash for BorrowedBytes { + fn hash(&self, state: &mut H) { + self.buf.hash(state); + } +} + impl fmt::Debug for BorrowedBytes { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) From 7fbef5afc2e1118db4ebcd8f3ca14a720b427a85 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Wed, 24 Jun 2026 20:42:23 +0100 Subject: [PATCH 111/138] Remove deprecated Value::as_str/as_string_lossy --- src/value.rs | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/value.rs b/src/value.rs index 9a58a2cc..ebb46f52 100644 --- a/src/value.rs +++ b/src/value.rs @@ -7,7 +7,7 @@ use num_traits::FromPrimitive; use crate::error::{Error, Result}; use crate::function::Function; -use crate::string::{BorrowedStr, LuaString}; +use crate::string::LuaString; use crate::table::Table; use crate::thread::Thread; use crate::types::{Integer, LightUserData, Number, ValueRef}; @@ -347,31 +347,6 @@ impl Value { } } - /// Cast the value to [`BorrowedStr`]. - /// - /// If the value is a [`LuaString`], try to convert it to [`BorrowedStr`] or return `None` - /// otherwise. - #[deprecated( - since = "0.11.0", - note = "This method does not follow Rust naming convention. Use `as_string().and_then(|s| s.to_str().ok())` instead." - )] - #[inline] - pub fn as_str(&self) -> Option { - self.as_string().and_then(|s| s.to_str().ok()) - } - - /// Cast the value to [`String`]. - /// - /// If the value is a [`LuaString`], converts it to [`String`] or returns `None` otherwise. - #[deprecated( - since = "0.11.0", - note = "This method does not follow Rust naming convention. Use `as_string().map(|s| s.to_string_lossy())` instead." - )] - #[inline] - pub fn as_string_lossy(&self) -> Option { - self.as_string().map(|s| s.to_string_lossy()) - } - /// Returns `true` if the value is a Lua [`Table`]. #[inline] pub fn is_table(&self) -> bool { From a1bc8181b5049a2c1b50cdc99cecde14d9c814d6 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 26 Jun 2026 23:03:49 +0100 Subject: [PATCH 112/138] mlua_derive(macros): Allow taking both `Lua` and `&Lua` in async functions. Async functions/methods by default receive owned `Lua` variant. When user want to take `&Lua`, the code fails with "arguments to this function are incorrect". We can dynamically detect "owned" or "ref" varians and pass corresponding type. Closes #713 --- mlua_derive/src/userdata/userdata_impl.rs | 69 +++++++++++++++-------- tests/userdata_macro.rs | 28 +++++++++ 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 5c9d184b..44de91a1 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -38,7 +38,7 @@ struct ArgInfo { struct MethodInfo { self_kind: SelfKind, - has_lua: bool, + lua: Option, args: Vec, } @@ -50,18 +50,31 @@ fn ref_inner_type(ty: &Type) -> Type { } } -/// Check if the type is `&Lua` or `&mlua::Lua`. -fn is_lua_ref(ty: &Type) -> bool { - let Type::Reference(ref_ty) = ty else { return false }; - match &*ref_ty.elem { - Type::Path(p) if p.path.segments.len() == 1 => p.path.segments[0].ident == "Lua", - Type::Path(p) if p.path.segments.len() == 2 => { - p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua" - } +/// How the `Lua` context parameter is passed to the method. +enum LuaArg { + Ref, + Owned, +} + +/// Check if the type is `Lua` or `mlua::Lua`. +fn is_lua_type(ty: &Type) -> bool { + let Type::Path(p) = ty else { return false }; + match p.path.segments.len() { + 1 => p.path.segments[0].ident == "Lua", + 2 => p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua", _ => false, } } +/// Classify the method's `Lua` context parameter, if present. +fn lua_arg_kind(ty: &Type) -> Option { + match ty { + Type::Reference(r) if r.mutability.is_none() && is_lua_type(&r.elem) => Some(LuaArg::Ref), + ty if is_lua_type(ty) => Some(LuaArg::Owned), + _ => None, + } +} + /// Classify a `&[mut] T` parameter, returning the callback wrapper type. /// /// Known borrow types come from the mapping table `BORROW_WRAPPERS`. @@ -128,10 +141,10 @@ fn try_unwrap_option(ty: &Type) -> Option<&Type> { /// Analyze method signature. /// /// Determine `self` kind and collect the callback arguments. -/// Auto-detects `&Lua` as the first non-self parameter. +/// Auto-detects `Lua` (owned or reference) as the first non-self parameter. fn analyze_self_and_args(sig: &Signature) -> syn::Result { let mut self_kind = SelfKind::None; - let mut has_lua = false; + let mut lua = None; let mut args = Vec::new(); let mut check_first_typed = true; @@ -147,12 +160,14 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result { self_kind = SelfKind::Owned; } FnArg::Typed(typed) => { - if check_first_typed && is_lua_ref(&typed.ty) { - has_lua = true; + if check_first_typed { check_first_typed = false; - continue; + if let Some(kind) = lua_arg_kind(&typed.ty) { + lua = Some(kind); + continue; + } } - check_first_typed = false; + if let syn::Pat::Ident(pat_ident) = &*typed.pat { let arg_type = &*typed.ty; let mut option_inner = None; @@ -202,11 +217,7 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result { } } - Ok(MethodInfo { - self_kind, - has_lua, - args, - }) + Ok(MethodInfo { self_kind, lua, args }) } fn strip_item_attrs(attrs: &[Attribute]) -> Vec { @@ -343,6 +354,16 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let info = try_compile!(analyze_self_and_args(&method.sig)); let is_async = method.sig.asyncness.is_some(); + // Owned `Lua` is only available to async callbacks. + if !is_async && matches!(info.lua, Some(LuaArg::Owned)) { + return syn::Error::new_spanned( + &method.sig, + "owned `Lua` parameter is only supported for `async` methods (use `&Lua` instead)", + ) + .to_compile_error() + .into(); + } + if lua_attr.getter { if is_async { return syn::Error::new_spanned(&method.sig, "async field getter is not supported") @@ -520,7 +541,7 @@ fn gen_call_args(info: &MethodInfo) -> TokenStream2 { _ => call_args.push(quote! { this }), } - if info.has_lua { + if info.lua.is_some() { call_args.push(quote! { lua }); } @@ -542,8 +563,10 @@ fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { SelfKind::Owned => call_args.push(quote! { this }), } - if info.has_lua { - call_args.push(quote! { lua }); + match info.lua { + Some(LuaArg::Ref) => call_args.push(quote! { &lua }), + Some(LuaArg::Owned) => call_args.push(quote! { lua }), + None => {} } for arg in &info.args { diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index 61c305dc..18a0b9ba 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -452,6 +452,14 @@ mod async_tests { Ok(42) } + async fn lua_version(lua: &Lua, extra: Option<&str>) -> Result { + (lua.globals().get("_VERSION")).map(|s: String| s + extra.unwrap_or("")) + } + + async fn lua_version_owned(lua: Lua) -> Result { + lua.globals().get("_VERSION") + } + #[cfg(not(any(feature = "lua51", feature = "luau")))] #[lua(meta)] async fn __tostring(&self) -> Result { @@ -483,6 +491,26 @@ mod async_tests { .unwrap(); } + #[tokio::test] + async fn test_async_lua_param() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + assert(type(AsyncCounter.lua_version()) == "string") + assert(string.sub(AsyncCounter.lua_version(" extra"), -5) == "extra", "expected 'extra' at the end") + assert(type(AsyncCounter.lua_version_owned()) == "string") + "#, + ) + .exec_async() + .await + .unwrap(); + } + #[tokio::test] async fn test_async_consume() { let lua = Lua::new(); From 82111f0ff73008f5f7f95ed078394e8a64bff9f5 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 26 Jun 2026 23:28:10 +0100 Subject: [PATCH 113/138] Fix tests --- tests/luau.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/luau.rs b/tests/luau.rs index b2c8387a..b95aadec 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -20,10 +20,11 @@ fn test_version() -> Result<()> { fn test_vectors() -> Result<()> { let lua = Lua::new(); - let v: Vector = lua + let v: Value = lua .load("vector.create(1, 2, 3) + vector.create(3, 2, 1)") .eval()?; - assert_eq!(v, [4.0, 4.0, 4.0]); + assert!(v.is_vector()); + assert_eq!(v.as_vector().unwrap(), [4.0, 4.0, 4.0]); // Test conversion into Rust array let v: [f64; 3] = lua.load("vector.create(1, 2, 3)").eval()?; @@ -64,7 +65,7 @@ fn test_vectors() -> Result<()> { .load("vector.create(1, 2, 3, 4) + vector.create(4, 3, 2, 1)") .eval()?; assert!(v.is_vector()); - assert_eq!(v.as_vector(), Some([5.0, 5.0, 5.0, 5.0])); + assert_eq!(v.as_vector().unwrap(), [5.0, 5.0, 5.0, 5.0]); // Test conversion into Rust array let v: [f64; 4] = lua.load("vector.create(1, 2, 3, 4)").eval()?; @@ -98,7 +99,6 @@ fn test_vectors() -> Result<()> { Ok(()) } -#[cfg(not(feature = "luau-vector4"))] #[test] fn test_vector_metatable() -> Result<()> { let lua = Lua::new(); From effc89f6b20c5f25049b639a411ae4291a61e56c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 27 Jun 2026 15:13:26 +0100 Subject: [PATCH 114/138] Add `Lua::set_jit_options` with support of Luau JIT inliner Requires Luau 0.727+ --- mlua-sys/Cargo.toml | 2 +- mlua-sys/src/luau/compat.rs | 33 +++++++++++++++++++--- mlua-sys/src/luau/luacodegen.rs | 3 ++ src/chunk.rs | 2 +- src/lib.rs | 3 ++ src/prelude.rs | 6 +++- src/state.rs | 49 +++++++++++++++++++++++++++++++++ tests/luau.rs | 26 +++++++++++++++++ 8 files changed, 117 insertions(+), 7 deletions(-) diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index 0340ded4..27d7ba1b 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -43,7 +43,7 @@ cfg-if = "1.0" pkg-config = "0.3.17" lua-src = { version = ">= 550.1.0, < 550.2.0", optional = true } luajit-src = { version = ">= 210.7.0, < 210.8.0", optional = true } -luau0-src = { version = "0.20.0", optional = true } +luau0-src = { version = "0.20.6", optional = true } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/src/luau/compat.rs b/mlua-sys/src/luau/compat.rs index bf2a7b95..c284d2e9 100644 --- a/mlua-sys/src/luau/compat.rs +++ b/mlua-sys/src/luau/compat.rs @@ -12,6 +12,11 @@ use super::luacode::*; pub const LUA_RESUMEERROR: c_int = -1; +// Keep in sync with Bytecode.h +const LBC_VERSION_MAX: u8 = 11; +const LBC_TYPE_VERSION_MIN: u8 = 1; +const LBC_TYPE_VERSION_MAX: u8 = 3; + unsafe fn compat53_reverse(L: *mut lua_State, mut a: c_int, mut b: c_int) { while a < b { lua_pushvalue(L, a); @@ -368,6 +373,24 @@ pub unsafe fn luaL_newmetatable(L: *mut lua_State, tname: *const c_char) -> c_in } } +// Detects whether a chunk is Luau bytecode or text source. +pub unsafe fn luaL_isbytecode(data: *const c_char, size: usize) -> bool { + if size == 0 { + return false; + } + match *data as u8 { + b if b < b'\t' => true, // bytecode + b if b <= LBC_VERSION_MAX => { + let types_version = (size >= 2).then(|| *data.add(1) as u8); + match types_version { + Some(LBC_TYPE_VERSION_MIN..=LBC_TYPE_VERSION_MAX) => true, // bytecode + _ => false, // text + } + } + _ => false, // text + } +} + pub unsafe fn luaL_loadbufferenv( L: *mut lua_State, data: *const c_char, @@ -384,19 +407,21 @@ pub unsafe fn luaL_loadbufferenv( free(*(data as *mut *mut c_char) as *mut c_void); } - let chunk_is_text = size == 0 || (*data as u8) >= b'\t'; + let is_bytecode = luaL_isbytecode(data, size); if !mode.is_null() { let modeb = CStr::from_ptr(mode).to_bytes(); - if !chunk_is_text && !modeb.contains(&b'b') { + let allow_binary = modeb.contains(&b'b'); + let allow_text = modeb.contains(&b't'); + if is_bytecode && !allow_binary { lua_pushfstring(L, cstr!("attempt to load a binary chunk (mode is '%s')"), mode); return LUA_ERRSYNTAX; - } else if chunk_is_text && !modeb.contains(&b't') { + } else if !is_bytecode && !allow_text { lua_pushfstring(L, cstr!("attempt to load a text chunk (mode is '%s')"), mode); return LUA_ERRSYNTAX; } } - let status = if chunk_is_text { + let status = if !is_bytecode { if env < 0 { env -= 1; } diff --git a/mlua-sys/src/luau/luacodegen.rs b/mlua-sys/src/luau/luacodegen.rs index 9e063ed2..0802c2c0 100644 --- a/mlua-sys/src/luau/luacodegen.rs +++ b/mlua-sys/src/luau/luacodegen.rs @@ -8,4 +8,7 @@ unsafe extern "C-unwind" { pub fn luau_codegen_supported() -> c_int; pub fn luau_codegen_create(state: *mut lua_State); pub fn luau_codegen_compile(state: *mut lua_State, idx: c_int); + + pub fn luau_enable_jit_inliner(state: *mut lua_State); + pub fn luau_disable_jit_inliner(state: *mut lua_State); } diff --git a/src/chunk.rs b/src/chunk.rs index b3c84652..ddc41a54 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -751,7 +751,7 @@ impl Chunk<'_> { return ChunkMode::Binary; } #[cfg(feature = "luau")] - if *source.first().unwrap_or(&u8::MAX) < b'\t' { + if unsafe { ffi::luaL_isbytecode(source.as_ptr().cast(), source.len()) } { return ChunkMode::Binary; } } diff --git a/src/lib.rs b/src/lib.rs index 5472e28f..795dd16c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,6 +153,9 @@ pub use crate::userdata::{ #[doc(inline)] pub use crate::debug::HookTriggers; +#[cfg(any(feature = "luau-jit", doc))] +#[doc(inline)] +pub use crate::state::JitOptions; #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub use crate::{buffer::Buffer, vector::Vector}; diff --git a/src/prelude.rs b/src/prelude.rs index 7cb6df72..83902c03 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -27,7 +27,7 @@ pub use crate::HookTriggers as LuaHookTriggers; #[doc(no_inline)] pub use crate::state::GcGenParams as LuaGcGenParams; -#[cfg(feature = "luau")] +#[cfg(any(feature = "luau", doc))] #[doc(no_inline)] pub use crate::{ Buffer as LuaBuffer, Vector as LuaVector, @@ -38,6 +38,10 @@ pub use crate::{ }, }; +#[cfg(any(feature = "luau-jit", doc))] +#[doc(no_inline)] +pub use crate::state::JitOptions as LuaJitOptions; + #[cfg(feature = "async")] #[doc(no_inline)] pub use crate::{function::LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThread}; diff --git a/src/state.rs b/src/state.rs index 2183cee5..de5d521b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -255,6 +255,38 @@ impl LuaOptions { } } +/// Luau JIT options +#[cfg(any(feature = "luau-jit", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "luau-jit")))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct JitOptions { + inliner: bool, +} + +#[cfg(any(feature = "luau-jit", doc))] +impl Default for JitOptions { + fn default() -> Self { + const { Self::new() } + } +} + +#[cfg(any(feature = "luau-jit", doc))] +impl JitOptions { + /// Creates default JIT options. + pub const fn new() -> Self { + JitOptions { inliner: false } + } + + /// Toggles the runtime bytecode inliner. + /// + /// Disabled by default. Changing this option does not affect already loaded functions. + #[must_use] + pub const fn set_inliner(mut self, enabled: bool) -> Self { + self.inliner = enabled; + self + } +} + impl Drop for Lua { fn drop(&mut self) { if self.collect_garbage { @@ -1251,6 +1283,23 @@ impl Lua { unsafe { (*lua.extra.get()).enable_jit = enable }; } + /// Configures JIT options for this Lua VM. + #[cfg(any(feature = "luau-jit", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau-jit")))] + pub fn set_jit_options(&self, options: JitOptions) { + let lua = self.lock(); + unsafe { + let state = lua.main_state(); + if options.inliner { + let _ = Self::set_fflag("LuauCallFeedback", true); + let _ = Self::set_fflag("LuauEmitCallFeedback", true); + ffi::luau_enable_jit_inliner(state); + } else { + ffi::luau_disable_jit_inliner(state); + } + } + } + /// Sets Luau feature flag (global setting). /// /// See https://github.com/luau-lang/luau/blob/master/CONTRIBUTING.md#feature-flags for details. diff --git a/tests/luau.rs b/tests/luau.rs index b95aadec..b9ca1e07 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -358,6 +358,32 @@ fn test_fflags() { assert!(Lua::set_fflag("UnknownFlag", true).is_err()); } +#[cfg(feature = "luau-jit")] +#[test] +fn test_jit_inliner() -> Result<()> { + let lua = Lua::new(); + lua.set_jit_options(mlua::JitOptions::new().set_inliner(true)); + + // An inlinable helper called in a hot loop. + let sum = lua + .load( + r#" + local function add(a, b) + return a + b + end + local sum = 0 + for i = 1, 1000 do + sum = add(sum, i) + end + return sum + "#, + ) + .eval::()?; + assert_eq!(sum, 500500); + + Ok(()) +} + #[test] fn test_loadstring() -> Result<()> { let lua = Lua::new(); From 5bf131ecc7f0a3e6b54a4c55416c47e53b8b0d5c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 27 Jun 2026 15:23:30 +0100 Subject: [PATCH 115/138] Add missing "doc" cfg to `Thread::resume_error` --- src/thread.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/thread.rs b/src/thread.rs index 885323e3..c413d4d5 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -314,7 +314,7 @@ impl Thread { /// Resumes execution of this thread, immediately raising an error. /// /// This is a Luau specific extension. - #[cfg(feature = "luau")] + #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub fn resume_error(&self, error: impl crate::IntoLua) -> Result where From dc24217320680635e1563e91818bc5aff9bf3ecd Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 27 Jun 2026 15:25:30 +0100 Subject: [PATCH 116/138] Add `#[must_use]` to `HookTriggers`/`GcIncParams`/`GcGenParams`/`ThreadTriggers` builders --- src/debug.rs | 4 ++++ src/state.rs | 7 +++++++ src/thread.rs | 3 +++ 3 files changed, 14 insertions(+) diff --git a/src/debug.rs b/src/debug.rs index 6d4eb77e..3ee7a23c 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -309,6 +309,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`on_calls`] trigger set. /// /// [`on_calls`]: #structfield.on_calls + #[must_use] pub const fn on_calls(mut self) -> Self { self.on_calls = true; self @@ -317,6 +318,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`on_returns`] trigger set. /// /// [`on_returns`]: #structfield.on_returns + #[must_use] pub const fn on_returns(mut self) -> Self { self.on_returns = true; self @@ -325,6 +327,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`every_line`] trigger set. /// /// [`every_line`]: #structfield.every_line + #[must_use] pub const fn every_line(mut self) -> Self { self.every_line = true; self @@ -333,6 +336,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`every_nth_instruction`] trigger set. /// /// [`every_nth_instruction`]: #structfield.every_nth_instruction + #[must_use] pub const fn every_nth_instruction(mut self, n: u32) -> Self { self.every_nth_instruction = Some(n); self diff --git a/src/state.rs b/src/state.rs index de5d521b..cfb4ea82 100644 --- a/src/state.rs +++ b/src/state.rs @@ -100,6 +100,7 @@ impl GcIncParams { /// Sets the `pause` parameter. #[cfg(not(feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] + #[must_use] pub fn pause(mut self, v: c_int) -> Self { self.pause = Some(v); self @@ -108,12 +109,14 @@ impl GcIncParams { /// Sets the `goal` parameter. #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[must_use] pub fn goal(mut self, v: c_int) -> Self { self.goal = Some(v); self } /// Sets the `step_multiplier` parameter. + #[must_use] pub fn step_multiplier(mut self, v: c_int) -> Self { self.step_multiplier = Some(v); self @@ -122,6 +125,7 @@ impl GcIncParams { /// Sets the `step_size` parameter. #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] + #[must_use] pub fn step_size(mut self, v: c_int) -> Self { self.step_size = Some(v); self @@ -155,12 +159,14 @@ pub struct GcGenParams { #[cfg(any(feature = "lua55", feature = "lua54"))] impl GcGenParams { /// Sets the `minor_multiplier` parameter. + #[must_use] pub fn minor_multiplier(mut self, v: c_int) -> Self { self.minor_multiplier = Some(v); self } /// Sets the `minor_to_major` threshold. + #[must_use] pub fn minor_to_major(mut self, v: c_int) -> Self { self.minor_to_major = Some(v); self @@ -169,6 +175,7 @@ impl GcGenParams { /// Sets the `major_to_minor` parameter. #[cfg(feature = "lua55")] #[cfg_attr(docsrs, doc(cfg(feature = "lua55")))] + #[must_use] pub fn major_to_minor(mut self, v: c_int) -> Self { self.major_to_minor = Some(v); self diff --git a/src/thread.rs b/src/thread.rs index c413d4d5..90a2a778 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -95,18 +95,21 @@ impl ThreadTriggers { } /// Returns an instance of `ThreadTriggers` with `on_create` trigger set. + #[must_use] pub const fn on_create(mut self) -> Self { self.on_create = true; self } /// Returns an instance of `ThreadTriggers` with `on_resume` trigger set. + #[must_use] pub const fn on_resume(mut self) -> Self { self.on_resume = true; self } /// Returns an instance of `ThreadTriggers` with `on_yield` trigger set. + #[must_use] pub const fn on_yield(mut self) -> Self { self.on_yield = true; self From d904423fabc230881ef6421610e0d9553b61d503 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 27 Jun 2026 20:01:40 +0100 Subject: [PATCH 117/138] Update docsrs attrs --- src/function.rs | 1 + src/userdata.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/function.rs b/src/function.rs index 7da31b7e..d0dbd8b8 100644 --- a/src/function.rs +++ b/src/function.rs @@ -795,6 +795,7 @@ pub trait LuaNativeFnMut { /// A trait for types that returns a future and can be used as Lua functions. #[cfg(feature = "async")] +#[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub trait LuaNativeAsyncFn { type Output; diff --git a/src/userdata.rs b/src/userdata.rs index 41c166a1..3b6b3b1c 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -490,7 +490,10 @@ pub trait UserDataMethods { /// /// [`add_meta_method_mut`]: UserDataMethods::add_meta_method_mut #[cfg(all(feature = "async", not(any(feature = "lua51", feature = "luau"))))] - #[cfg_attr(docsrs, doc(cfg(feature = "async")))] + #[cfg_attr( + docsrs, + doc(cfg(all(feature = "async", not(any(feature = "lua51", feature = "luau"))))) + )] fn add_async_meta_method_mut(&mut self, name: impl Into, method: M) where T: 'static, From d6784a7e1063d673f234bb20a315f6276f88147a Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Thu, 2 Jul 2026 09:20:20 +0100 Subject: [PATCH 118/138] Update `Error::downcast_ref` to follow `Error::chain` `Error::downcast_ref` now descends through `BadArgument` and `CallbackError`, matching `chain` traversal logic --- src/error.rs | 4 +++- tests/error.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 6fd21ca6..d41f2df5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -359,7 +359,9 @@ impl Error { { match self { Error::ExternalError(err) => err.downcast_ref(), - Error::WithContext { cause, .. } => Self::downcast_ref(cause), + Error::BadArgument { cause, .. } + | Error::CallbackError { cause, .. } + | Error::WithContext { cause, .. } => Self::downcast_ref(cause), _ => None, } } diff --git a/tests/error.rs b/tests/error.rs index 6f70f770..8023dd1e 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -1,4 +1,5 @@ use std::error::Error as _; +use std::sync::Arc; use std::{fmt, io}; use mlua::{Error, ErrorContext, Lua, Result}; @@ -77,6 +78,31 @@ fn test_error_chain() -> Result<()> { Ok(()) } +#[test] +fn test_error_downcast() -> Result<()> { + let lua = Lua::new(); + + let func = lua.create_function(|_, ()| Err::<(), _>(Error::external(io::Error::other("boom"))))?; + let err = func.call::<()>(()).unwrap_err(); + assert!(matches!(err, Error::CallbackError { .. })); + assert!(err.downcast_ref::().is_some()); + assert!(err.downcast_ref::().is_none()); + + let bad_arg = Error::BadArgument { + to: Some("func".to_string()), + pos: 1, + name: Some("arg".to_string()), + cause: Arc::new(Error::external(io::Error::new( + io::ErrorKind::InvalidInput, + "bad", + ))), + }; + assert!(bad_arg.downcast_ref::().is_some()); + assert!(bad_arg.downcast_ref::().is_none()); + + Ok(()) +} + #[test] fn test_external_error() { // `Error::external` should preserve `mlua::Error` From 406c18f143842ded08f044c48f37d9ee583a6098 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Thu, 2 Jul 2026 22:14:48 +0100 Subject: [PATCH 119/138] Add `CoverageInfo` to prelude --- src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/prelude.rs b/src/prelude.rs index 83902c03..8ecb7fe0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -32,6 +32,7 @@ pub use crate::state::GcGenParams as LuaGcGenParams; pub use crate::{ Buffer as LuaBuffer, Vector as LuaVector, chunk::{CompileConstant as LuaCompileConstant, Compiler as LuaCompiler}, + function::CoverageInfo as LuaCoverageInfo, luau::{ FsRequirer as LuaFsRequirer, HeapDump as LuaHeapDump, NavigateError as LuaNavigateError, Require as LuaRequire, From ec26d3d7c254a5d355c4b9a310916ac307e08120 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sat, 4 Jul 2026 23:11:00 +0100 Subject: [PATCH 120/138] Fix stack underflow in thread event callbacks Introduce a new guard to reject resuming/resetting active thread from within it's own callback. Before it was possible to mutate the thread stack and making xmove to move more values than remain on the thread stack. --- src/state.rs | 8 +- src/state/extra.rs | 2 + src/state/raw.rs | 18 ++-- src/thread.rs | 201 ++++++++++++++++++++++++++++----------------- tests/thread.rs | 39 ++++++++- 5 files changed, 186 insertions(+), 82 deletions(-) diff --git a/src/state.rs b/src/state.rs index cfb4ea82..ec7db0b4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -889,6 +889,9 @@ impl Lua { /// Only one callback can be registered at a time. Calling this again replaces the previous /// callback and its triggers. /// + /// If the callback returns an error, it's propagated out of the operation that triggered the + /// event. For a [`ThreadEvent::Yield`], the yielded values are discarded. + /// /// # Example /// /// Subscribe only to yield events: @@ -949,17 +952,18 @@ impl Lua { } let extra = ExtraData::get(child); - if !(*extra).thread_triggers.on_create { + if !(*extra).thread_triggers.on_create || !(*extra).thread_event_state.is_null() { return; } let callback = match &(*extra).thread_event_callback { - Some(cb) if XRc::strong_count(cb) == 1 => cb.clone(), + Some(cb) => cb.clone(), _ => return, }; ffi::lua_pushthread(child); ffi::lua_xmove(child, (*extra).ref_thread, 1); let thread = Thread((*extra).raw_lua().pop_ref_thread(), child); callback_error_ext(parent, extra, false, move |extra, _| { + let _guard = crate::thread::ThreadEventGuard::new((*extra).raw_lua(), child); callback((*extra).lua(), ThreadEvent::Create(thread)) }) } diff --git a/src/state/extra.rs b/src/state/extra.rs index 0064065c..9391195c 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -87,6 +87,7 @@ pub(crate) struct ExtraData { pub(super) interrupt_callback: Option, pub(super) thread_triggers: ThreadTriggers, pub(super) thread_event_callback: Option, + pub(super) thread_event_state: *mut ffi::lua_State, #[cfg(feature = "luau")] pub(crate) running_gc: bool, @@ -192,6 +193,7 @@ impl ExtraData { interrupt_callback: None, thread_triggers: ThreadTriggers::default(), thread_event_callback: None, + thread_event_state: ptr::null_mut(), #[cfg(feature = "luau")] sandboxed: false, #[cfg(feature = "luau")] diff --git a/src/state/raw.rs b/src/state/raw.rs index a84e5c16..5468f6e1 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -659,12 +659,10 @@ impl RawLua { // Exec creation callback for non-Luau (Luau handles this via `userthread_proc`) #[cfg(not(feature = "luau"))] - if self.thread_event_triggers().on_create { + if self.thread_event_triggers().on_create && self.thread_event_state().is_null() { let extra = self.extra.get(); - if let Some(ref cb) = (*extra).thread_event_callback - && XRc::strong_count(cb) == 1 - { - let cb = cb.clone(); + if let Some(cb) = (*extra).thread_event_callback.clone() { + let _guard = crate::thread::ThreadEventGuard::new(self, thread_state); cb((*extra).lua(), crate::thread::ThreadEvent::Create(thread.clone()))?; } } @@ -732,6 +730,16 @@ impl RawLua { (*self.extra.get()).thread_event_callback.clone() } + #[inline(always)] + pub(crate) unsafe fn thread_event_state(&self) -> *mut ffi::lua_State { + (*self.extra.get()).thread_event_state + } + + #[inline(always)] + pub(crate) unsafe fn set_thread_event_state(&self, state: *mut ffi::lua_State) { + (*self.extra.get()).thread_event_state = state; + } + /// Pushes a primitive type value onto the Lua stack. pub(crate) unsafe fn push_primitive_type(&self) -> bool { match T::TYPE_ID { diff --git a/src/thread.rs b/src/thread.rs index 90a2a778..93ba3315 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -42,7 +42,7 @@ use crate::error::{Error, Result}; use crate::function::Function; use crate::state::RawLua; use crate::traits::{FromLuaMulti, IntoLuaMulti}; -use crate::types::{LuaType, ValueRef, XRc}; +use crate::types::{LuaType, ValueRef}; use crate::util::{StackGuard, check_stack, error_traceback_thread, pop_error}; #[cfg(not(feature = "luau"))] @@ -214,6 +214,57 @@ pub struct AsyncThread { recycle: bool, } +pub(crate) struct ThreadEventGuard<'a> { + lua: &'a RawLua, + prev_state: *mut ffi::lua_State, +} + +impl<'a> ThreadEventGuard<'a> { + #[inline] + pub(crate) unsafe fn new(lua: &'a RawLua, thread_state: *mut ffi::lua_State) -> Self { + let guard = ThreadEventGuard { + lua, + prev_state: lua.thread_event_state(), + }; + lua.set_thread_event_state(thread_state); + guard + } +} + +impl Drop for ThreadEventGuard<'_> { + #[inline] + fn drop(&mut self) { + unsafe { self.lua.set_thread_event_state(self.prev_state) }; + } +} + +#[inline] +fn check_thread_reentrancy(thread_state: *mut ffi::lua_State, lua: &RawLua) -> Result<()> { + if thread_state == unsafe { lua.thread_event_state() } { + let err = "cannot resume or reset a thread from within its own event callback"; + return Err(Error::runtime(err)); + } + Ok(()) +} + +#[inline] +unsafe fn exec_thread_event( + lua: &RawLua, + enabled: bool, + thread_state: *mut ffi::lua_State, + event: impl FnOnce() -> ThreadEvent, +) -> Result { + if enabled + && lua.thread_event_state().is_null() + && let Some(cb) = lua.thread_event_callback() + { + let _guard = ThreadEventGuard::new(lua, thread_state); + cb(lua.lua(), event())?; + return Ok(true); + } + Ok(false) +} + impl Thread { /// Returns reference to the Lua state that this thread is associated with. #[inline(always)] @@ -270,22 +321,20 @@ impl Thread { R: FromLuaMulti, { let lua = self.0.lua.lock(); - let mut pushed_nargs = match self.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Err(Error::CoroutineUnresumable), - }; + check_thread_reentrancy(self.state(), &lua)?; + let mut pushed_nargs = self.resumable_nargs(&lua)?; let state = lua.state(); let thread_state = self.state(); unsafe { let _sg = StackGuard::new(state); - // Exec thread resume callback - if lua.thread_event_triggers().on_resume - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Resume(self.clone()))?; + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.clone()) + })? { + pushed_nargs = self.resumable_nargs(&lua)?; } let nargs = args.push_into_stack_multi(&lua)?; @@ -298,18 +347,13 @@ impl Thread { let _thread_sg = StackGuard::with_top(thread_state, 0); let (status, nresults) = self.resume_inner(&lua, pushed_nargs)?; - // Exec thread yield callback - if lua.thread_event_triggers().on_yield - && status.is_yielded() - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Yield(self.clone()))?; - } - check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield && status.is_yielded(); + exec_thread_event(&lua, on_yield, thread_state, || ThreadEvent::Yield(self.clone()))?; + R::from_stack_multi(nresults, &lua) } } @@ -324,6 +368,7 @@ impl Thread { R: FromLuaMulti, { let lua = self.0.lua.lock(); + check_thread_reentrancy(self.state(), &lua)?; match self.status_inner(&lua) { ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_) => {} _ => return Err(Error::CoroutineUnresumable), @@ -335,12 +380,10 @@ impl Thread { let _sg = StackGuard::new(state); // Exec thread resume callback - if lua.thread_event_triggers().on_resume - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Resume(self.clone()))?; - } + let on_resume = lua.thread_event_triggers().on_resume; + exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.clone()) + })?; check_stack(state, 1)?; error.push_into_stack(&lua)?; @@ -349,18 +392,13 @@ impl Thread { let _thread_sg = StackGuard::with_top(thread_state, 0); let (status, nresults) = self.resume_inner(&lua, ffi::LUA_RESUMEERROR)?; - // Exec thread yield callback - if lua.thread_event_triggers().on_yield - && status.is_yielded() - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Yield(self.clone()))?; - } - check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield && status.is_yielded(); + exec_thread_event(&lua, on_yield, thread_state, || ThreadEvent::Yield(self.clone()))?; + R::from_stack_multi(nresults, &lua) } } @@ -433,6 +471,15 @@ impl Thread { } } + /// Returns the number of pending arguments on the thread stack if the thread is resumable. + #[inline] + fn resumable_nargs(&self, lua: &RawLua) -> Result { + match self.status_inner(lua) { + ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => Ok(nargs), + _ => Err(Error::CoroutineUnresumable), + } + } + /// Returns `true` if this thread is resumable (meaning it can be resumed by calling /// [`Thread::resume`]). #[inline(always)] @@ -516,6 +563,7 @@ impl Thread { /// [Lua 5.4]: https://www.lua.org/manual/5.4/manual.html#lua_closethread pub fn reset(&self, func: Function) -> Result<()> { let lua = self.0.lua.lock(); + check_thread_reentrancy(self.state(), &lua)?; let thread_state = self.state(); unsafe { let status = self.status_inner(&lua); @@ -625,6 +673,7 @@ impl Thread { R: FromLuaMulti, { let lua = self.0.lua.lock(); + check_thread_reentrancy(self.state(), &lua)?; if !self.status_inner(&lua).is_resumable() { return Err(Error::CoroutineUnresumable); } @@ -763,9 +812,10 @@ impl Stream for AsyncThread { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let lua = self.thread.0.lua.lock(); - let nargs = match self.thread.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Poll::Ready(None), + check_thread_reentrancy(self.thread.state(), &lua)?; + let mut nargs = match self.thread.resumable_nargs(&lua) { + Ok(nargs) => nargs, + Err(_) => return Poll::Ready(None), }; let state = lua.state(); @@ -775,35 +825,40 @@ impl Stream for AsyncThread { let _thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); - // Exec thread resume callback - if lua.thread_event_triggers().on_resume - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Resume(self.thread.clone()))?; + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.thread.clone()) + })? { + nargs = match self.thread.resumable_nargs(&lua) { + Ok(nargs) => nargs, + Err(_) => return Poll::Ready(None), + }; } let (status, nresults) = (self.thread).resume_inner(&lua, nargs)?; - if status.is_yielded() { + if status.is_yielded() && nresults == 1 && is_poll_pending(thread_state) { // Exec thread yield callback - if lua.thread_event_triggers().on_yield - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Yield(self.thread.clone()))?; - } - - if nresults == 1 && is_poll_pending(thread_state) { - return Poll::Pending; - } - // Continue polling - cx.waker().wake_by_ref(); + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; + return Poll::Pending; } check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); + if status.is_yielded() { + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; + // Continue polling + cx.waker().wake_by_ref(); + } + Poll::Ready(Some(R::from_stack_multi(nresults, &lua))) } } @@ -815,10 +870,8 @@ impl Future for AsyncThread { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let lua = self.thread.0.lua.lock(); - let nargs = match self.thread.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Poll::Ready(Err(Error::CoroutineUnresumable)), - }; + check_thread_reentrancy(self.thread.state(), &lua)?; + let mut nargs = self.thread.resumable_nargs(&lua)?; let state = lua.state(); let thread_state = self.thread.state(); @@ -827,26 +880,26 @@ impl Future for AsyncThread { let _thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); - // Exec thread resume callback - if lua.thread_event_triggers().on_resume - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Resume(self.thread.clone()))?; + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.thread.clone()) + })? { + nargs = self.thread.resumable_nargs(&lua)?; } let (status, nresults) = self.thread.resume_inner(&lua, nargs)?; if status.is_yielded() { + let pending = nresults == 1 && is_poll_pending(thread_state); + // Exec thread yield callback - if lua.thread_event_triggers().on_yield - && let Some(cb) = lua.thread_event_callback() - && XRc::strong_count(&cb) <= 2 - { - cb(lua.lua(), ThreadEvent::Yield(self.thread.clone()))?; - } + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; - if !(nresults == 1 && is_poll_pending(thread_state)) { + if !pending { // Ignore values returned via yield() cx.waker().wake_by_ref(); } diff --git a/tests/thread.rs b/tests/thread.rs index da55851c..2d978fec 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -2,7 +2,7 @@ use std::panic::catch_unwind; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadEvent, ThreadTriggers, Value}; +use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadEvent, ThreadStatus, ThreadTriggers, Value}; #[test] fn test_thread() -> Result<()> { @@ -433,6 +433,43 @@ fn test_thread_event_yield_error() -> Result<()> { Ok(()) } +#[test] +fn test_thread_event_reentrant_resume() -> Result<()> { + let lua = Lua::new(); + + // Self-resume from within the yield callback is not allowed + let reentered = Arc::new(AtomicBool::new(false)); + let reentered2 = reentered.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, event| { + if let ThreadEvent::Yield(thread) = event + && !reentered2.swap(true, Ordering::Relaxed) + { + assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); + let err = thread.resume::(()).unwrap_err(); + assert!( + matches!(&err, Error::RuntimeError(msg) if msg.contains("from within its own event callback")), + "unexpected error: {err:?}" + ); + } + Ok(()) + }); + + let thread = lua.create_thread( + lua.load("coroutine.yield(1, 2, 3, 4, 5) return 7") + .into_function()?, + )?; + + let vals = thread.resume::<(i32, i32, i32, i32, i32)>(())?; + assert_eq!(vals, (1, 2, 3, 4, 5)); + assert!(reentered.load(Ordering::Relaxed)); + + assert_eq!(thread.resume::(())?, 7); + assert!(thread.is_finished()); + + Ok(()) +} + #[test] fn test_thread_event_swap() -> Result<()> { let lua = Lua::new(); From 5c1a73193fd8edbf9b8664cb62f6c49fea36701c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 10:52:37 +0100 Subject: [PATCH 121/138] mlua_derive: Fix static metamethods shifting arguments in userdata_impl --- docs/UserData.md | 24 ++++++++- mlua_derive/src/userdata/userdata_impl.rs | 25 +--------- tests/userdata_macro.rs | 61 ++++++++++++++++++++++- 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/docs/UserData.md b/docs/UserData.md index f46f2144..15280e1a 100644 --- a/docs/UserData.md +++ b/docs/UserData.md @@ -115,7 +115,29 @@ impl MyType { fn __add(&self, other: &Self) -> Self { ... } #[lua(meta, name = "__call", infallible)] - fn construct(lua: &Lua, value: u32) -> Self { ... } + fn construct(this: mlua::AnyUserData, value: u32) -> Self { ... } +} +``` + +A metamethod with a `self` receiver is registered via `add_meta_method` +and behaves like a regular method. A metamethod without a receiver is +registered via `add_meta_function` and receives exactly the values Lua passes. +Declare every argument Lua provides, in order: + +- Binary metamethods (`__add`, `__sub`, `__eq`, `__concat`, ...) receive both + operands, so both must be declared. This is also the way to support reversed + operands (e.g. `2 + obj`), where the userdata is the second argument. +- Method-style metamethods (`__call`, `__index`, `__newindex`, ...) receive the + object as their first argument. When registered without a `self` receiver + (for example a constructor invoked as `MyType(value)`), declare that leading + argument explicitly (typically `mlua::AnyUserData`), even if it is ignored. + +```rust,ignore +#[mlua::userdata_impl] +impl Vec2 { + // No receiver: both operands are declared and passed directly by Lua. + #[lua(meta, infallible, name = "__add")] + fn add(a: &Vec2, b: &Vec2) -> Vec2 { ... } } ``` diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index 44de91a1..c15e29c9 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -654,18 +654,7 @@ fn gen_meta(type_path: &syn::Path, fn_name: &Ident, lua_attr: &LuaAttr, info: &M Ok(name) => name, Err(err) => return err.to_compile_error(), }; - let closure_params = if matches!(info.self_kind, SelfKind::None) { - // Lua always passes `self` to the stack arg, just ignore it. - 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_closure_params(info) - }; + let closure_params = gen_closure_params(info); let call_args = gen_call_args(info); let fn_path = quote! { #type_path::#fn_name }; @@ -761,17 +750,7 @@ fn gen_async_meta( 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 closure_params = gen_async_closure_params(info); let call_args = gen_async_call_args(info); let fn_path = quote! { #type_path::#fn_name }; diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index 18a0b9ba..ee15482e 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -1,6 +1,6 @@ #![cfg(feature = "macros")] -use mlua::{Lua, Result, UserData}; +use mlua::{AnyUserData, Lua, Result, UserData}; #[derive(Default, Clone, Debug, UserData)] struct Rectangle { @@ -412,6 +412,65 @@ fn test_known_borrow_wrappers() -> Result<()> { Ok(()) } +#[derive(Clone, Copy, Debug, PartialEq, UserData)] +struct Vec2 { + x: i32, + y: i32, +} + +#[mlua::userdata_impl] +impl Vec2 { + #[lua(infallible)] + fn new(x: i32, y: i32) -> Self { + Vec2 { x, y } + } + + #[lua(meta, infallible, name = "__add")] + fn add(this: &Vec2, other: &Vec2) -> Vec2 { + Vec2 { + x: this.x + other.x, + y: this.y + other.y, + } + } + + #[lua(meta, infallible, name = "__eq")] + fn eq(this: &Vec2, other: &Vec2) -> bool { + this == other + } + + #[lua(meta, infallible, name = "__call")] + fn call(_lua: &Lua, _proxy: AnyUserData, x: i32, y: i32) -> Vec2 { + Self::new(x, y) + } +} + +#[test] +fn test_static_metamethods() { + let lua = Lua::new(); + lua.globals() + .set("Vec2", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local a = Vec2.new(1, 2) + local b = Vec2.new(3, 4) + + local c = a + b + assert(c.x == 4, "__add x should be 1 + 3 = 4, got " .. tostring(c.x)) + assert(c.y == 6, "__add y should be 2 + 4 = 6, got " .. tostring(c.y)) + + assert(a == Vec2.new(1, 2), "__eq should report vectors equal") + assert(a ~= b, "__eq should report vectors unequal") + + -- `__call` on the proxy + local d = Vec2(7, 14) + assert(d.x == 7 and d.y == 14, "__call should build Vec2(7, 14)") + "#, + ) + .exec() + .unwrap(); +} + #[cfg(feature = "async")] mod async_tests { use mlua::{Lua, Result, UserData}; From 529f8189b28b00528a5e9d7285705d158a4636a2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 11:14:30 +0100 Subject: [PATCH 122/138] mlua_derive: Fix hygiene of callback bindings in userdata_impl --- mlua_derive/src/userdata/userdata_impl.rs | 56 ++++++++++++++--------- tests/userdata_macro.rs | 48 +++++++++++++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index c15e29c9..d78e4e1d 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::spanned::Spanned; use syn::{ @@ -535,14 +535,16 @@ fn gen_arg_token(arg: &ArgInfo) -> TokenStream2 { /// Generate call arguments for invoking the original method. fn gen_call_args(info: &MethodInfo) -> TokenStream2 { let mut call_args: Vec = Vec::new(); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); match info.self_kind { SelfKind::None => {} - _ => call_args.push(quote! { this }), + _ => call_args.push(quote! { #this }), } if info.lua.is_some() { - call_args.push(quote! { lua }); + call_args.push(quote! { #lua }); } for arg in &info.args { @@ -555,17 +557,19 @@ fn gen_call_args(info: &MethodInfo) -> TokenStream2 { /// Generate call arguments for invoking the original async method. fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { let mut call_args: Vec = Vec::new(); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); match info.self_kind { SelfKind::None => {} - SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => call_args.push(quote! { &mut this }), - SelfKind::Ref(_) => call_args.push(quote! { &this }), - SelfKind::Owned => call_args.push(quote! { this }), + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => call_args.push(quote! { &mut #this }), + SelfKind::Ref(_) => call_args.push(quote! { &#this }), + SelfKind::Owned => call_args.push(quote! { #this }), } match info.lua { - Some(LuaArg::Ref) => call_args.push(quote! { &lua }), - Some(LuaArg::Owned) => call_args.push(quote! { lua }), + Some(LuaArg::Ref) => call_args.push(quote! { &#lua }), + Some(LuaArg::Owned) => call_args.push(quote! { #lua }), None => {} } @@ -579,19 +583,25 @@ fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { /// Generate the closure params for the registration callback. fn gen_closure_params(info: &MethodInfo) -> TokenStream2 { let destructure = gen_closure_destructure(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + match info.self_kind { - SelfKind::None => quote! { |lua, #destructure| }, - _ => quote! { |lua, this, #destructure| }, + SelfKind::None => quote! { |#lua, #destructure| }, + _ => quote! { |#lua, #this, #destructure| }, } } /// Generate the closure params for an async registration callback. fn gen_async_closure_params(info: &MethodInfo) -> TokenStream2 { let destructure = gen_closure_destructure(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + match info.self_kind { - SelfKind::None => quote! { |lua, #destructure| }, - SelfKind::Ref(RefKind::Mut) => quote! { |lua, mut this, #destructure| }, - _ => quote! { |lua, this, #destructure| }, + SelfKind::None => quote! { |#lua, #destructure| }, + SelfKind::Ref(RefKind::Mut) => quote! { |#lua, mut #this, #destructure| }, + _ => quote! { |#lua, #this, #destructure| }, } } @@ -603,19 +613,21 @@ fn gen_field_getter( ) -> TokenStream2 { let lua_name = lua_attr.name(fn_name); let call_args = gen_call_args(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); if lua_attr.infallible { return quote! { - registry.add_field_method_get(#lua_name, |lua, this| { - let _ = lua; // silence unused variable warning + registry.add_field_method_get(#lua_name, |#lua, #this| { + let _ = #lua; // silence unused variable warning Ok(#type_path::#fn_name(#call_args)) }); }; } quote! { - registry.add_field_method_get(#lua_name, |lua, this| { - let _ = lua; // silence unused variable warning + registry.add_field_method_get(#lua_name, |#lua, #this| { + let _ = #lua; // silence unused variable warning #type_path::#fn_name(#call_args) }); } @@ -629,12 +641,14 @@ fn gen_field_setter( ) -> TokenStream2 { let lua_name = lua_attr.name(fn_name); let call_args = gen_call_args(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); if lua_attr.infallible { let val_ident = info.args.first().map(|a| &a.ident); return quote! { - registry.add_field_method_set(#lua_name, |lua, this, #val_ident| { - let _ = lua; // silence unused variable warning + registry.add_field_method_set(#lua_name, |#lua, #this, #val_ident| { + let _ = #lua; // silence unused variable warning Ok(#type_path::#fn_name(#call_args)) }); }; @@ -642,8 +656,8 @@ fn gen_field_setter( let val_ident = info.args.first().map(|a| &a.ident); quote! { - registry.add_field_method_set(#lua_name, |lua, this, #val_ident| { - let _ = lua; // silence unused variable warning + registry.add_field_method_set(#lua_name, |#lua, #this, #val_ident| { + let _ = #lua; // silence unused variable warning #type_path::#fn_name(#call_args) }); } diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index ee15482e..8f4ed42b 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -471,6 +471,48 @@ fn test_static_metamethods() { .unwrap(); } +#[derive(Clone, Debug, UserData)] +struct Hygiene { + value: i32, +} + +#[mlua::userdata_impl] +impl Hygiene { + #[lua(infallible)] + fn new(value: i32) -> Self { + Hygiene { value } + } + + // `this` must not clash with the generated receiver binding. + #[lua(infallible)] + fn add_this(&self, this: i32) -> i32 { + self.value + this + } + + // `lua` must not clash either. + #[lua(infallible)] + fn add_lua(&self, lua: i32) -> i32 { + self.value + lua + } +} + +#[test] +fn test_param_name_hygiene() { + let lua = Lua::new(); + lua.globals() + .set("Hygiene", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local h = Hygiene.new(10) + assert(h:add_this(5) == 15, "add_this should be 10 + 5 = 15") + assert(h:add_lua(3) == 13, "add_lua should be 10 + 3 = 13") + "#, + ) + .exec() + .unwrap(); +} + #[cfg(feature = "async")] mod async_tests { use mlua::{Lua, Result, UserData}; @@ -507,6 +549,10 @@ mod async_tests { Ok(self.0 * factor) } + async fn add(&self, this: u64, lua: u64) -> Result { + Ok(self.0 + this + lua) + } + async fn default_value() -> Result { Ok(42) } @@ -541,6 +587,8 @@ mod async_tests { assert(val == 10, "expected 10, got " .. tostring(val)) local doubled = c:multiply(3) assert(doubled == 30, "expected 30, got " .. tostring(doubled)) + local summed = c:add(1, 2) + assert(summed == 13, "expected 10 + 1 + 2 = 13, got " .. tostring(summed)) local inf = c:get_value_infallible() assert(inf == 10, "expected infallible 10, got " .. tostring(inf)) "#, From 6c1d336a5d5d29be024d26680c09a6499eb3553e Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 11:32:29 +0100 Subject: [PATCH 123/138] mlua_derive: Strip raw-identifier prefix from Lua names --- mlua_derive/src/from_lua.rs | 3 ++- mlua_derive/src/userdata/attr.rs | 5 +++-- mlua_derive/src/userdata/mod.rs | 3 ++- tests/userdata_macro.rs | 17 ++++++++++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/mlua_derive/src/from_lua.rs b/mlua_derive/src/from_lua.rs index 4a0df519..16556caa 100644 --- a/mlua_derive/src/from_lua.rs +++ b/mlua_derive/src/from_lua.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use quote::quote; +use syn::ext::IdentExt; use syn::{DeriveInput, parse_macro_input, parse_quote}; pub fn from_lua(input: TokenStream) -> TokenStream { @@ -7,7 +8,7 @@ pub fn from_lua(input: TokenStream) -> TokenStream { ident, mut generics, .. } = parse_macro_input!(input as DeriveInput); - let ident_str = ident.to_string(); + let ident_str = ident.unraw().to_string(); generics .make_where_clause() .predicates diff --git a/mlua_derive/src/userdata/attr.rs b/mlua_derive/src/userdata/attr.rs index c6ad4f30..1dfa2263 100644 --- a/mlua_derive/src/userdata/attr.rs +++ b/mlua_derive/src/userdata/attr.rs @@ -1,4 +1,5 @@ use proc_macro2::Span; +use syn::ext::IdentExt; use syn::meta::ParseNestedMeta; use syn::{Ident, LitStr, Result}; @@ -63,7 +64,7 @@ impl LuaAttr { /// Returns the effective Lua name. pub(crate) fn name(&self, ident: &Ident) -> String { - self.name.clone().unwrap_or_else(|| ident.to_string()) + self.name.clone().unwrap_or_else(|| ident.unraw().to_string()) } /// Returns the span to use for error reporting. @@ -79,7 +80,7 @@ impl LuaAttr { if let Some(ref name) = self.name { return Ok(name.clone()); } - let fn_name = fn_ident.to_string(); + let fn_name = fn_ident.unraw().to_string(); if fn_name.starts_with("__") { return Ok(fn_name); } diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs index be155bca..15ae11a0 100644 --- a/mlua_derive/src/userdata/mod.rs +++ b/mlua_derive/src/userdata/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod userdata_impl; use proc_macro::TokenStream; use quote::{format_ident, quote}; +use syn::ext::IdentExt; use syn::spanned::Spanned; use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; @@ -103,7 +104,7 @@ pub fn userdata_type(item: TokenStream) -> TokenStream { continue; } - let lua_name = lua_attr.name.unwrap_or_else(|| field_name.to_string()); + let lua_name = lua_attr.name.unwrap_or_else(|| field_name.unraw().to_string()); // Assume get/set by default (unless explicitly specified) let (has_get, has_set) = if lua_attr.get || lua_attr.set { diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index 8f4ed42b..b54b20e4 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -474,13 +474,14 @@ fn test_static_metamethods() { #[derive(Clone, Debug, UserData)] struct Hygiene { value: i32, + r#type: i32, } #[mlua::userdata_impl] impl Hygiene { #[lua(infallible)] fn new(value: i32) -> Self { - Hygiene { value } + Hygiene { value, r#type: 7 } } // `this` must not clash with the generated receiver binding. @@ -494,6 +495,11 @@ impl Hygiene { fn add_lua(&self, lua: i32) -> i32 { self.value + lua } + + #[lua(infallible)] + fn r#double_type(&self) -> i32 { + self.r#type * 2 + } } #[test] @@ -505,8 +511,17 @@ fn test_param_name_hygiene() { lua.load( r#" local h = Hygiene.new(10) + + -- reserved parameter names must not clash with generated bindings assert(h:add_this(5) == 15, "add_this should be 10 + 5 = 15") assert(h:add_lua(3) == 13, "add_lua should be 10 + 3 = 13") + + -- the `r#` prefix must not leak into Lua names + assert(h.type == 7, "field r#type should be exposed as 'type'") + h.type = 10 + assert(h.type == 10, "field r#type setter should update via 'type'") + assert(h:double_type() == 20, "method r#double_type should be callable as 'double_type'") + assert(h["r#type"] == nil, "the raw prefix must not leak into the Lua name") "#, ) .exec() From dab2fe67b0517c3ee69cd7018102cd9731c24a23 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Sun, 5 Jul 2026 15:14:11 +0200 Subject: [PATCH 124/138] mlua-sys: separate compile error when no Lua feature is enabled (#712) --- mlua-sys/build/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mlua-sys/build/main.rs b/mlua-sys/build/main.rs index 5bb8ddbe..7fe0fe3e 100644 --- a/mlua-sys/build/main.rs +++ b/mlua-sys/build/main.rs @@ -13,6 +13,10 @@ cfg_if::cfg_if! { include!("main_inner.rs"); } else if #[cfg(all(feature = "luau", not(any(feature = "lua55", feature = "lua54", feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit"))))] { include!("main_inner.rs"); + } else if #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit", feature = "luau")))] { + fn main() { + compile_error!("No Lua feature enabled. Please enable one of: lua55, lua54, lua53, lua52, lua51, luajit, luajit52, luau"); + } } else { fn main() { compile_error!("You can enable only one of the features: lua55, lua54, lua53, lua52, lua51, luajit, luajit52, luau"); From 466b327a3e342252d64c31e04fec129237a263d0 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 14:41:50 +0100 Subject: [PATCH 125/138] Re-generate compile error messages --- tests/compile/lua_norefunwindsafe.stderr | 94 ++++++++------- tests/compile/ref_nounwindsafe.stderr | 139 ++++++----------------- 2 files changed, 89 insertions(+), 144 deletions(-) diff --git a/tests/compile/lua_norefunwindsafe.stderr b/tests/compile/lua_norefunwindsafe.stderr index e2807c4e..4094cd2b 100644 --- a/tests/compile/lua_norefunwindsafe.stderr +++ b/tests/compile/lua_norefunwindsafe.stderr @@ -1,28 +1,32 @@ -error[E0277]: the type `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `mlua::types::sync::inner::ReentrantMutex`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<*mut lua_State>` -note: required because it appears within the type `Cell<*mut lua_State>` - --> $RUST/core/src/cell.rs + = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs | - | pub struct Cell { - | ^^^^ -note: required because it appears within the type `mlua::state::RawLua` - --> src/state/raw.rs + | pub struct ReentrantMutex { + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `alloc::sync::ArcInner>` + --> $RUST/alloc/src/sync.rs | - | pub struct RawLua { - | ^^^^^^ -note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` - --> src/types/sync.rs + | struct ArcInner { + | ^^^^^^^^ +note: required because it appears within the type `PhantomData>>` + --> $RUST/core/src/marker.rs | - | pub(crate) struct ReentrantMutex(T); - | ^^^^^^^^^^^^^^ - = note: required for `Rc>` to implement `RefUnwindSafe` + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `Arc>` + --> $RUST/alloc/src/sync.rs + | + | pub struct Arc< + | ^^^ note: required because it appears within the type `Lua` --> src/state.rs | @@ -40,37 +44,45 @@ note: required by a bound in `std::panic::catch_unwind` | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { | ^^^^^^^^^^ required by this bound in `catch_unwind` -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = 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 - | - | pub struct RawLua { - | ^^^^^^ -note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` - --> src/types/sync.rs - | - | pub(crate) struct ReentrantMutex(T); - | ^^^^^^^^^^^^^^ - = note: required for `Rc>` to implement `RefUnwindSafe` + = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `Cell` + --> $RUST/core/src/cell.rs + | + | pub struct Cell { + | ^^^^ +note: required because it appears within the type `lock_api::remutex::RawReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs + | + | pub struct RawReentrantMutex { + | ^^^^^^^^^^^^^^^^^ +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs + | + | pub struct ReentrantMutex { + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `alloc::sync::ArcInner>` + --> $RUST/alloc/src/sync.rs + | + | struct ArcInner { + | ^^^^^^^^ +note: required because it appears within the type `PhantomData>>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `Arc>` + --> $RUST/alloc/src/sync.rs + | + | pub struct Arc< + | ^^^ note: required because it appears within the type `Lua` --> src/state.rs | diff --git a/tests/compile/ref_nounwindsafe.stderr b/tests/compile/ref_nounwindsafe.stderr index 06990592..757083df 100644 --- a/tests/compile/ref_nounwindsafe.stderr +++ b/tests/compile/ref_nounwindsafe.stderr @@ -1,25 +1,25 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `rc::RcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `Cell` - --> $RUST/core/src/cell.rs + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs | - | pub struct Cell { - | ^^^^ -note: required because it appears within the type `rc::RcInner>` - --> $RUST/alloc/src/rc.rs + | pub struct ReentrantMutex { + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `alloc::sync::ArcInner>` + --> $RUST/alloc/src/sync.rs | - | struct RcInner { - | ^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::rc::Weak>` - --> $RUST/alloc/src/rc.rs + | struct ArcInner { + | ^^^^^^^^ + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` + --> $RUST/alloc/src/sync.rs | | pub struct Weak< | ^^^^ @@ -49,105 +49,38 @@ note: required by a bound in `std::panic::catch_unwind` | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { | ^^^^^^^^^^ required by this bound in `catch_unwind` -error[E0277]: the type `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell<*mut lua_State>` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `rc::RcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell<*mut lua_State>` -note: required because it appears within the type `Cell<*mut lua_State>` + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `Cell` --> $RUST/core/src/cell.rs | | pub struct Cell { | ^^^^ -note: required because it appears within the type `mlua::state::RawLua` - --> src/state/raw.rs - | - | pub struct RawLua { - | ^^^^^^ -note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` - --> src/types/sync.rs - | - | pub(crate) struct ReentrantMutex(T); - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `rc::RcInner>` - --> $RUST/alloc/src/rc.rs - | - | struct RcInner { - | ^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::rc::Weak>` - --> $RUST/alloc/src/rc.rs - | - | pub struct Weak< - | ^^^^ -note: required because it appears within the type `WeakLua` - --> src/state.rs - | - | pub struct WeakLua(XWeak>); - | ^^^^^^^ -note: required because it appears within the type `mlua::types::value_ref::ValueRef` - --> src/types/value_ref.rs - | - | pub struct ValueRef { - | ^^^^^^^^ -note: required because it appears within the type `LuaTable` - --> src/table.rs - | - | pub struct Table(pub(crate) ValueRef); - | ^^^^^ -note: required because it's used within this closure - --> tests/compile/ref_nounwindsafe.rs:8:18 - | -8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ^^^^^^^ -note: required by a bound in `std::panic::catch_unwind` - --> $RUST/std/src/panic.rs - | - | pub fn catch_unwind R + UnwindSafe, R>(f: F) -> Result { - | ^^^^^^^^^^ required by this bound in `catch_unwind` - -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary - --> tests/compile/ref_nounwindsafe.rs:8:18 - | -8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary - | | - | required by a bound introduced by this call - | - = 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 - | - | pub struct RawLua { - | ^^^^^^ -note: required because it appears within the type `mlua::types::sync::inner::ReentrantMutex` - --> src/types/sync.rs - | - | pub(crate) struct ReentrantMutex(T); - | ^^^^^^^^^^^^^^ -note: required because it appears within the type `rc::RcInner>` - --> $RUST/alloc/src/rc.rs - | - | struct RcInner { - | ^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::rc::Weak>` - --> $RUST/alloc/src/rc.rs +note: required because it appears within the type `lock_api::remutex::RawReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs + | + | pub struct RawReentrantMutex { + | ^^^^^^^^^^^^^^^^^ +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + --> $CARGO/lock_api-$VERSION/src/remutex.rs + | + | pub struct ReentrantMutex { + | ^^^^^^^^^^^^^^ +note: required because it appears within the type `alloc::sync::ArcInner>` + --> $RUST/alloc/src/sync.rs + | + | struct ArcInner { + | ^^^^^^^^ + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` + --> $RUST/alloc/src/sync.rs | | pub struct Weak< | ^^^^ From 7b9ec57786375ac06bda3f990e90bed830be4b4f Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 14:42:40 +0100 Subject: [PATCH 126/138] mlua_derive: Support wildcard params and reject invalid impls in userdata_impl --- mlua_derive/src/userdata/userdata_impl.rs | 109 +++++++++++------- tests/compile.rs | 2 + tests/compile/userdata_destructuring_arg.rs | 14 +++ .../compile/userdata_destructuring_arg.stderr | 5 + tests/compile/userdata_generic_impl.rs | 14 +++ tests/compile/userdata_generic_impl.stderr | 5 + tests/userdata_macro.rs | 41 +++++++ 7 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 tests/compile/userdata_destructuring_arg.rs create mode 100644 tests/compile/userdata_destructuring_arg.stderr create mode 100644 tests/compile/userdata_generic_impl.rs create mode 100644 tests/compile/userdata_generic_impl.stderr diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs index d78e4e1d..8bed5b67 100644 --- a/mlua_derive/src/userdata/userdata_impl.rs +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -168,51 +168,64 @@ fn analyze_self_and_args(sig: &Signature) -> syn::Result { } } - if let syn::Pat::Ident(pat_ident) = &*typed.pat { - let arg_type = &*typed.ty; - let mut option_inner = None; - let ref_kind = match arg_type { - Type::Reference(r) if r.mutability.is_some() => Some(RefKind::Mut), - Type::Reference(_) => Some(RefKind::Ref), - _ => { - // Check if it's `Option<&T>` or `Option<&mut T>` - option_inner = try_unwrap_option(arg_type); - option_inner.and_then(|inner| match inner { - Type::Reference(r) if r.mutability.is_some() => Some(RefKind::OptionMut), - Type::Reference(_) => Some(RefKind::OptionRef), - _ => None, - }) - } - }; - let callback_type = match &ref_kind { - Some(RefKind::OptionRef | RefKind::OptionMut) => { - match classify_ref_type(option_inner.unwrap()) { - Some(ty) => parse_quote! { Option<#ty> }, - None => { - return Err(syn::Error::new_spanned( - arg_type, - "this reference type is not supported as a callback parameter", - )); - } - } - } - Some(_) => match classify_ref_type(arg_type) { - Some(ty) => ty, + let ident = match &*typed.pat { + syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(), + syn::Pat::Wild(_) => { + // For wildcards we generate a unique identifier to avoid collisions with other + // parameters. + Ident::new(&format!("__mlua_arg_{}", args.len()), Span2::mixed_site()) + } + _ => { + return Err(syn::Error::new_spanned( + &typed.pat, + "`#[mlua::userdata_impl]` requires a named parameter or `_`; destructuring patterns are not supported", + )); + } + }; + + let arg_type = &*typed.ty; + let mut option_inner = None; + let ref_kind = match arg_type { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::Mut), + Type::Reference(_) => Some(RefKind::Ref), + _ => { + // Check if it's `Option<&T>` or `Option<&mut T>` + option_inner = try_unwrap_option(arg_type); + option_inner.and_then(|inner| match inner { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::OptionMut), + Type::Reference(_) => Some(RefKind::OptionRef), + _ => None, + }) + } + }; + let callback_type = match &ref_kind { + Some(RefKind::OptionRef | RefKind::OptionMut) => { + match classify_ref_type(option_inner.unwrap()) { + Some(ty) => parse_quote! { Option<#ty> }, None => { return Err(syn::Error::new_spanned( arg_type, "this reference type is not supported as a callback parameter", )); } - }, - None => arg_type.clone(), - }; - args.push(ArgInfo { - ident: pat_ident.ident.clone(), - userdata_ref: ref_kind, - callback_type, - }); - } + } + } + Some(_) => match classify_ref_type(arg_type) { + Some(ty) => ty, + None => { + return Err(syn::Error::new_spanned( + arg_type, + "this reference type is not supported as a callback parameter", + )); + } + }, + None => arg_type.clone(), + }; + args.push(ArgInfo { + ident, + userdata_ref: ref_kind, + callback_type, + }); } } } @@ -275,6 +288,24 @@ pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { let mut input = parse_macro_input!(item as ItemImpl); + // Generic impl blocks are not supported + if !input.generics.params.is_empty() { + return syn::Error::new_spanned( + &input.generics, + "`#[mlua::userdata_impl]` does not support generic impl blocks.", + ) + .to_compile_error() + .into(); + } + if let Some(where_clause) = &input.generics.where_clause { + return syn::Error::new_spanned( + where_clause, + "`#[mlua::userdata_impl]` does not support `where` clauses on the impl block.", + ) + .to_compile_error() + .into(); + } + let type_path = match &*input.self_ty { Type::Path(type_path) => &type_path.path, _ => { diff --git a/tests/compile.rs b/tests/compile.rs index 6bd84083..1c85ffb3 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -34,6 +34,8 @@ fn test_compilation() { t.compile_fail("tests/compile/userdata_setter_no_value.rs"); t.compile_fail("tests/compile/userdata_static_with_self.rs"); t.compile_fail("tests/compile/userdata_meta_owned_self.rs"); + t.compile_fail("tests/compile/userdata_destructuring_arg.rs"); + t.compile_fail("tests/compile/userdata_generic_impl.rs"); t.compile_fail("tests/compile/userdata_const_getter.rs"); t.compile_fail("tests/compile/userdata_field_with_args.rs"); } diff --git a/tests/compile/userdata_destructuring_arg.rs b/tests/compile/userdata_destructuring_arg.rs new file mode 100644 index 00000000..8bd206e8 --- /dev/null +++ b/tests/compile/userdata_destructuring_arg.rs @@ -0,0 +1,14 @@ +#[derive(mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(infallible)] + fn takes_pattern(&self, (a, b): (u32, u32)) -> u32 { + self.x + a + b + } +} + +fn main() {} diff --git a/tests/compile/userdata_destructuring_arg.stderr b/tests/compile/userdata_destructuring_arg.stderr new file mode 100644 index 00000000..1028d6eb --- /dev/null +++ b/tests/compile/userdata_destructuring_arg.stderr @@ -0,0 +1,5 @@ +error: `#[mlua::userdata_impl]` requires a named parameter or `_`; destructuring patterns are not supported + --> tests/compile/userdata_destructuring_arg.rs:9:29 + | +9 | fn takes_pattern(&self, (a, b): (u32, u32)) -> u32 { + | ^^^^^^ diff --git a/tests/compile/userdata_generic_impl.rs b/tests/compile/userdata_generic_impl.rs new file mode 100644 index 00000000..d9f62dcf --- /dev/null +++ b/tests/compile/userdata_generic_impl.rs @@ -0,0 +1,14 @@ +#[derive(mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(infallible)] + fn get(&self) -> u32 { + self.x + } +} + +fn main() {} diff --git a/tests/compile/userdata_generic_impl.stderr b/tests/compile/userdata_generic_impl.stderr new file mode 100644 index 00000000..db15e78c --- /dev/null +++ b/tests/compile/userdata_generic_impl.stderr @@ -0,0 +1,5 @@ +error: `#[mlua::userdata_impl]` does not support generic impl blocks. + --> tests/compile/userdata_generic_impl.rs:7:5 + | +7 | impl Foo { + | ^^^ diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs index b54b20e4..a681cef7 100644 --- a/tests/userdata_macro.rs +++ b/tests/userdata_macro.rs @@ -528,6 +528,47 @@ fn test_param_name_hygiene() { .unwrap(); } +#[derive(Clone, Debug, UserData)] +struct Wild { + n: i32, +} + +#[mlua::userdata_impl] +impl Wild { + #[lua(infallible)] + fn new(n: i32) -> Self { + Wild { n } + } + + #[lua(infallible)] + fn ignore_one(&self, _: i32) -> i32 { + self.n + } + + #[lua(infallible)] + fn add_second(&self, _: i32, other: &Wild) -> i32 { + self.n + other.n + } +} + +#[test] +fn test_wildcard_params() { + let lua = Lua::new(); + lua.globals() + .set("Wild", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local w = Wild.new(7) + assert(w:ignore_one(99) == 7, "single wildcard arg should be ignored") + assert(w:add_second(1, Wild.new(5)) == 12, "named arg after a wildcard should work") + assert(not pcall(function() return w:ignore_one({}) end), "wildcard arg is still type-checked") + "#, + ) + .exec() + .unwrap(); +} + #[cfg(feature = "async")] mod async_tests { use mlua::{Lua, Result, UserData}; From 55488376924e3b8e5d24a1cddc6190c3d426c0b8 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 15:39:05 +0100 Subject: [PATCH 127/138] clippy --- Cargo.toml | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5646618f..d8595b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ rustyline = "18.0" tokio = { version = "1.0", features = ["full"] } [lints.rust] -unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)', 'cfg(force_memory_limit)'] } [[bench]] name = "benchmark" diff --git a/src/lib.rs b/src/lib.rs index 795dd16c..3afda0b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,7 @@ // Deny warnings inside doc tests / examples. When this isn't present, rustdoc doesn't show *any* // warnings at all. #![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(not(send), allow(clippy::arc_with_non_send_sync))] +#![cfg_attr(not(feature = "send"), allow(clippy::arc_with_non_send_sync))] #![allow(unsafe_op_in_unsafe_fn)] #[macro_use] From 34825d62702591e55896591f4a20e95bba77f731 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 15:55:09 +0100 Subject: [PATCH 128/138] Fix `Lua::gc_set_mode` panic when GC is internally stopped (Lua 5.4/5.5) --- src/state.rs | 15 +++++++-------- tests/memory.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/state.rs b/src/state.rs index ec7db0b4..bd8d61e6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1170,6 +1170,9 @@ impl Lua { /// `None` because Lua's C API does not provide a way to read back current parameter values /// without changing them. /// + /// If the collector is internally stopped, the mode cannot be changed and the requested mode is + /// returned as-is. + /// /// # Examples /// /// Switch to generational mode (Lua 5.4+): @@ -1200,9 +1203,8 @@ impl Lua { ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPSIZE, v); } match ffi::lua_gc(state, ffi::LUA_GCINC) { - ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), - _ => unreachable!(), + _ => GcMode::Incremental(GcIncParams::default()), } }, #[cfg(feature = "lua54")] @@ -1211,9 +1213,8 @@ impl Lua { let step_mul = params.step_multiplier.unwrap_or(0); let step_size = params.step_size.unwrap_or(0); match ffi::lua_gc(state, ffi::LUA_GCINC, pause, step_mul, step_size) { - ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), - _ => unreachable!(), + _ => GcMode::Incremental(GcIncParams::default()), } }, #[cfg(any(feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit"))] @@ -1252,9 +1253,8 @@ impl Lua { ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMAJORMINOR, v); } match ffi::lua_gc(state, ffi::LUA_GCGEN) { - ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), - _ => unreachable!(), + _ => GcMode::Generational(GcGenParams::default()), } }, #[cfg(feature = "lua54")] @@ -1262,9 +1262,8 @@ impl Lua { let minor = params.minor_multiplier.unwrap_or(0); let minor_to_major = params.minor_to_major.unwrap_or(0); match ffi::lua_gc(state, ffi::LUA_GCGEN, minor, minor_to_major) { - ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), - _ => unreachable!(), + _ => GcMode::Generational(GcGenParams::default()), } }, } diff --git a/tests/memory.rs b/tests/memory.rs index 4f91221e..b1772ca4 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -130,6 +130,36 @@ fn test_gc_control() -> Result<()> { Ok(()) } +#[cfg(any(feature = "lua54", feature = "lua55"))] +#[test] +fn test_gc_set_mode_in_finalizer() -> Result<()> { + use std::sync::atomic::{AtomicBool, Ordering}; + + let lua = Lua::new(); + + // While finalizers are running, the GC is internally stopped and `lua_gc` rejects all + // options + let switched = Arc::new(AtomicBool::new(false)); + let switched2 = switched.clone(); + let finalizer = lua.create_function(move |lua, ()| { + let mode = lua.gc_set_mode(GcMode::Generational(GcGenParams::default())); + switched2.store(matches!(mode, GcMode::Generational(_)), Ordering::Relaxed); + Ok(()) + })?; + lua.globals().set("finalizer", finalizer)?; + lua.load("setmetatable({}, { __gc = finalizer })").exec()?; + lua.globals().remove("finalizer")?; + + lua.gc_collect()?; + lua.gc_collect()?; + assert!( + switched.load(Ordering::Relaxed), + "gc_set_mode did not complete in finalizer" + ); + + Ok(()) +} + #[cfg(any(feature = "lua53", feature = "lua52"))] #[test] fn test_gc_error() { From 9099d5f2bc365f435847080d4cd4a0f75468d483 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 15:59:47 +0100 Subject: [PATCH 129/138] Rename `JitOptions::set_inliner` to `JitOptions::inliner` for consistency --- src/state.rs | 2 +- tests/luau.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state.rs b/src/state.rs index bd8d61e6..2bc2fa2a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -288,7 +288,7 @@ impl JitOptions { /// /// Disabled by default. Changing this option does not affect already loaded functions. #[must_use] - pub const fn set_inliner(mut self, enabled: bool) -> Self { + pub const fn inliner(mut self, enabled: bool) -> Self { self.inliner = enabled; self } diff --git a/tests/luau.rs b/tests/luau.rs index b9ca1e07..5ac2b1f3 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -362,7 +362,7 @@ fn test_fflags() { #[test] fn test_jit_inliner() -> Result<()> { let lua = Lua::new(); - lua.set_jit_options(mlua::JitOptions::new().set_inliner(true)); + lua.set_jit_options(mlua::JitOptions::new().inliner(true)); // An inlinable helper called in a hot loop. let sum = lua From eef59853609cccbcec3f875b652b68cd2e649a44 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 16:04:43 +0100 Subject: [PATCH 130/138] Rename LuaJitOptions to LuauJitOptions in prelude --- src/prelude.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prelude.rs b/src/prelude.rs index 8ecb7fe0..19d02bd8 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -41,7 +41,7 @@ pub use crate::{ #[cfg(any(feature = "luau-jit", doc))] #[doc(no_inline)] -pub use crate::state::JitOptions as LuaJitOptions; +pub use crate::state::JitOptions as LuauJitOptions; #[cfg(feature = "async")] #[doc(no_inline)] From 1af17ec64d4f9c8c7924469d0e886b47ebc56b99 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 17:25:27 +0100 Subject: [PATCH 131/138] Various doc updates and refinement --- README.md | 6 +++--- docs/UserData.md | 4 ++-- docs/chunk.md | 7 ------- src/buffer.rs | 8 ++++++++ src/conversion.rs | 2 ++ src/debug.rs | 7 +++++-- src/error.rs | 11 +++++++++-- src/function.rs | 2 +- src/lib.rs | 6 ++++-- src/luau/heap_dump.rs | 6 +++++- src/luau/require.rs | 7 +++++-- src/state.rs | 33 ++++++++++++++++++++++----------- src/thread.rs | 15 +++++++++++---- src/types.rs | 8 ++++++-- src/userdata.rs | 8 ++++---- src/value.rs | 2 +- 16 files changed, 88 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b253b571..a73a4b30 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [docs.rs]: https://docs.rs/mlua [Coverage Status]: https://codecov.io/gh/mlua-rs/mlua/branch/main/graph/badge.svg?token=99339FS1CG [codecov.io]: https://codecov.io/gh/mlua-rs/mlua -[MSRV]: https://img.shields.io/badge/rust-1.79+-brightgreen.svg?&logo=rust +[MSRV]: https://img.shields.io/badge/rust-1.88+-brightgreen.svg?&logo=rust [Guided Tour] | [Benchmarks] | [FAQ] @@ -51,8 +51,8 @@ Below is a list of the available feature flags. By default `mlua` does not enabl * `vendored`: build static Lua(JIT) libraries from sources during `mlua` compilation using [lua-src] or [luajit-src] * `module`: enable module mode (building loadable `cdylib` library for Lua) * `async`: enable async/await support (any executor can be used, eg. [tokio] or [async-std]) -* `send`: make `mlua::Lua: Send + Sync` (adds [`Send`] requirement to `mlua::Function` and `mlua::UserData`) -* `error-send`: make `mlua:Error: Send + Sync` +* `send`: make `mlua::Lua: Send + Sync` (adds [`Send`] requirement to `mlua::Function` and `Send + Sync` to `mlua::UserData`) +* `error-send`: make `mlua::Error: Send + Sync` * `serde`: add serialization and deserialization support to `mlua` types using [serde] * `macros`: enable procedural macros (such as `chunk!`) * `anyhow`: enable `anyhow::Error` conversion into Lua diff --git a/docs/UserData.md b/docs/UserData.md index 15280e1a..15db89b5 100644 --- a/docs/UserData.md +++ b/docs/UserData.md @@ -54,8 +54,8 @@ The generated getter clones the field value when accessed from Lua. # Methods registration Use `#[mlua::userdata_impl]` on an `impl` block to register methods, -metamethods, and constants. All public items in the block are registered -automatically. +metamethods, and constants. All items in the block are registered automatically, +regardless of visibility. ## Method detection diff --git a/docs/chunk.md b/docs/chunk.md index 972048ef..f8dcc1d9 100644 --- a/docs/chunk.md +++ b/docs/chunk.md @@ -29,13 +29,6 @@ The main thing to remember is: (Single quoted strings only work if they contain a single character, since in Rust, `'a'` is a character literal). -- Using Lua comments `--` is not desirable in **stable** Rust and can have bad side effects. - - This is because procedural macros have Line/Column information available only in - **nightly** Rust. Instead, Lua chunks represented as a big single line of code in stable Rust. - - As workaround, Rust comments `//` can be used. - Other minor limitations: - Certain escape codes in string literals don't work. (Specifically: `\a`, `\b`, `\f`, `\v`, diff --git a/src/buffer.rs b/src/buffer.rs index d27f8b3d..d256c626 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -37,6 +37,10 @@ impl Buffer { /// Reads given number of bytes from the buffer at the given offset. /// /// Offset is 0-based. + /// + /// # Panics + /// + /// Panics if `offset + N` is greater than the buffer length. #[track_caller] pub fn read_bytes(&self, offset: usize) -> [u8; N] { let lua = self.0.lua.lock(); @@ -49,6 +53,10 @@ impl Buffer { /// Writes given bytes to the buffer at the given offset. /// /// Offset is 0-based. + /// + /// # Panics + /// + /// Panics if `offset + bytes.len()` is greater than the buffer length. #[track_caller] pub fn write_bytes(&self, offset: usize, bytes: &[u8]) { let lua = self.0.lua.lock(); diff --git a/src/conversion.rs b/src/conversion.rs index 3ee8a1f0..70c34829 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -680,6 +680,8 @@ impl IntoLua for &OsStr { Ok(Value::String(lua.create_string(self.as_bytes())?)) } + // On non-Unix platforms `OsStr` is not guaranteed to be valid Unicode, invalid sequences are + // replaced with `U+FFFD` rather than erroring. #[cfg(not(unix))] #[inline] fn into_lua(self, lua: &Lua) -> Result { diff --git a/src/debug.rs b/src/debug.rs index 3ee7a23c..964d4528 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,8 +1,11 @@ //! Lua debugging interface. //! //! This module provides access to the Lua debug interface, allowing inspection of the call stack, -//! and function information. The main types are [`struct@Debug`] for accessing debug information -//! and [`HookTriggers`] for configuring debug hooks. +//! and function information. The main type is [`struct@Debug`] for accessing debug information. +#![cfg_attr( + not(feature = "luau"), + doc = "\nDebug hooks are configured with [`HookTriggers`]." +)] use std::borrow::Cow; use std::os::raw::c_int; diff --git a/src/error.rs b/src/error.rs index d41f2df5..0e1a13b8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -352,7 +352,9 @@ impl Error { } } - /// Attempts to downcast the external error object to a concrete type by reference. + /// Attempts to downcast to a concrete external error type by reference. + /// + /// The search descends through wrapping layers following the same path as [`Error::chain`]. pub fn downcast_ref(&self) -> Option<&T> where T: StdError + 'static, @@ -366,7 +368,12 @@ impl Error { } } - /// An iterator over the chain of nested errors wrapped by this Error. + /// An iterator over the chain of nested errors wrapped by this `Error`. + /// + /// Iteration starts with `self` and descends through wrapping layers. + /// A bare [`Error::ExternalError`] wrapper is skipped in favor of the error it wraps. + /// The chain stops at the innermost error and does not follow the external error's own + /// [`StdError::source`] chain. pub fn chain(&self) -> impl Iterator { Chain { root: self, diff --git a/src/function.rs b/src/function.rs index d0dbd8b8..8081ec9f 100644 --- a/src/function.rs +++ b/src/function.rs @@ -618,7 +618,7 @@ struct WrappedFunction(pub(crate) Callback); struct WrappedAsyncFunction(pub(crate) AsyncCallback); impl Function { - /// Wraps a Rust function or closure, returning an opaque type that implements [`IntoLua`] + /// Wraps a Rust function or closure, returning an opaque type that implements the [`IntoLua`] /// trait. #[inline] pub fn wrap(func: F) -> impl IntoLua diff --git a/src/lib.rs b/src/lib.rs index 3afda0b0..df73ca2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,8 +47,10 @@ //! //! # `Send` and `Sync` support //! -//! By default `mlua` is `!Send`. This can be changed by enabling `feature = "send"` that adds -//! `Send` requirement to Rust functions and [`UserData`] types. +//! By default `mlua` is `!Send`. This can be changed by enabling `feature = "send"` that adds a +//! `Send` requirement to Rust functions and a `Send + Sync` requirement to [`UserData`] types. +//! A `Send`-only userdata types must therefore be wrapped (e.g. in a `Mutex`) or created through a +//! [`Scope`] to be used with the `send` feature. //! //! In this case [`Lua`] object and their types can be send or used from other threads. Internally //! access to Lua VM is synchronized using a reentrant mutex that can be locked many times within diff --git a/src/luau/heap_dump.rs b/src/luau/heap_dump.rs index e8b7a266..ed2a57b6 100644 --- a/src/luau/heap_dump.rs +++ b/src/luau/heap_dump.rs @@ -65,7 +65,8 @@ impl HeapDump { /// Returns a mapping from object type to (count, total size in bytes). /// - /// If `category` is provided, only objects in that category are considered. + /// If `category` is provided, only objects in that category are considered. An unknown category + /// yields an empty map. pub fn size_by_type<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> { self.size_by_type_inner(category).unwrap_or_default() } @@ -104,6 +105,9 @@ impl HeapDump { } /// Returns a mapping from userdata type to (count, total size in bytes). + /// + /// If `category` is provided, only objects in that category are considered. An unknown category + /// yields an empty map. pub fn size_by_userdata<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> { self.size_by_userdata_inner(category).unwrap_or_default() } diff --git a/src/luau/require.rs b/src/luau/require.rs index 7e8db695..be52efcf 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -17,8 +17,11 @@ pub use fs::FsRequirer; /// An error that can occur during navigation in the Luau `require-by-string` system. #[derive(Debug, Clone)] pub enum NavigateError { + /// The path is ambiguous (more than one candidate matches). Ambiguous, + /// The requested path could not be found. NotFound, + /// Another error occurred during navigation. Other(Error), } @@ -61,7 +64,7 @@ pub trait Require { /// Resets the internal state to point at an aliased module. /// - /// This function received an exact path from a configuration file. + /// This function receives an exact path from a configuration file. /// It's only called when an alias's path cannot be resolved relative to its /// configuration file. fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError>; @@ -84,7 +87,7 @@ pub trait Require { Err(NavigateError::NotFound) } - // Navigate to parent directory + /// Navigates to the parent directory of the current requirer. fn to_parent(&mut self) -> StdResult<(), NavigateError>; /// Navigate to the given child directory. diff --git a/src/state.rs b/src/state.rs index 2bc2fa2a..50f9383a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -70,9 +70,9 @@ pub(crate) struct LuaGuard(ArcReentrantMutexGuard); /// Tuning parameters for the incremental GC collector. /// -/// More information can be found in the Lua [documentation]. -/// -/// [documentation]: https://www.lua.org/manual/5.5/manual.html#2.5.1 +/// Each field is an [`Option`]: `None` leaves the corresponding parameter unchanged, while +/// `Some(v)` sets it. Units and ranges depend on the Lua version, check the Lua reference manual +/// for details. #[non_exhaustive] #[derive(Clone, Copy, Debug, Default)] pub struct GcIncParams { @@ -90,7 +90,9 @@ pub struct GcIncParams { /// GC work performed per unit of memory allocated. pub step_multiplier: Option, - /// Granularity of each GC step (see Lua reference for details). + /// Granularity of each GC step. + /// + /// The unit is version-dependent, check the Lua reference manual for details. #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] pub step_size: Option, @@ -134,9 +136,9 @@ impl GcIncParams { /// Tuning parameters for the generational GC collector (Lua 5.4+). /// -/// More information can be found in the Lua [documentation]. -/// -/// [documentation]: https://www.lua.org/manual/5.5/manual.html#2.5.2 +/// Each field is an [`Option`]: `None` leaves the corresponding parameter unchanged, while +/// `Some(v)` sets it. Units and ranges depend on the Lua version, check the reference manual +/// for details. #[cfg(any(feature = "lua55", feature = "lua54"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] #[non_exhaustive] @@ -1115,6 +1117,16 @@ impl Lua { feature = "lua52", feature = "luau" ))] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "lua55", + feature = "lua54", + feature = "lua53", + feature = "lua52", + feature = "luau" + ))) + )] pub fn gc_is_running(&self) -> bool { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCISRUNNING, 0) != 0 } @@ -1166,9 +1178,8 @@ impl Lua { /// Switches the GC to the given mode with the provided parameters. /// - /// Returns the previous [`GcMode`]. The returned value's parameter fields are always - /// `None` because Lua's C API does not provide a way to read back current parameter values - /// without changing them. + /// Returns the previous [`GcMode`]. Only the collector *mode* is reported, the returned value's + /// parameter fields are always `None`. /// /// If the collector is internally stopped, the mode cannot be changed and the requested mode is /// returned as-is. @@ -1183,7 +1194,7 @@ impl Lua { /// Switch to incremental mode with custom parameters: /// ```ignore /// lua.gc_set_mode(GcMode::Incremental( - /// GcIncParams::default().pause(200).step_multiplier(100) + /// GcIncParams::default().step_multiplier(100) /// )); /// ``` pub fn gc_set_mode(&self, mode: GcMode) -> GcMode { diff --git a/src/thread.rs b/src/thread.rs index 93ba3315..968be182 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -68,10 +68,15 @@ use { #[non_exhaustive] pub struct ThreadTriggers { /// Trigger the callback when a new thread is created. + /// + /// On Luau this fires for every thread creation, on other Lua versions only for threads created + /// via [`Lua::create_thread`](crate::Lua::create_thread). pub on_create: bool, - /// Trigger the callback before a thread is resumed (via [`Thread::resume`]). + /// Trigger the callback before a thread is resumed via [`Thread::resume`] (or an async resume + /// driven by mlua). It does not fire for a `coroutine.resume` performed inside Lua code. pub on_resume: bool, - /// Trigger the callback after a thread yields. + /// Trigger the callback after a thread yields back to a [`Thread::resume`] driven by mlua. + /// It does not fire for a yield consumed by a `coroutine.resume` inside Lua code. pub on_yield: bool, } @@ -266,7 +271,9 @@ unsafe fn exec_thread_event( } impl Thread { - /// Returns reference to the Lua state that this thread is associated with. + /// Returns the raw pointer to the Lua state that this thread is associated with. + /// + /// The pointer is valid only while this [`Thread`] is alive. #[inline(always)] pub fn state(&self) -> *mut ffi::lua_State { self.1 @@ -516,7 +523,7 @@ impl Thread { /// Sets a hook function that will periodically be called as Lua code executes. /// - /// This function is similar or [`Lua::set_hook`] except that it sets for the thread. + /// This function is similar to [`Lua::set_hook`] except that it sets the hook for the thread. /// You can have multiple hooks for different threads. /// /// To remove a hook call [`Thread::remove_hook`]. diff --git a/src/types.rs b/src/types.rs index 0c512635..b4cdb352 100644 --- a/src/types.rs +++ b/src/types.rs @@ -119,13 +119,17 @@ pub trait MaybeSend {} #[cfg(not(feature = "send"))] impl MaybeSend for T {} -/// A trait that adds `Sync` requirement if `send` feature is enabled. +/// Adds a `Sync` requirement to userdata types when the `send` feature is enabled. +/// +/// It is automatically implemented for all applicable types. #[cfg(feature = "send")] pub trait MaybeSync: Sync {} #[cfg(feature = "send")] impl MaybeSync for T {} -/// A trait that adds `Sync` requirement if `send` feature is enabled. +/// Adds a `Sync` requirement to userdata types when the `send` feature is enabled. +/// +/// It is automatically implemented for all applicable types. #[cfg(not(feature = "send"))] pub trait MaybeSync {} #[cfg(not(feature = "send"))] diff --git a/src/userdata.rs b/src/userdata.rs index 3b6b3b1c..912efa22 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -326,8 +326,8 @@ pub trait UserDataMethods { /// The userdata `T` will be moved out of the userdata container. This is useful for /// methods that need to consume the userdata. /// - /// The method can be called only once per userdata instance, subsequent calls will result in a - /// [`Error::UserDataDestructed`] error. + /// The method can be called only once per userdata instance. A subsequent call returns an + /// [`Error::BadArgument`] for `self` whose cause is [`Error::UserDataDestructed`]. fn add_method_once(&mut self, name: impl Into, method: M) where T: 'static, @@ -378,8 +378,8 @@ pub trait UserDataMethods { /// The userdata `T` will be moved out of the userdata container. This is useful for /// methods that need to consume the userdata. /// - /// The method can be called only once per userdata instance, subsequent calls will result in a - /// [`Error::UserDataDestructed`] error. + /// The method can be called only once per userdata instance. A subsequent call returns an + /// [`Error::BadArgument`] for `self` whose cause is [`Error::UserDataDestructed`]. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] fn add_async_method_once(&mut self, name: impl Into, method: M) diff --git a/src/value.rs b/src/value.rs index ebb46f52..d273dad7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -38,7 +38,7 @@ pub enum Value { LightUserData(LightUserData), /// An integer number. /// - /// Any Lua number convertible to a `Integer` will be represented as this variant. + /// Any Lua number convertible to an `Integer` will be represented as this variant. Integer(Integer), /// A floating point number. Number(Number), From cddbcc5e7ba7cb0f1fbea44689121bcbb735bc7f Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 18:36:19 +0100 Subject: [PATCH 132/138] Refactor crate-root re-exports and drop hidden backward-compat aliases --- examples/async_http_server.rs | 2 +- mlua_derive/src/chunk/mod.rs | 3 ++- src/lib.rs | 45 ++++++++++++----------------------- src/prelude.rs | 29 +++++++++++----------- src/serde/mod.rs | 6 +++-- src/state.rs | 8 ++++--- tests/chunk.rs | 8 ++++--- tests/function.rs | 2 +- tests/luau.rs | 7 +++--- tests/memory.rs | 2 +- tests/serde.rs | 4 ++-- tests/tests.rs | 7 +++--- tests/thread.rs | 3 ++- 13 files changed, 60 insertions(+), 66 deletions(-) diff --git a/examples/async_http_server.rs b/examples/async_http_server.rs index f5057ed6..95c64a8b 100644 --- a/examples/async_http_server.rs +++ b/examples/async_http_server.rs @@ -11,7 +11,7 @@ use hyper::{Request, Response}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; -use mlua::{Error as LuaError, Function, Lua, String as LuaString, Table, UserData, UserDataMethods, chunk}; +use mlua::{Error as LuaError, Function, Lua, LuaString, Table, UserData, UserDataMethods, chunk}; /// Wrapper around incoming request that implements UserData struct LuaRequest(SocketAddr, Request); diff --git a/mlua_derive/src/chunk/mod.rs b/mlua_derive/src/chunk/mod.rs index f89372c1..ae38d5a7 100644 --- a/mlua_derive/src/chunk/mod.rs +++ b/mlua_derive/src/chunk/mod.rs @@ -109,7 +109,8 @@ impl Chunk { }); quote! {{ - use mlua::{AsChunk, ChunkMode, Lua, Result, Table}; + use mlua::chunk::{AsChunk, ChunkMode}; + use mlua::{Lua, Result, Table}; use ::std::borrow::Cow; use ::std::cell::Cell; use ::std::io::Result as IoResult; diff --git a/src/lib.rs b/src/lib.rs index df73ca2a..f1b8acd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,8 @@ //! the same thread. //! //! [Lua programming language]: https://www.lua.org/ -//! [executing]: crate::Chunk::exec -//! [evaluating]: crate::Chunk::eval +//! [executing]: crate::chunk::Chunk::exec +//! [evaluating]: crate::chunk::Chunk::eval //! [globals]: crate::Lua::globals //! [`Future`]: std::future::Future //! [`serde::Serialize`]: https://docs.serde.rs/serde/ser/trait.Serialize.html @@ -108,6 +108,7 @@ pub use inventory as __inventory; #[doc(inline)] pub use crate::error::{Error, Result}; +pub use crate::error::{ErrorContext, ExternalError, ExternalResult}; #[doc(inline)] pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; @@ -116,7 +117,8 @@ pub use crate::scope::Scope; pub use crate::state::{Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; #[doc(inline)] -pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; +pub use crate::string::LuaString; +pub use crate::string::{BorrowedBytes, BorrowedStr}; #[doc(inline)] pub use crate::table::Table; #[doc(inline)] @@ -128,43 +130,24 @@ pub use crate::types::{ VmState, }; #[doc(inline)] -pub use crate::userdata::AnyUserData; +pub use crate::userdata::{AnyUserData, UserData}; +pub use crate::userdata::{ + MetaMethod, UserDataFields, UserDataMethods, UserDataOwned, UserDataRef, UserDataRefMut, UserDataRegistry, +}; pub use crate::value::{Nil, Value}; -// Re-export some types to keep backward compatibility and avoid breaking changes in the public API. -#[doc(hidden)] -pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; -#[cfg(feature = "luau")] -#[doc(hidden)] -pub use crate::chunk::{CompileConstant, Compiler}; -#[doc(hidden)] -pub use crate::error::{ErrorContext, ExternalError, ExternalResult}; -#[doc(hidden)] -pub use crate::string::LuaString as String; -#[doc(hidden)] -pub use crate::table::{TablePairs, TableSequence}; +/// Deprecated alias to [`LuaString`]. +#[deprecated(since = "0.12.0", note = "use `mlua::LuaString` instead")] #[doc(hidden)] -pub use crate::thread::{ThreadEvent, ThreadStatus, ThreadTriggers}; -#[doc(hidden)] -pub use crate::userdata::{ - MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataOwned, UserDataRef, - UserDataRefMut, UserDataRegistry, -}; +pub type String = crate::string::LuaString; #[cfg(not(feature = "luau"))] -#[doc(inline)] pub use crate::debug::HookTriggers; -#[cfg(any(feature = "luau-jit", doc))] -#[doc(inline)] -pub use crate::state::JitOptions; #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub use crate::{buffer::Buffer, vector::Vector}; -#[cfg(feature = "serde")] -#[doc(hidden)] -pub use crate::serde::{DeserializeOptions, SerializeOptions}; #[cfg(feature = "serde")] #[doc(inline)] pub use crate::{serde::LuaSerdeExt, value::SerializableValue}; @@ -196,7 +179,9 @@ pub use mlua_derive::FromLua; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::UserData; -#[doc(hidden)] +/// Registers items in an `impl` block as methods/fields of a [`UserData`](trait@UserData) type. +/// +/// See the [`UserData`](derive@UserData) derive macro documentation for usage details. #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::userdata_impl; diff --git a/src/prelude.rs b/src/prelude.rs index 19d02bd8..a8099960 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,25 +3,26 @@ #[doc(no_inline)] pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, - Either as LuaEither, Error as LuaError, FromLua, FromLuaMulti, Function as LuaFunction, - Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, Lua, LuaOptions, - LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, - ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, - Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, UserDataFields as LuaUserDataFields, - UserDataMetatable as LuaUserDataMetatable, UserDataMethods as LuaUserDataMethods, + Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, + ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti, + Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, + Lua, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, + Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, + StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, + UserDataFields as LuaUserDataFields, UserDataMethods as LuaUserDataMethods, UserDataOwned as LuaUserDataOwned, UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, chunk::Chunk as LuaChunk, - chunk::ChunkMode as LuaChunkMode, error::ErrorContext as LuaErrorContext, - error::ExternalError as LuaExternalError, error::ExternalResult as LuaExternalResult, - function::FunctionInfo as LuaFunctionInfo, function::LuaNativeFn, function::LuaNativeFnMut, - state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, - table::TableSequence as LuaTableSequence, thread::ThreadStatus as LuaThreadStatus, + chunk::ChunkMode as LuaChunkMode, function::FunctionInfo as LuaFunctionInfo, function::LuaNativeFn, + function::LuaNativeFnMut, state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, + table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, + thread::ThreadEvent as LuaThreadEvent, thread::ThreadStatus as LuaThreadStatus, + thread::ThreadTriggers as LuaThreadTriggers, userdata::UserDataMetatable as LuaUserDataMetatable, }; #[cfg(not(feature = "luau"))] #[doc(no_inline)] -pub use crate::HookTriggers as LuaHookTriggers; +pub use crate::debug::HookTriggers as LuaHookTriggers; #[cfg(any(feature = "lua54", feature = "lua55"))] #[doc(no_inline)] @@ -50,6 +51,6 @@ pub use crate::{function::LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThrea #[cfg(feature = "serde")] #[doc(no_inline)] pub use crate::{ - DeserializeOptions as LuaDeserializeOptions, LuaSerdeExt, SerializableValue as LuaSerializableValue, - SerializeOptions as LuaSerializeOptions, + LuaSerdeExt, SerializableValue as LuaSerializableValue, + serde::DeserializeOptions as LuaDeserializeOptions, serde::SerializeOptions as LuaSerializeOptions, }; diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 2d39f59e..21edc6ff 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -101,7 +101,8 @@ pub trait LuaSerdeExt: Sealed { /// # Example /// /// ``` - /// use mlua::{Lua, Result, LuaSerdeExt, SerializeOptions}; + /// use mlua::serde::SerializeOptions; + /// use mlua::{Lua, Result, LuaSerdeExt}; /// /// fn main() -> Result<()> { /// let lua = Lua::new(); @@ -151,7 +152,8 @@ pub trait LuaSerdeExt: Sealed { /// # Example /// /// ``` - /// use mlua::{Lua, Result, LuaSerdeExt, DeserializeOptions}; + /// use mlua::serde::DeserializeOptions; + /// use mlua::{Lua, Result, LuaSerdeExt}; /// use serde::Deserialize; /// /// #[derive(Deserialize, Debug, PartialEq)] diff --git a/src/state.rs b/src/state.rs index 50f9383a..83e7ac94 100644 --- a/src/state.rs +++ b/src/state.rs @@ -804,7 +804,8 @@ impl Lua { /// /// ``` /// # use std::sync::{Arc, atomic::{AtomicU64, Ordering}}; - /// # use mlua::{Lua, Result, ThreadStatus, VmState}; + /// # use mlua::thread::ThreadStatus; + /// # use mlua::{Lua, Result, VmState}; /// # #[cfg(feature = "luau")] /// # fn main() -> Result<()> { /// let lua = Lua::new(); @@ -899,7 +900,8 @@ impl Lua { /// Subscribe only to yield events: /// /// ``` - /// # use mlua::{Lua, Result, ThreadTriggers, ThreadEvent}; + /// # use mlua::thread::{ThreadTriggers, ThreadEvent}; + /// # use mlua::{Lua, Result}; /// # fn main() -> Result<()> { /// let lua = Lua::new(); /// lua.set_thread_event_callback( @@ -1342,7 +1344,7 @@ impl Lua { /// similar on the returned builder. Code is not even parsed until one of these methods is /// called. /// - /// [`Chunk::exec`]: crate::Chunk::exec + /// [`Chunk::exec`]: crate::chunk::Chunk::exec #[track_caller] pub fn load<'a>(&self, chunk: impl AsChunk + 'a) -> Chunk<'a> { self.load_with_location(chunk, Location::caller()) diff --git a/tests/chunk.rs b/tests/chunk.rs index 468757e1..b442e68b 100644 --- a/tests/chunk.rs +++ b/tests/chunk.rs @@ -1,7 +1,8 @@ #[cfg(not(target_os = "wasi"))] use std::{fs, io}; -use mlua::{Chunk, ChunkMode, Lua, Result}; +use mlua::chunk::{Chunk, ChunkMode}; +use mlua::{Lua, Result}; #[test] fn test_chunk_methods() -> Result<()> { @@ -131,7 +132,7 @@ fn test_chunk_macro() -> Result<()> { #[cfg(feature = "luau")] #[test] fn test_compiler() -> Result<()> { - let compiler = mlua::Compiler::new() + let compiler = mlua::chunk::Compiler::new() .set_optimization_level(2) .set_debug_level(2) .set_type_info_level(1) @@ -158,7 +159,8 @@ fn test_compiler() -> Result<()> { #[cfg(feature = "luau")] #[test] fn test_compiler_library_constants() { - use mlua::{Compiler, Vector}; + use mlua::Vector; + use mlua::chunk::Compiler; let compiler = Compiler::new() .set_optimization_level(2) diff --git a/tests/function.rs b/tests/function.rs index 8cb75d4c..a227f73b 100644 --- a/tests/function.rs +++ b/tests/function.rs @@ -239,7 +239,7 @@ fn test_function_dump() -> Result<()> { fn test_function_coverage() -> Result<()> { let lua = Lua::new(); - lua.set_compiler(mlua::Compiler::default().set_coverage_level(1)); + lua.set_compiler(mlua::chunk::Compiler::default().set_coverage_level(1)); let f = lua .load( diff --git a/tests/luau.rs b/tests/luau.rs index 5ac2b1f3..f97dfa0f 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -4,9 +4,8 @@ use std::fmt::Debug; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; -use mlua::{ - Compiler, Error, Function, Lua, LuaOptions, ObjectLike, Result, StdLib, Table, Value, Vector, VmState, -}; +use mlua::chunk::Compiler; +use mlua::{Error, Function, Lua, LuaOptions, ObjectLike, Result, StdLib, Table, Value, Vector, VmState}; #[test] fn test_version() -> Result<()> { @@ -362,7 +361,7 @@ fn test_fflags() { #[test] fn test_jit_inliner() -> Result<()> { let lua = Lua::new(); - lua.set_jit_options(mlua::JitOptions::new().inliner(true)); + lua.set_jit_options(mlua::state::JitOptions::new().inliner(true)); // An inlinable helper called in a hot loop. let sum = lua diff --git a/tests/memory.rs b/tests/memory.rs index b1772ca4..e8cd7e2b 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -148,7 +148,7 @@ fn test_gc_set_mode_in_finalizer() -> Result<()> { })?; lua.globals().set("finalizer", finalizer)?; lua.load("setmetatable({}, { __gc = finalizer })").exec()?; - lua.globals().remove("finalizer")?; + lua.globals().raw_remove("finalizer")?; lua.gc_collect()?; lua.gc_collect()?; diff --git a/tests/serde.rs b/tests/serde.rs index a8c84b6c..15508a29 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -4,9 +4,9 @@ use std::collections::HashMap; use std::error::Error as StdError; use bstr::BString; +use mlua::serde::{DeserializeOptions, SerializeOptions}; use mlua::{ - AnyUserData, DeserializeOptions, Error, ExternalResult, IntoLua, Lua, LuaSerdeExt, Result as LuaResult, - SerializeOptions, UserData, Value, + AnyUserData, Error, ExternalResult, IntoLua, Lua, LuaSerdeExt, Result as LuaResult, UserData, Value, }; use serde::{Deserialize, Serialize}; diff --git a/tests/tests.rs b/tests/tests.rs index ac438741..ae5b6545 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,9 +5,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; use std::{error, f32, f64, fmt}; +use mlua::chunk::ChunkMode; use mlua::{ - ChunkMode, Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, Table, UserData, Value, - Variadic, ffi, + Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, Table, UserData, Value, Variadic, + ffi, }; #[test] @@ -187,7 +188,7 @@ fn test_load_mode() -> Result<()> { #[cfg(not(feature = "luau"))] let bytecode = lua.load("return 1 + 1").into_function()?.dump(true); #[cfg(feature = "luau")] - let bytecode = mlua::Compiler::new().compile("return 1 + 1")?; + let bytecode = mlua::chunk::Compiler::new().compile("return 1 + 1")?; assert_eq!(lua.load(&bytecode).eval::()?, 2); assert_eq!(lua.load(&bytecode).set_mode(ChunkMode::Binary).eval::()?, 2); match lua.load(&bytecode).set_mode(ChunkMode::Text).exec() { diff --git a/tests/thread.rs b/tests/thread.rs index 2d978fec..a8b52b2d 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -2,7 +2,8 @@ use std::panic::catch_unwind; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadEvent, ThreadStatus, ThreadTriggers, Value}; +use mlua::thread::{ThreadEvent, ThreadStatus, ThreadTriggers}; +use mlua::{Error, Function, IntoLua, Lua, Result, Thread, Value}; #[test] fn test_thread() -> Result<()> { From b64da48c5e1d8f12fed3136c6d393f9bbba7f859 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 18:46:46 +0100 Subject: [PATCH 133/138] Make `NavigateError` as non_exhaustive (Luau) --- src/luau/require.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/luau/require.rs b/src/luau/require.rs index be52efcf..efd852f9 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -16,6 +16,7 @@ pub use fs::FsRequirer; /// An error that can occur during navigation in the Luau `require-by-string` system. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum NavigateError { /// The path is ambiguous (more than one candidate matches). Ambiguous, From 6b19eeb8238c8d577192a6d7e6b2a78b74af17ee Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 19:43:33 +0100 Subject: [PATCH 134/138] Make `VmState` as non_exhaustive --- src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.rs b/src/types.rs index b4cdb352..6c7704db 100644 --- a/src/types.rs +++ b/src/types.rs @@ -67,6 +67,7 @@ pub(crate) type AsyncCallbackUpvalue = Upvalue; pub(crate) type AsyncPollUpvalue = Upvalue>>>; /// Type to set next Lua VM action after executing interrupt or hook function. +#[non_exhaustive] pub enum VmState { Continue, /// Yield the current thread. From 7ddcdc1ffdd12ff584eb8d75891e34382e3eb7a2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 19:42:42 +0100 Subject: [PATCH 135/138] Add `Table::remove` --- src/table.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/table.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/table.rs b/src/table.rs index 92f69583..e93610ff 100644 --- a/src/table.rs +++ b/src/table.rs @@ -341,6 +341,54 @@ impl Table { } } + /// Removes a key from the table. + /// + /// If `key` is an integer, mlua shifts down the elements from `table[key+1]`, + /// and erases element `table[key]`. The complexity is `O(n)` in the worst case, + /// where `n` is the table length. + /// + /// For other key types this is equivalent to setting `table[key] = nil`. + /// + /// This might invoke the `__len`, `__index` and `__newindex` metamethods. + /// Use the [`raw_remove`] method if that is not desired. + /// + /// [`raw_remove`]: Table::raw_remove + pub fn remove(&self, key: impl IntoLua) -> Result<()> { + // Fast track (skip protected call) + if !self.has_metatable() { + return self.raw_remove(key); + } + + let lua = self.0.lua.lock(); + let key = key.into_lua(lua.lua())?; + match key { + Value::Integer(idx) => { + let size = self.len()?; + if idx < 1 || idx > size { + return Err(Error::runtime("index out of bounds")); + } + + let state = lua.state(); + unsafe { + let _sg = StackGuard::new(state); + check_stack(state, 4)?; + + lua.push_ref(&self.0); + protect_lua!(state, 1, 0, |state| { + for i in idx..size { + // table[i] = table[i+1] + ffi::lua_geti(state, -1, i + 1); + ffi::lua_seti(state, -2, i); + } + ffi::lua_pushnil(state); + ffi::lua_seti(state, -2, size); + }) + } + } + _ => self.set(key, Nil), + } + } + /// Compares two tables for equality. /// /// Tables are compared by reference first. diff --git a/tests/table.rs b/tests/table.rs index e0bc5f44..f4aaf9b0 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -142,6 +142,38 @@ fn test_table_insert_remove() -> Result<()> { Ok(()) } +#[test] +fn test_table_remove_metatable() -> Result<()> { + let lua = Lua::new(); + + let inner = lua.create_sequence_from([1, 2, 3, 4, 5])?; + let mt = lua.create_table()?; + mt.set("__index", &inner)?; + mt.set("__newindex", &inner)?; + mt.set("__len", { + let inner = inner.clone(); + lua.create_function(move |_, ()| Ok(inner.raw_len()))? + })?; + + let t = lua.create_table()?; + t.set_metatable(Some(mt))?; + + t.remove(2)?; // removes value `2` + assert_eq!(t.len()?, 4); + assert_eq!( + inner.pairs().collect::>>()?, + vec![(1, 1), (2, 3), (3, 4), (4, 5)] + ); + + // Remove non-integer key + t.set("abc", "abcdef")?; + assert_eq!(inner.get::("abc")?, "abcdef"); + t.remove("abc")?; + assert_eq!(inner.get::("abc")?, Value::Nil); + + Ok(()) +} + #[test] fn test_table_clear() -> Result<()> { let lua = Lua::new(); From 46d3783f4ae6391311eb7e3f224fbd7307ce86fb Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 19:44:48 +0100 Subject: [PATCH 136/138] Update CHANGELOG --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0719f119..6a0850c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.12.0 (Jul 05, 2026) + +Changes since v0.12.0-rc.2 + +- Create root re-exports are refactored (only essentials are re-exported, the rest is in the new submodules) +- Added `ThreadStatus::Normal` and `Thread::is_normal` (mimic `coroutine.status`) +- Added `Lua::set_jit_options` with support of Luau JIT inliner (Luau) +- Added `Value::as_vector`/`Value::is_vector` (Luau) +- Added `Table::remove` +- `serde`: tables with the array metatable are always encoded as arrays (incl. `detect_mixed_tables` option) +- impl `Hash` for `BorrowedStr`/`BorrowedBytes` +- `Lua::current_thread` resolves implicit async threads to their root owner (#706) +- Bugfixes and improvements + ## v0.12.0-rc.2 (Jun 06, 2026) - Add `#[derive(UserData)]` and `#[mlua::userdata_impl]` macros From 8eddf85de0e1576f07145d04201d07ca108d963c Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Sun, 5 Jul 2026 20:26:50 +0100 Subject: [PATCH 137/138] v0.12.0 --- Cargo.toml | 6 +++--- README.md | 6 ++---- mlua-sys/Cargo.toml | 2 +- mlua_derive/Cargo.toml | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d8595b40..406fcf9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua" -version = "0.12.0-rc.2" # remember to update mlua_derive +version = "0.12.0" # remember to update mlua_derive authors = ["Aleksandr Orlenko ", "kyren "] rust-version = "1.88" edition = "2024" @@ -50,7 +50,7 @@ userdata-wrappers = ["parking_lot/send_guard"] serialize = ["serde"] [dependencies] -mlua_derive = { version = "=0.12.0-rc.1", optional = true, path = "mlua_derive" } +mlua_derive = { version = "=0.12.0", optional = true, path = "mlua_derive" } bstr = { version = "1.0", features = ["std"], default-features = false } either = "1.0" num-traits = { version = "0.2.14" } @@ -64,7 +64,7 @@ anyhow = { version = "1.0", optional = true } inventory = { version = "0.3", optional = true } libc = "0.2" -ffi = { package = "mlua-sys", version = "0.11.0-rc.1", path = "mlua-sys" } +ffi = { package = "mlua-sys", version = "0.11.0", path = "mlua-sys" } [dev-dependencies] trybuild = "1.0" diff --git a/README.md b/README.md index a73a4b30..bcc307ff 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ [Benchmarks]: https://github.com/khvzak/script-bench-rs [FAQ]: FAQ.md -## The main branch is the development version of `mlua`. Please see the [v0.11](https://github.com/mlua-rs/mlua/tree/v0.11) branch for the stable versions of `mlua`. - `mlua` is a set of bindings to the [Lua](https://www.lua.org) programming language for Rust with a goal of providing a _safe_ (as much as possible), high level, easy to use, practical and flexible API. @@ -133,7 +131,7 @@ Add to `Cargo.toml`: ``` toml [dependencies] -mlua = { version = "0.11", features = ["lua54", "vendored"] } +mlua = { version = "0.12", features = ["lua54", "vendored"] } ``` `main.rs` @@ -168,7 +166,7 @@ Add to `Cargo.toml`: crate-type = ["cdylib"] [dependencies] -mlua = { version = "0.11", features = ["lua54", "module"] } +mlua = { version = "0.12", features = ["lua54", "module"] } ``` `lib.rs`: diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index 27d7ba1b..81a988e2 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua-sys" -version = "0.11.0-rc.1" +version = "0.11.0" authors = ["Aleksandr Orlenko "] rust-version = "1.88" edition = "2024" diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 7f0f6548..f19be27c 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua_derive" -version = "0.12.0-rc.1" +version = "0.12.0" authors = ["Aleksandr Orlenko "] rust-version = "1.88" edition = "2024" From 66b9f08e63a694ea7e4ced21b5ca9119bd78c8b3 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Tue, 28 Jul 2026 13:31:07 +0100 Subject: [PATCH 138/138] Fix coroutine stack handling after hook yields Preserve the coroutine stack when a debug hook yields, so Lua stack is not truncated by `StackGuard`. Fixes #723 --- src/state.rs | 2 +- src/state/extra.rs | 4 ++ src/state/raw.rs | 39 +++++++++++++++++++ src/thread.rs | 93 ++++++++++++++++++++++++++++------------------ tests/async.rs | 43 +++++++++++++++++++++ tests/hooks.rs | 34 +++++++++++++++++ 6 files changed, 177 insertions(+), 38 deletions(-) diff --git a/src/state.rs b/src/state.rs index 83e7ac94..a43fe72f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -782,7 +782,7 @@ impl Lua { pub fn remove_hook(&self) { let lua = self.lock(); unsafe { - ffi::lua_sethook(lua.state(), None, 0, 0); + lua.remove_thread_hook(lua.state()); } } diff --git a/src/state/extra.rs b/src/state/extra.rs index 9391195c..9f273572 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -81,6 +81,8 @@ pub(crate) struct ExtraData { pub(super) hook_callback: Option, #[cfg(not(feature = "luau"))] pub(super) hook_triggers: crate::debug::HookTriggers, + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + pub(super) hook_removed_while_yielded: bool, #[cfg(any(feature = "lua55", feature = "lua54"))] pub(super) warn_callback: Option, #[cfg(feature = "luau")] @@ -187,6 +189,8 @@ impl ExtraData { hook_callback: None, #[cfg(not(feature = "luau"))] hook_triggers: Default::default(), + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + hook_removed_while_yielded: false, #[cfg(any(feature = "lua55", feature = "lua54"))] warn_callback: None, #[cfg(feature = "luau")] diff --git a/src/state/raw.rs b/src/state/raw.rs index 5468f6e1..79588fdd 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -427,6 +427,10 @@ impl RawLua { if event == ffi::LUA_HOOKCOUNT || event == ffi::LUA_HOOKLINE { #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] if ffi::lua_isyieldable(state) != 0 { + if ffi::lua_gethook(state).is_none() { + let extra = ExtraData::get(state); + (*extra).hook_removed_while_yielded = true; + } ffi::lua_yield(state, 0); } #[cfg(any(feature = "lua52", feature = "lua51", feature = "luajit"))] @@ -518,6 +522,41 @@ impl RawLua { Ok(()) } + #[cfg(not(feature = "luau"))] + #[inline] + pub(crate) unsafe fn remove_thread_hook(&self, thread_state: *mut ffi::lua_State) { + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + if ffi::lua_status(thread_state) == ffi::LUA_YIELD && Self::has_hook_yielded_frame(thread_state) { + (*self.extra.get()).hook_removed_while_yielded = true; + } + ffi::lua_sethook(thread_state, None, 0, 0); + } + + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + unsafe fn has_hook_yielded_frame(thread_state: *mut ffi::lua_State) -> bool { + let mut ar = mem::zeroed::(); + ffi::lua_getstack(thread_state, 0, &mut ar) != 0 + && ffi::lua_getinfo(thread_state, cstr!("S"), &mut ar) != 0 + && !ar.what.is_null() + && CStr::from_ptr(ar.what).to_bytes() != b"C" + } + + pub(crate) unsafe fn is_hook_yielded(&self, thread_state: *mut ffi::lua_State) -> bool { + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + { + if ffi::lua_gethook(thread_state).is_none() && !(*self.extra.get()).hook_removed_while_yielded { + return false; + } + Self::has_hook_yielded_frame(thread_state) + } + + #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "lua53")))] + { + let _ = thread_state; + false + } + } + /// See [`Lua::create_string`] pub(crate) unsafe fn create_string(&self, s: &[u8]) -> Result { let state = self.state(); diff --git a/src/thread.rs b/src/thread.rs index 968be182..1ce31dc2 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -185,12 +185,6 @@ enum ThreadStatusInner { } impl ThreadStatusInner { - #[cfg(feature = "async")] - #[inline(always)] - fn is_resumable(self) -> bool { - matches!(self, ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_)) - } - #[inline(always)] fn is_yielded(self) -> bool { matches!(self, ThreadStatusInner::Yielded(_)) @@ -329,7 +323,7 @@ impl Thread { { let lua = self.0.lua.lock(); check_thread_reentrancy(self.state(), &lua)?; - let mut pushed_nargs = self.resumable_nargs(&lua)?; + let (mut pushed_nargs, mut hook_yielded) = self.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.state(); @@ -341,18 +335,24 @@ impl Thread { if exec_thread_event(&lua, on_resume, thread_state, || { ThreadEvent::Resume(self.clone()) })? { - pushed_nargs = self.resumable_nargs(&lua)?; + (pushed_nargs, hook_yielded) = self.resumable_state(&lua)?; } - let nargs = args.push_into_stack_multi(&lua)?; - if nargs > 0 { - check_stack(thread_state, nargs)?; - ffi::lua_xmove(state, thread_state, nargs); - pushed_nargs += nargs; + if !hook_yielded { + let nargs = args.push_into_stack_multi(&lua)?; + if nargs > 0 { + check_stack(thread_state, nargs)?; + ffi::lua_xmove(state, thread_state, nargs); + pushed_nargs += nargs; + } } - let _thread_sg = StackGuard::with_top(thread_state, 0); + let mut thread_sg = StackGuard::with_top(thread_state, 0); let (status, nresults) = self.resume_inner(&lua, pushed_nargs)?; + if status.is_yielded() && self.is_hook_yielded(&lua) { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); + } check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); @@ -478,15 +478,24 @@ impl Thread { } } - /// Returns the number of pending arguments on the thread stack if the thread is resumable. + /// Returns the pending argument count and whether the thread was interrupted by a hook. #[inline] - fn resumable_nargs(&self, lua: &RawLua) -> Result { + fn resumable_state(&self, lua: &RawLua) -> Result<(c_int, bool)> { match self.status_inner(lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => Ok(nargs), + ThreadStatusInner::New(nargs) => Ok((nargs, false)), + ThreadStatusInner::Yielded(nargs) => { + let hook_yielded = self.is_hook_yielded(lua); + Ok((if hook_yielded { 0 } else { nargs }, hook_yielded)) + } _ => Err(Error::CoroutineUnresumable), } } + /// Distinguishes a hook interruption from a normal yield. + fn is_hook_yielded(&self, lua: &RawLua) -> bool { + unsafe { lua.is_hook_yielded(self.state()) } + } + /// Returns `true` if this thread is resumable (meaning it can be resumed by calling /// [`Thread::resume`]). #[inline(always)] @@ -548,9 +557,9 @@ impl Thread { #[cfg(not(feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] pub fn remove_hook(&self) { - let _lua = self.0.lua.lock(); + let lua = self.0.lua.lock(); unsafe { - ffi::lua_sethook(self.state(), None, 0, 0); + lua.remove_thread_hook(self.state()); } } @@ -681,19 +690,19 @@ impl Thread { { let lua = self.0.lua.lock(); check_thread_reentrancy(self.state(), &lua)?; - if !self.status_inner(&lua).is_resumable() { - return Err(Error::CoroutineUnresumable); - } + let (_, hook_yielded) = self.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.state(); unsafe { let _sg = StackGuard::new(state); - let nargs = args.push_into_stack_multi(&lua)?; - if nargs > 0 { - check_stack(thread_state, nargs)?; - ffi::lua_xmove(state, thread_state, nargs); + if !hook_yielded { + let nargs = args.push_into_stack_multi(&lua)?; + if nargs > 0 { + check_stack(thread_state, nargs)?; + ffi::lua_xmove(state, thread_state, nargs); + } } Ok(AsyncThread { @@ -794,7 +803,7 @@ impl Drop for AsyncThread { { unsafe { let mut status = self.thread.status_inner(&lua); - if matches!(status, ThreadStatusInner::Yielded(0)) { + if matches!(status, ThreadStatusInner::Yielded(0)) && !self.thread.is_hook_yielded(&lua) { // The thread is dropped while yielded, resume it with the "terminate" signal ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0); if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) { @@ -820,8 +829,8 @@ impl Stream for AsyncThread { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let lua = self.thread.0.lua.lock(); check_thread_reentrancy(self.thread.state(), &lua)?; - let mut nargs = match self.thread.resumable_nargs(&lua) { - Ok(nargs) => nargs, + let mut nargs = match self.thread.resumable_state(&lua) { + Ok((nargs, _)) => nargs, Err(_) => return Poll::Ready(None), }; @@ -829,7 +838,7 @@ impl Stream for AsyncThread { let thread_state = self.thread.state(); unsafe { let _sg = StackGuard::new(state); - let _thread_sg = StackGuard::with_top(thread_state, 0); + let mut thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); // If the resume callback runs, it may touch this thread, so re-read the argument count @@ -837,15 +846,20 @@ impl Stream for AsyncThread { if exec_thread_event(&lua, on_resume, thread_state, || { ThreadEvent::Resume(self.thread.clone()) })? { - nargs = match self.thread.resumable_nargs(&lua) { - Ok(nargs) => nargs, + nargs = match self.thread.resumable_state(&lua) { + Ok((nargs, _)) => nargs, Err(_) => return Poll::Ready(None), }; } let (status, nresults) = (self.thread).resume_inner(&lua, nargs)?; + let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua); + if hook_yielded { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); + } - if status.is_yielded() && nresults == 1 && is_poll_pending(thread_state) { + if status.is_yielded() && !hook_yielded && nresults == 1 && is_poll_pending(thread_state) { // Exec thread yield callback let on_yield = lua.thread_event_triggers().on_yield; exec_thread_event(&lua, on_yield, thread_state, || { @@ -878,13 +892,13 @@ impl Future for AsyncThread { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let lua = self.thread.0.lua.lock(); check_thread_reentrancy(self.thread.state(), &lua)?; - let mut nargs = self.thread.resumable_nargs(&lua)?; + let (mut nargs, _) = self.thread.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.thread.state(); unsafe { let _sg = StackGuard::new(state); - let _thread_sg = StackGuard::with_top(thread_state, 0); + let mut thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); // If the resume callback runs, it may touch this thread, so re-read the argument count @@ -892,13 +906,18 @@ impl Future for AsyncThread { if exec_thread_event(&lua, on_resume, thread_state, || { ThreadEvent::Resume(self.thread.clone()) })? { - nargs = self.thread.resumable_nargs(&lua)?; + (nargs, _) = self.thread.resumable_state(&lua)?; } let (status, nresults) = self.thread.resume_inner(&lua, nargs)?; + let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua); + if hook_yielded { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); + } if status.is_yielded() { - let pending = nresults == 1 && is_poll_pending(thread_state); + let pending = !hook_yielded && nresults == 1 && is_poll_pending(thread_state); // Exec thread yield callback let on_yield = lua.thread_event_triggers().on_yield; diff --git a/tests/async.rs b/tests/async.rs index fb5a2bd7..40df52b0 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -687,6 +687,49 @@ async fn test_async_hook() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] +async fn test_async_hook_yield_preserves_stack() -> Result<()> { + use std::future::{Future, poll_fn}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::task::Poll; + + let lua = Lua::new(); + + let thread = lua.create_thread( + lua.load( + r#" + local x = 40 + local y = 2 + return x + y + "#, + ) + .into_function()?, + )?; + + let yielded = Arc::new(AtomicBool::new(false)); + let yielded2 = yielded.clone(); + thread.set_hook(mlua::HookTriggers::EVERY_LINE, move |lua, debug| { + if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) { + lua.remove_hook(); + return Ok(mlua::VmState::Yield); + } + Ok(mlua::VmState::Continue) + })?; + + let mut thread = Box::pin(thread.into_async::(())?); + poll_fn(|cx| { + assert!(thread.as_mut().poll(cx).is_pending()); + Poll::Ready(()) + }) + .await; + assert!(yielded.load(Ordering::Relaxed)); + lua.gc_collect()?; + assert_eq!(thread.await?, 42); + + Ok(()) +} + #[test] fn test_async_yield_with() -> Result<()> { let lua = Lua::new(); diff --git a/tests/hooks.rs b/tests/hooks.rs index 9d68c84b..8a6270a4 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -294,6 +294,40 @@ fn test_hook_yield() -> Result<()> { Ok(()) } +#[test] +#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] +fn test_hook_yield_preserves_stack() -> Result<()> { + let lua = Lua::new(); + + let func = lua + .load( + r#" + local x = { value = 40 } + local y = 2 + return x.value + y + "#, + ) + .into_function()?; + let co = lua.create_thread(func)?; + + let yielded = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let yielded2 = yielded.clone(); + co.set_hook(HookTriggers::EVERY_LINE, move |_lua, debug| { + if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) { + return Ok(VmState::Yield); + } + Ok(VmState::Continue) + })?; + + co.resume::<()>(())?; + assert!(yielded.load(Ordering::Relaxed)); + co.remove_hook(); + lua.gc_collect()?; + assert_eq!(co.resume::(())?, 42); + + Ok(()) +} + #[test] fn test_global_hook() -> Result<()> { let lua = Lua::new();