Skip to main content

lua_rs_runtime/
lib.rs

1//! Embedding helper for lua-rs.
2//!
3//! This crate sits above `lua-vm`, `lua-stdlib`, and `lua-parse` and exposes a
4//! handle-based embedding API: a [`Lua`] state, typed [`Value`] / [`Table`] /
5//! [`Function`] handles that root themselves via RAII, [`UserData`] for binding
6//! Rust types, and a typed [`LuaError`]. It also provides the common setup
7//! sequence (state, parser hook, host hooks, stdlib).
8//!
9//! # Userdata model
10//!
11//! Userdata behavior in lua-rs runs through real Lua metatables, exactly as in
12//! reference Lua 5.4. The runtime builds the metatable for a type once, on the
13//! first [`Lua::create_userdata`] for that `TypeId`, permanently roots it on
14//! the state, and shares it across every later value of the type. This keeps
15//! `getmetatable`, `setmetatable`, `rawget`, `debug.setmetatable`, and every
16//! other reflective Lua operation behaving as in C Lua, which is what lets
17//! lua-rs pass the upstream 5.4 test suite and stand in for C Lua in real
18//! embedders.
19//!
20//! Fields and methods both live on that single metatable. Register fields with
21//! [`UserDataMethods::add_field_method_get`] / `add_field_method_set` and
22//! methods with [`UserDataMethods::add_method`] / `add_method_mut`. The runtime
23//! composes a single `__index` whose lookup order is field, then method, then
24//! a raw `add_meta_method(MetaMethod::Index, ...)` if you registered one as an
25//! escape hatch, with the symmetric composition on `__newindex`.
26//!
27//! # Derive
28//!
29//! Enable the `derive` feature for `#[derive(LuaUserData)]`, `#[lua_methods]`,
30//! and `#[lua_impl(Display, PartialEq, PartialOrd)]`. The derive targets the
31//! field API above; `#[lua_methods]` exposes each `pub fn(&self / &mut self,
32//! ...)` as `obj:method(args)`; `#[lua_impl(...)]` wires `__tostring`, `__eq`,
33//! `__lt`, and `__le` from the type's Rust trait impls.
34//!
35//! ```ignore
36//! use lua_rs_runtime::{lua_methods, Lua, LuaUserData};
37//!
38//! #[derive(LuaUserData, PartialEq, PartialOrd)]
39//! #[lua(methods)]
40//! #[lua_impl(Display, PartialEq, PartialOrd)]
41//! struct Vec2 { pub x: f64, pub y: f64 }
42//!
43//! #[lua_methods]
44//! impl Vec2 {
45//!     pub fn length(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }
46//!     pub fn scale(&mut self, k: f64) { self.x *= k; self.y *= k; }
47//! }
48//! ```
49//!
50//! # Scope: lending non-`'static` borrows to Lua
51//!
52//! [`Lua::create_userdata`] takes its value by ownership, so the type must be
53//! `'static`. When you instead want to lend Lua a value that lives on the Rust
54//! stack for the duration of one call (typically a game engine's
55//! `&mut World`), use [`Lua::scope`]. A scope hands Lua a borrow that is
56//! invalidated the moment the scope closure returns: any Lua reference that
57//! escaped the scope fails with a clean runtime error on next use instead of
58//! touching freed memory.
59//!
60//! ```
61//! use lua_rs_runtime::{Lua, UserData, UserDataMethods};
62//!
63//! struct Counter { value: i64 }
64//! impl UserData for Counter {
65//!     fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
66//!         m.add_method_mut("inc", |_, this, by: i64| { this.value += by; Ok(this.value) });
67//!     }
68//! }
69//!
70//! let lua = Lua::new();
71//! let mut counter = Counter { value: 0 };
72//! lua.scope(|s| {
73//!     let ud = s.create_userdata_ref_mut(&lua, &mut counter)?;
74//!     lua.globals().set("c", &ud)?;
75//!     lua.load("c:inc(5); c:inc(7)").exec()
76//! }).unwrap();
77//! assert_eq!(counter.value, 12);
78//! ```
79//!
80//! [`Scope::create_function`] / [`Scope::create_function_mut`] do the same for
81//! closures that capture non-`'static` borrows. And
82//! [`AnyUserData::delegate`] builds a *sub-userdata* that re-borrows a field of
83//! its parent on every call (`world:entity(id)` returning a live `&mut Entity`),
84//! so an `App -> World -> Component` chain stays a chain of short borrows rather
85//! than one long-held `&mut`. See [`Lua::scope`] for the full contract.
86//!
87//! With the `derive` feature, a `#[lua_methods]` method that returns a
88//! reference is registered as a delegate automatically: `fn entity(&mut self,
89//! id: u32) -> &mut Entity` becomes `world:entity(id)` with no hand-written
90//! accessor. `&mut T` returns give a mutable delegate, `&T` a read-only one.
91//!
92//! # Known limitations and planned work
93//!
94//! - `#[lua_methods]` does not yet special-case methods that return
95//!   `Result<T, E>`, associated functions and constructors (`Type::new`), or
96//!   `Option<T>` parameters and returns.
97//! - The derive does not yet handle enums (a `register_enum::<T>()` path) or
98//!   the iteration, `__close`, and arithmetic metamethods. The runtime already
99//!   supports adding these as ordinary `add_meta_method` registrations today.
100
101use std::any::{Any, TypeId};
102use std::cell::{Cell, Ref, RefCell, RefMut};
103use std::collections::HashMap;
104use std::ffi::c_void;
105use std::fmt;
106use std::hash::Hash;
107use std::ops::{Deref, DerefMut};
108use std::panic::{catch_unwind, AssertUnwindSafe};
109use std::ptr::NonNull;
110use std::rc::Rc;
111
112use lua_stdlib::auxlib::load_buffer;
113use lua_stdlib::init::open_libs;
114use lua_types::closure::{LuaCClosure as RawLuaCClosure, LuaClosure as RawLuaClosure, LuaLClosure};
115use lua_types::gc::GcRef;
116use lua_types::string::LuaString as RawLuaString;
117use lua_types::upval::UpVal;
118use lua_types::userdata::LuaUserData as RawLuaUserData;
119use lua_types::value::{LuaTable as RawLuaTable, LuaValue as RawLuaValue};
120use lua_vm::state::{
121    new_state, CpuClockHook, DynLibLoadHook, DynLibSymbolHook, DynLibUnloadHook, EntropyHook,
122    EnvHook, ExternalRootKey, FileLoaderHook, FileOpenHook, FileRemoveHook, FileRenameHook,
123    InputHook, LuaCallable, LuaRustFunction, LuaState, OsExecuteHook, OutputHook, PopenHook,
124    TempNameHook, UnixTimeHook,
125};
126
127pub use lua_types::{LuaError, LuaFileHandle, LuaVersion, NumberModel};
128pub use lua_vm::state::{DynLibId, DynamicSymbol, OsExecuteReason, OsExecuteResult};
129
130#[cfg(feature = "derive")]
131pub use lua_rs_derive::{lua_methods, LuaUserData};
132
133pub type Error = LuaError;
134pub type Result<T> = std::result::Result<T, Error>;
135
136/// Host capabilities exposed to Lua stdlib.
137///
138/// Every field is optional. Missing file, process, and dynamic-loading hooks
139/// produce Lua errors or Lua failure tuples. On bare `wasm32-unknown-unknown`,
140/// missing stdio/time/env/temp hooks avoid unsupported Rust `std` stubs and fail
141/// at the Lua boundary. Native builds may still use compatibility fallbacks for
142/// some stdio and OS functions when hooks are absent.
143#[derive(Clone, Copy, Default)]
144pub struct HostHooks {
145    pub file_loader_hook: Option<FileLoaderHook>,
146    pub file_open_hook: Option<FileOpenHook>,
147    pub stdin_hook: Option<InputHook>,
148    pub stdout_hook: Option<OutputHook>,
149    pub stderr_hook: Option<OutputHook>,
150    pub env_hook: Option<EnvHook>,
151    pub unix_time_hook: Option<UnixTimeHook>,
152    pub cpu_clock_hook: Option<CpuClockHook>,
153    pub entropy_hook: Option<EntropyHook>,
154    pub temp_name_hook: Option<TempNameHook>,
155    pub popen_hook: Option<PopenHook>,
156    pub file_remove_hook: Option<FileRemoveHook>,
157    pub file_rename_hook: Option<FileRenameHook>,
158    pub os_execute_hook: Option<OsExecuteHook>,
159    pub dynlib_load_hook: Option<DynLibLoadHook>,
160    pub dynlib_symbol_hook: Option<DynLibSymbolHook>,
161    pub dynlib_unload_hook: Option<DynLibUnloadHook>,
162}
163
164impl HostHooks {
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    pub fn file_loader(mut self, hook: FileLoaderHook) -> Self {
170        self.file_loader_hook = Some(hook);
171        self
172    }
173
174    pub fn file_open(mut self, hook: FileOpenHook) -> Self {
175        self.file_open_hook = Some(hook);
176        self
177    }
178
179    pub fn stdin(mut self, hook: InputHook) -> Self {
180        self.stdin_hook = Some(hook);
181        self
182    }
183
184    pub fn stdout(mut self, hook: OutputHook) -> Self {
185        self.stdout_hook = Some(hook);
186        self
187    }
188
189    pub fn stderr(mut self, hook: OutputHook) -> Self {
190        self.stderr_hook = Some(hook);
191        self
192    }
193
194    pub fn env(mut self, hook: EnvHook) -> Self {
195        self.env_hook = Some(hook);
196        self
197    }
198
199    pub fn unix_time(mut self, hook: UnixTimeHook) -> Self {
200        self.unix_time_hook = Some(hook);
201        self
202    }
203
204    pub fn cpu_clock(mut self, hook: CpuClockHook) -> Self {
205        self.cpu_clock_hook = Some(hook);
206        self
207    }
208
209    pub fn entropy(mut self, hook: EntropyHook) -> Self {
210        self.entropy_hook = Some(hook);
211        self
212    }
213
214    pub fn temp_name(mut self, hook: TempNameHook) -> Self {
215        self.temp_name_hook = Some(hook);
216        self
217    }
218
219    pub fn popen(mut self, hook: PopenHook) -> Self {
220        self.popen_hook = Some(hook);
221        self
222    }
223
224    pub fn file_remove(mut self, hook: FileRemoveHook) -> Self {
225        self.file_remove_hook = Some(hook);
226        self
227    }
228
229    pub fn file_rename(mut self, hook: FileRenameHook) -> Self {
230        self.file_rename_hook = Some(hook);
231        self
232    }
233
234    pub fn os_execute(mut self, hook: OsExecuteHook) -> Self {
235        self.os_execute_hook = Some(hook);
236        self
237    }
238
239    pub fn dynlib_load(mut self, hook: DynLibLoadHook) -> Self {
240        self.dynlib_load_hook = Some(hook);
241        self
242    }
243
244    pub fn dynlib_symbol(mut self, hook: DynLibSymbolHook) -> Self {
245        self.dynlib_symbol_hook = Some(hook);
246        self
247    }
248
249    pub fn dynlib_unload(mut self, hook: DynLibUnloadHook) -> Self {
250        self.dynlib_unload_hook = Some(hook);
251        self
252    }
253
254    pub fn install(self, state: &mut LuaState) {
255        let global = &mut *state.global_mut();
256        global.file_loader_hook = self.file_loader_hook;
257        global.file_open_hook = self.file_open_hook;
258        global.stdin_hook = self.stdin_hook;
259        global.stdout_hook = self.stdout_hook;
260        global.stderr_hook = self.stderr_hook;
261        global.env_hook = self.env_hook;
262        global.unix_time_hook = self.unix_time_hook;
263        global.cpu_clock_hook = self.cpu_clock_hook;
264        global.entropy_hook = self.entropy_hook;
265        global.temp_name_hook = self.temp_name_hook;
266        global.popen_hook = self.popen_hook;
267        global.file_remove_hook = self.file_remove_hook;
268        global.file_rename_hook = self.file_rename_hook;
269        global.os_execute_hook = self.os_execute_hook;
270        global.dynlib_load_hook = self.dynlib_load_hook;
271        global.dynlib_symbol_hook = self.dynlib_symbol_hook;
272        global.dynlib_unload_hook = self.dynlib_unload_hook;
273    }
274}
275
276/// Primary owned embedding handle.
277///
278/// `Lua` is intentionally cheap to clone and single-threaded. State access is
279/// borrowed at the embedding boundary only; opcode dispatch still runs with
280/// direct `&mut LuaState` access. Captured Rust callbacks will need a call-path
281/// adapter that releases this boundary borrow before invoking user code.
282// VERSION SEAM (architecture decision, 2026-05): there is one shared runtime
283// (`LuaInner.state`) and the active Lua version is a flag — `LuaInner.version`,
284// mirrored onto `GlobalState.lua_version` — that the cold-path seams read
285// (lexer `global`-contextuality, parser global/for-const rules, per-version
286// stdlib roster, float `tostring` precision). It is deliberately NOT the
287// `enum Engine` / monomorphized `Semantics` the spec sketched: every version
288// difference implemented so far lives off the VM dispatch loop, so the flag
289// costs nothing on the hot path and a typed seam would be premature
290// abstraction. If/when a version difference must live *inside* the opcode
291// dispatch loop, introduce a monomorphized `Semantics` parameter at that point
292// (and revisit `specs/WEBLUA_MULTIVERSION_API_SPEC.md` §4.1). See
293// `specs/MULTIVERSION_PRELIM_REVIEW.md` M1/M2.
294
295#[derive(Clone)]
296pub struct Lua {
297    inner: Rc<LuaInner>,
298}
299
300struct LuaInner {
301    /// The Lua language version this instance speaks. Fixed for the instance's
302    /// life (the monomorphic-instance rule, spec §1.2). Mirrored onto
303    /// `GlobalState.lua_version`, which the version seams actually read.
304    version: LuaVersion,
305    state: RefCell<LuaState>,
306    active_state: Cell<*mut LuaState>,
307    pending_external_unroots: RefCell<Vec<ExternalRootKey>>,
308    /// One metatable per `UserData` type, built on first `create_userdata::<T>`
309    /// and reused for every later value of that type. Each entry is permanently
310    /// rooted in the state's external-root set, so it survives even when no
311    /// instance currently exists, and frees with the state.
312    userdata_metatables: RefCell<HashMap<TypeId, GcRef<RawLuaTable>>>,
313    /// Same shape as `userdata_metatables` but for the `Scope::create_userdata`
314    /// path: the method closures here downcast `host_value` to
315    /// `Rc<ScopedCell<T>>` and check the cell's validity flag before
316    /// dereferencing the pointer it holds.
317    userdata_scoped_metatables: RefCell<HashMap<TypeId, GcRef<RawLuaTable>>>,
318}
319
320struct UserDataCell<T> {
321    value: RefCell<T>,
322}
323
324// ---------------------------------------------------------------------------
325// Scope: pass non-`'static` borrows into Lua safely.
326//
327// `Scope::create_userdata::<T>(&mut data)` stores a raw pointer to `data` in a
328// `ScopedCell<T>` and registers the cell with the scope. While the scope is
329// alive the cell's pointer is dereferenced (validity-checked) on every method
330// call from Lua. When the scope drops, every registered cell's pointer is set
331// to `None`, so any leaked userdata calls return a clean Lua error instead of
332// using-after-the-borrow-ended.
333//
334// Safety model:
335// - The raw pointer's borrow originates from `&mut data`, whose lifetime is
336//   tied to the scope's lifetime via `&'scope mut T`. The borrow checker holds
337//   the borrow alive for the full scope body.
338// - Re-entrant access (a Lua callback that fires another callback on the same
339//   userdata) is rejected at runtime via `ScopedCell::borrow`'s shared/exclusive
340//   counter, mirroring `RefCell`.
341// - On scope drop, callbacks have already returned (they run synchronously
342//   inside the scope body), so `invalidate` only nulls the pointer; no
343//   concurrent dereference can be in progress.
344
345/// Holder for a borrowed Rust value passed into Lua via [`Scope::create_userdata`].
346///
347/// Generic over `T: 'static` so it satisfies the existing `UserData: 'static`
348/// requirement and `Any`-based downcast lookup; the actual borrow lifetime is
349/// erased into a raw pointer and re-checked on every access.
350struct ScopedCell<T: 'static> {
351    ptr: Cell<Option<NonNull<T>>>,
352    /// Same encoding as `RefCell`: positive = shared borrows, negative = one
353    /// exclusive borrow, zero = unborrowed.
354    borrow: Cell<isize>,
355}
356
357impl<T: 'static> ScopedCell<T> {
358    fn new(data: &mut T) -> Self {
359        Self {
360            ptr: Cell::new(Some(NonNull::from(data))),
361            borrow: Cell::new(0),
362        }
363    }
364
365    fn try_borrow(&self) -> Result<ScopedRef<'_, T>> {
366        let b = self.borrow.get();
367        if b < 0 {
368            return Err(LuaError::runtime(format_args!(
369                "scoped userdata is already mutably borrowed"
370            )));
371        }
372        let ptr = self.ptr.get().ok_or_else(|| {
373            LuaError::runtime(format_args!(
374                "scoped userdata is no longer valid (its scope has ended)"
375            ))
376        })?;
377        self.borrow.set(b + 1);
378        Ok(ScopedRef { cell: self, ptr })
379    }
380
381    fn try_borrow_mut(&self) -> Result<ScopedRefMut<'_, T>> {
382        let b = self.borrow.get();
383        if b != 0 {
384            return Err(LuaError::runtime(format_args!(
385                "scoped userdata is already borrowed"
386            )));
387        }
388        let ptr = self.ptr.get().ok_or_else(|| {
389            LuaError::runtime(format_args!(
390                "scoped userdata is no longer valid (its scope has ended)"
391            ))
392        })?;
393        self.borrow.set(-1);
394        Ok(ScopedRefMut { cell: self, ptr })
395    }
396}
397
398/// Trait-object handle a `Scope` uses to invalidate any cell type on drop
399/// without knowing its `T`.
400trait ScopeInvalidate {
401    fn invalidate(&self);
402}
403
404impl<T: 'static> ScopeInvalidate for ScopedCell<T> {
405    fn invalidate(&self) {
406        // Safe only because callbacks have all returned by the time `Scope`
407        // drops: they run synchronously inside the closure body. If a callback
408        // is somehow mid-execution, its `ScopedRef`/`ScopedRefMut` guard still
409        // has the raw pointer copied locally and dereferences it; the next
410        // `try_borrow*` after invalidate sees `ptr = None` and errors cleanly.
411        self.ptr.set(None);
412    }
413}
414
415struct ScopedRef<'a, T: 'static> {
416    cell: &'a ScopedCell<T>,
417    ptr: NonNull<T>,
418}
419
420impl<'a, T: 'static> Drop for ScopedRef<'a, T> {
421    fn drop(&mut self) {
422        self.cell.borrow.set(self.cell.borrow.get() - 1);
423    }
424}
425
426impl<'a, T: 'static> Deref for ScopedRef<'a, T> {
427    type Target = T;
428    fn deref(&self) -> &T {
429        // SAFETY: pointer was obtained from a live `&mut T` (or `&mut T`-derived)
430        // value whose lifetime spans the scope. Re-entrant borrow conflicts are
431        // rejected by `borrow` above. The pointer is set to `None` only when
432        // `invalidate` runs, which can only happen after `Scope` drops; by then
433        // no `ScopedRef` can exist because callbacks have returned.
434        unsafe { self.ptr.as_ref() }
435    }
436}
437
438struct ScopedRefMut<'a, T: 'static> {
439    cell: &'a ScopedCell<T>,
440    ptr: NonNull<T>,
441}
442
443impl<'a, T: 'static> Drop for ScopedRefMut<'a, T> {
444    fn drop(&mut self) {
445        self.cell.borrow.set(0);
446    }
447}
448
449impl<'a, T: 'static> Deref for ScopedRefMut<'a, T> {
450    type Target = T;
451    fn deref(&self) -> &T {
452        // SAFETY: same as `ScopedRef::deref`.
453        unsafe { self.ptr.as_ref() }
454    }
455}
456
457impl<'a, T: 'static> DerefMut for ScopedRefMut<'a, T> {
458    fn deref_mut(&mut self) -> &mut T {
459        // SAFETY: same as `ScopedRef::deref`, plus the cell's `borrow == -1`
460        // ensures no other shared or exclusive borrow is currently outstanding.
461        unsafe { self.ptr.as_mut() }
462    }
463}
464
465/// Handle passed to the closure body of [`Lua::scope`].
466///
467/// `Scope::create_userdata` produces an [`AnyUserData`] whose backing storage
468/// is a borrow you provide; when the scope drops every cell it created is
469/// invalidated. Any later Lua call that reaches one of those userdatas fails
470/// with a clean error rather than touching freed memory.
471pub struct Scope<'scope> {
472    invalidators: RefCell<Vec<Rc<dyn ScopeInvalidate>>>,
473    _phantom: std::marker::PhantomData<&'scope mut ()>,
474}
475
476impl<'scope> Scope<'scope> {
477    fn new() -> Self {
478        Self {
479            invalidators: RefCell::new(Vec::new()),
480            _phantom: std::marker::PhantomData,
481        }
482    }
483
484    /// Wrap a `&mut T` borrow as a Lua userdata that lives for the duration of
485    /// this scope. Any call from Lua to the returned userdata after the scope
486    /// ends fails with a clean Lua runtime error instead of touching the
487    /// freed borrow.
488    ///
489    /// Naming mirrors mlua's `Scope::create_userdata_ref_mut`. The bare
490    /// `create_userdata` name on `Scope` is intentionally reserved for the
491    /// future by-value, non-`'static` constructor (mlua's
492    /// `Scope::create_userdata<T: UserData + 'env>(T)`), tracked as a
493    /// follow-up to lua-rs#27.
494    pub fn create_userdata_ref_mut<T>(&self, lua: &Lua, data: &'scope mut T) -> Result<AnyUserData>
495    where
496        T: UserData,
497    {
498        let cell = Rc::new(ScopedCell::<T>::new(data));
499        self.invalidators
500            .borrow_mut()
501            .push(cell.clone() as Rc<dyn ScopeInvalidate>);
502        lua.create_scoped_userdata::<T>(cell)
503    }
504
505    /// Build a Lua [`Function`] from a non-`'static` Rust closure. The closure
506    /// is owned by a [`ScopedFnCell`] that the scope holds; once the scope
507    /// drops, the cell drops the closure and any later Lua call that reaches
508    /// the returned function fails cleanly with "no longer valid" instead of
509    /// touching the released captures.
510    ///
511    /// This is the function counterpart to [`Self::create_userdata`] — pair
512    /// them when you want to hand Lua a `&mut World` plus a few closures that
513    /// also borrow from the same stack frame.
514    pub fn create_function<A, R, F>(&self, lua: &Lua, func: F) -> Result<Function>
515    where
516        A: FromLuaMulti + 'static,
517        R: IntoLuaMulti + 'static,
518        F: Fn(&Lua, A) -> Result<R> + 'scope,
519    {
520        let adapter: Box<dyn Fn(&Lua, Vec<Value>) -> Result<Vec<Value>> + 'scope> =
521            Box::new(move |lua, args| {
522                let args = A::from_lua_multi(args, lua)?;
523                let returns = func(lua, args)?;
524                returns.into_lua_multi(lua)
525            });
526        self.install_function(lua, adapter)
527    }
528
529    /// Like [`Self::create_function`] but accepts an `FnMut`. Mirrors
530    /// [`Lua::create_function_mut`]: re-entrant calls into the same closure
531    /// are rejected with an "already borrowed" runtime error rather than
532    /// producing aliasing `&mut` captures.
533    pub fn create_function_mut<A, R, F>(&self, lua: &Lua, func: F) -> Result<Function>
534    where
535        A: FromLuaMulti + 'static,
536        R: IntoLuaMulti + 'static,
537        F: FnMut(&Lua, A) -> Result<R> + 'scope,
538    {
539        let func = RefCell::new(func);
540        self.create_function(lua, move |lua, args| {
541            let mut func = func.try_borrow_mut().map_err(|_| {
542                LuaError::runtime(format_args!("mutable Rust callback is already borrowed"))
543            })?;
544            func(lua, args)
545        })
546    }
547
548    /// Internal: launder the closure's `'scope` lifetime bound to `'static`
549    /// so the resulting cell can be held by a `'static` Lua callback, park
550    /// the box inside a [`ScopedFnCell`], and register that cell with the
551    /// scope so its closure is dropped on scope end.
552    fn install_function(
553        &self,
554        lua: &Lua,
555        adapter: Box<dyn Fn(&Lua, Vec<Value>) -> Result<Vec<Value>> + 'scope>,
556    ) -> Result<Function> {
557        // SAFETY: extending the trait-object lifetime bound from `'scope` to
558        // `'static` is sound here because the closure is owned by the
559        // [`ScopedFnCell`] we are about to build, that cell is registered in
560        // `self.invalidators`, and `Scope::drop` invokes `invalidate()` on
561        // every registered cell. `invalidate()` calls `take()` on the box,
562        // which drops the closure (and therefore its `'scope` captures)
563        // while `'scope` is still alive (we are mid-drop of `Scope`). After
564        // `invalidate()` the cell holds `None`, so any subsequent call sees
565        // "no longer valid" before it can reach a dangling capture.
566        let adapter_static: Box<dyn Fn(&Lua, Vec<Value>) -> Result<Vec<Value>>> =
567            unsafe { std::mem::transmute(adapter) };
568        let cell = Rc::new(ScopedFnCell {
569            boxed: RefCell::new(Some(adapter_static)),
570        });
571        self.invalidators
572            .borrow_mut()
573            .push(cell.clone() as Rc<dyn ScopeInvalidate>);
574        lua.create_scoped_function(cell)
575    }
576}
577
578impl<'scope> Drop for Scope<'scope> {
579    fn drop(&mut self) {
580        for inv in self.invalidators.borrow().iter() {
581            inv.invalidate();
582        }
583    }
584}
585
586/// Owns a scoped Rust closure on behalf of [`Scope`]. The closure is stored
587/// as `Box<dyn Fn(...)>` (lifetime laundered to `'static`); on scope drop the
588/// option is taken and the closure (with its `'scope` captures) is dropped.
589/// All later calls see `None` and return a clean Lua runtime error.
590struct ScopedFnCell {
591    boxed: RefCell<Option<Box<dyn Fn(&Lua, Vec<Value>) -> Result<Vec<Value>>>>>,
592}
593
594impl ScopedFnCell {
595    /// Dispatch the wrapped closure, or surface a clean error if the scope
596    /// already ended.
597    fn try_call(&self, lua: &Lua, args: Vec<Value>) -> Result<Vec<Value>> {
598        let guard = self.boxed.borrow();
599        let func = guard.as_deref().ok_or_else(|| {
600            LuaError::runtime(format_args!(
601                "scoped function is no longer valid (its scope has ended)"
602            ))
603        })?;
604        func(lua, args)
605    }
606}
607
608impl ScopeInvalidate for ScopedFnCell {
609    fn invalidate(&self) {
610        *self.boxed.borrow_mut() = None;
611    }
612}
613
614// ---------------------------------------------------------------------------
615// Delegated cell: a sub-userdata that re-acquires a fresh `&mut S` from a
616// parent cell on every method call. Lives at the same scope as the parent.
617//
618// The cell stores no live borrow itself. Instead it holds a closure that
619// knows how to enter the parent (`try_borrow_mut`), apply the user's
620// accessor (`|p: &mut P| -> &mut S`), invoke a caller-supplied callback
621// with the derived `&mut S`, then release the parent's borrow. Methods on
622// the sub-userdata's metatable invoke `enter_mut` to do their work; if a
623// nested Lua call tries to re-enter the parent during a delegate call, the
624// inner `try_borrow_mut` surfaces the same "already borrowed" error path
625// `ScopedCell` already uses.
626//
627// Invalidation: the cell holds an `Rc<dyn ScopeInvalidate>` for the parent
628// so the scope drop chain still works. The cell's own `invalidate` also
629// nulls the `enter_mut` closure to short-circuit any caller that managed
630// to retain a `Rc<DelegatedCell<S>>` past scope end (the closure captures
631// the parent cell's `Rc`, which we want to release).
632//
633// Generic over `S` only — the parent type `P` is type-erased inside the
634// closure so that one `Rc<DelegatedCell<S>>` covers any chain of accessors
635// regardless of where it bottomed out (`App -> World`, `World -> Inner`,
636// etc.). Composition (`delegate` on a delegated userdata) builds a fresh
637// closure that wraps the parent's `enter_mut` plus the new accessor.
638/// How a delegated cell reaches its referent. `Mut` borrows the parent
639/// exclusively and yields `&mut S` (from `delegate`); `Ref` borrows the
640/// parent shared and yields `&S` (from `delegate_ref`). A `Ref` delegate is
641/// read-only: a mutating child method on it fails cleanly.
642enum DelegateEnter<S: 'static> {
643    Mut(Box<dyn Fn(&mut dyn FnMut(&mut S)) -> Result<()>>),
644    Ref(Box<dyn Fn(&mut dyn FnMut(&S)) -> Result<()>>),
645}
646
647struct DelegatedCell<S: 'static> {
648    enter: RefCell<Option<DelegateEnter<S>>>,
649}
650
651impl<S: 'static> DelegatedCell<S> {
652    fn invalid() -> LuaError {
653        LuaError::runtime(format_args!(
654            "scoped userdata is no longer valid (its scope has ended)"
655        ))
656    }
657
658    /// Shared access. Works for both delegate kinds: a `Mut` cell yields
659    /// `&mut S` which is downgraded to `&S`; a `Ref` cell yields `&S`.
660    fn enter_ref(&self, f: &mut dyn FnMut(&S)) -> Result<()> {
661        let guard = self.enter.borrow();
662        match guard.as_ref().ok_or_else(Self::invalid)? {
663            DelegateEnter::Mut(g) => g(&mut |t| f(&*t)),
664            DelegateEnter::Ref(g) => g(f),
665        }
666    }
667
668    /// Exclusive access. Only a `Mut` delegate can grant it; a `Ref` delegate
669    /// is read-only and rejects mutating methods.
670    fn enter_mut(&self, f: &mut dyn FnMut(&mut S)) -> Result<()> {
671        let guard = self.enter.borrow();
672        match guard.as_ref().ok_or_else(Self::invalid)? {
673            DelegateEnter::Mut(g) => g(f),
674            DelegateEnter::Ref(_) => Err(LuaError::runtime(format_args!(
675                "cannot call a mutating method on a read-only delegated reference"
676            ))),
677        }
678    }
679}
680
681impl<S: 'static> ScopeInvalidate for DelegatedCell<S> {
682    fn invalidate(&self) {
683        *self.enter.borrow_mut() = None;
684    }
685}
686
687// ---------------------------------------------------------------------------
688
689struct RustCallbackCell {
690    function: LuaRustFunction,
691}
692
693struct ActiveStateGuard<'a> {
694    inner: &'a LuaInner,
695    previous: *mut LuaState,
696}
697
698impl Drop for ActiveStateGuard<'_> {
699    fn drop(&mut self) {
700        self.inner.active_state.set(self.previous);
701    }
702}
703
704impl LuaInner {
705    fn enter_active(&self, state: *mut LuaState) -> ActiveStateGuard<'_> {
706        let previous = self.active_state.replace(state);
707        ActiveStateGuard {
708            inner: self,
709            previous,
710        }
711    }
712
713    fn flush_pending_external_unroots(&self, state: &mut LuaState) {
714        let pending = self.pending_external_unroots.replace(Vec::new());
715        if pending.is_empty() {
716            return;
717        }
718
719        let mut still_pending = Vec::new();
720        for key in pending {
721            if state.try_external_unroot_value(key).is_err() {
722                still_pending.push(key);
723            }
724        }
725
726        if !still_pending.is_empty() {
727            self.pending_external_unroots
728                .borrow_mut()
729                .extend(still_pending);
730        }
731    }
732}
733
734impl fmt::Debug for Lua {
735    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736        f.debug_struct("Lua").finish_non_exhaustive()
737    }
738}
739
740impl Lua {
741    /// Create a Lua runtime with parser and standard libraries installed.
742    ///
743    /// Defaults to Lua 5.4 ([`LuaVersion::default`]). For another version use
744    /// [`Lua::new_versioned`].
745    pub fn new() -> Self {
746        Self::try_new().expect("Lua runtime should initialize")
747    }
748
749    /// Create a Lua runtime for a specific language version.
750    ///
751    /// The version is fixed for the instance's entire life (the
752    /// monomorphic-instance rule). It is reflected by [`Lua::version`] and by
753    /// the `_VERSION` global. No public embedding-API type carries the version;
754    /// it is a backend selector only.
755    ///
756    /// NOTE: only [`LuaVersion::V54`] has a real backend today. Other versions
757    /// currently run on the 5.4 engine (so the seam is end-to-end observable),
758    /// and will gain their own backends as the multi-version port proceeds.
759    pub fn new_versioned(version: LuaVersion) -> Self {
760        Self::try_new_versioned(version).expect("Lua runtime should initialize")
761    }
762
763    /// Fallible variant of [`Lua::new`].
764    pub fn try_new() -> Result<Self> {
765        Self::with_hooks(HostHooks::default())
766    }
767
768    /// Fallible variant of [`Lua::new_versioned`].
769    pub fn try_new_versioned(version: LuaVersion) -> Result<Self> {
770        Self::with_hooks_versioned(HostHooks::default(), version)
771    }
772
773    /// Create a Lua runtime with the supplied host capabilities.
774    pub fn with_hooks(hooks: HostHooks) -> Result<Self> {
775        Self::with_hooks_versioned(hooks, LuaVersion::default())
776    }
777
778    /// Create a Lua runtime with the supplied host capabilities for a specific
779    /// language version.
780    pub fn with_hooks_versioned(hooks: HostHooks, version: LuaVersion) -> Result<Self> {
781        if !version.is_supported() {
782            // Refuse rather than masquerade as 5.4: 5.1/5.2 (the float-only
783            // legacy family) have no backend yet. See specs/LUA_5_1_PLAN.md.
784            return Err(LuaError::runtime(format_args!(
785                "{} is not yet supported by lua-rs (supported: 5.3, 5.4, 5.5)",
786                version.version_str()
787            )));
788        }
789        let mut state = new_state().ok_or(LuaError::Memory)?;
790        state.global_mut().lua_version = version;
791        install_parser_hook(&mut state);
792        hooks.install(&mut state);
793        open_libs(&mut state)?;
794        let lua = Self::from_initialized_state(state, version);
795        lua.sync_version_global()?;
796        Ok(lua)
797    }
798
799    /// The Lua language version this instance speaks. Fixed at construction.
800    pub fn version(&self) -> LuaVersion {
801        self.inner.version
802    }
803
804    /// Make the `_VERSION` global reflect [`Lua::version`].
805    ///
806    /// `open_libs` writes the stdlib's compiled-in default (`"Lua 5.4"`); this
807    /// rewrites it from the instance's [`LuaVersion`] so the version is the
808    /// single source of truth. For a default 5.4 instance this writes the same
809    /// string, leaving behavior unchanged.
810    fn sync_version_global(&self) -> Result<()> {
811        self.globals()
812            .set("_VERSION", self.inner.version.version_str())
813    }
814
815    fn from_initialized_state(state: LuaState, version: LuaVersion) -> Self {
816        Lua {
817            inner: Rc::new(LuaInner {
818                version,
819                state: RefCell::new(state),
820                active_state: Cell::new(std::ptr::null_mut()),
821                pending_external_unroots: RefCell::new(Vec::new()),
822                userdata_metatables: RefCell::new(HashMap::new()),
823                userdata_scoped_metatables: RefCell::new(HashMap::new()),
824            }),
825        }
826    }
827
828    fn with_state<R>(&self, f: impl FnOnce(&mut LuaState) -> R) -> R {
829        if let Ok(mut state) = self.inner.state.try_borrow_mut() {
830            let _active = self.inner.enter_active(&mut *state);
831            self.inner.flush_pending_external_unroots(&mut state);
832            let result = f(&mut state);
833            self.inner.flush_pending_external_unroots(&mut state);
834            return result;
835        }
836
837        let state = self
838            .active_state_mut()
839            .expect("re-entrant Lua access without an active state");
840        let result = f(state);
841        self.inner.flush_pending_external_unroots(state);
842        result
843    }
844
845    fn active_state_mut(&self) -> Option<&mut LuaState> {
846        let state = self.inner.active_state.get();
847        if state.is_null() {
848            return None;
849        }
850
851        // SAFETY: `active_state` is set only while this `Lua` owns the outer
852        // `RefCell` borrow and is executing VM code. Re-entrant access can only
853        // happen when that VM frame has synchronously transferred control to a
854        // Rust callback and is suspended. The callback path does not touch the
855        // suspended `&mut LuaState` while user code re-enters through `Lua`.
856        Some(unsafe { &mut *state })
857    }
858
859    fn unroot_external_key(&self, key: ExternalRootKey) {
860        let removed = if let Ok(mut state) = self.inner.state.try_borrow_mut() {
861            let _active = self.inner.enter_active(&mut *state);
862            self.inner.flush_pending_external_unroots(&mut state);
863            let removed = state.try_external_unroot_value(key).is_ok();
864            self.inner.flush_pending_external_unroots(&mut state);
865            removed
866        } else {
867            if let Some(state) = self.active_state_mut() {
868                let removed = state.try_external_unroot_value(key).is_ok();
869                self.inner.flush_pending_external_unroots(state);
870                removed
871            } else {
872                false
873            }
874        };
875
876        if !removed {
877            self.inner.pending_external_unroots.borrow_mut().push(key);
878        }
879    }
880
881    fn root_raw(&self, value: RawLuaValue) -> RootedValue {
882        let key = self.with_state(|state| state.external_root_value(value));
883        RootedValue {
884            lua: self.clone(),
885            key,
886        }
887    }
888
889    fn root_raw_in_state(&self, state: &mut LuaState, value: RawLuaValue) -> RootedValue {
890        let key = state.external_root_value(value);
891        RootedValue {
892            lua: self.clone(),
893            key,
894        }
895    }
896
897    fn userdata_cell<'a, T: 'static>(
898        &self,
899        userdata: &'a AnyUserData,
900    ) -> Result<&'a UserDataCell<T>> {
901        if !Rc::ptr_eq(&self.inner, &userdata.root.lua.inner) {
902            return Err(LuaError::runtime(format_args!(
903                "Lua userdata belongs to a different state"
904            )));
905        }
906        userdata.host_cell()
907    }
908
909    /// Load a Lua source chunk.
910    pub fn load(&self, source: impl AsRef<[u8]>) -> Chunk {
911        Chunk {
912            lua: self.clone(),
913            source: source.as_ref().to_vec(),
914            name: b"chunk".to_vec(),
915        }
916    }
917
918    /// Return the global environment table.
919    pub fn globals(&self) -> Table {
920        let raw = self.with_state(|state| state.global().globals.clone());
921        Table {
922            root: self.root_raw(raw),
923        }
924    }
925
926    /// Create a new empty table.
927    pub fn create_table(&self) -> Result<Table> {
928        let root = self.with_state(|state| {
929            let _heap_guard = heap_guard(state);
930            let table = state.new_table();
931            let raw = RawLuaValue::Table(table);
932            let key = state.external_root_value(raw);
933            state.gc().check_step();
934            RootedValue {
935                lua: self.clone(),
936                key,
937            }
938        });
939        Ok(Table { root })
940    }
941
942    /// Create a new Lua string from bytes.
943    pub fn create_string(&self, bytes: impl AsRef<[u8]>) -> Result<LuaString> {
944        let bytes = bytes.as_ref();
945        let root = self.with_state(|state| {
946            let _heap_guard = heap_guard(state);
947            let string = state.new_string(bytes)?;
948            let raw = RawLuaValue::Str(string);
949            let key = state.external_root_value(raw);
950            state.gc().check_step();
951            Ok::<_, LuaError>(RootedValue {
952                lua: self.clone(),
953                key,
954            })
955        })?;
956        Ok(LuaString { root })
957    }
958
959    pub fn create_function<A, R, F>(&self, func: F) -> Result<Function>
960    where
961        A: FromLuaMulti + 'static,
962        R: IntoLuaMulti + 'static,
963        F: Fn(&Lua, A) -> Result<R> + 'static,
964    {
965        let lua_weak = Rc::downgrade(&self.inner);
966        let callable: LuaRustFunction = Rc::new(move |state| {
967            let lua = match lua_weak.upgrade() {
968                Some(inner) => Lua { inner },
969                None => {
970                    return Err(LuaError::runtime(format_args!(
971                        "Lua callback fired after the state was dropped"
972                    )))
973                }
974            };
975            match catch_unwind(AssertUnwindSafe(|| {
976                let args = callback_args(state, &lua)?;
977                let args = A::from_lua_multi(args, &lua)?;
978                let returns = func(&lua, args)?;
979                let returns = returns.into_lua_multi(&lua)?;
980                push_callback_returns(state, &lua, returns)
981            })) {
982                Ok(result) => result,
983                Err(_) => Err(LuaError::runtime(format_args!("Rust callback panicked"))),
984            }
985        });
986        self.create_registered_function(callable)
987    }
988
989    pub fn create_function_mut<A, R, F>(&self, func: F) -> Result<Function>
990    where
991        A: FromLuaMulti + 'static,
992        R: IntoLuaMulti + 'static,
993        F: FnMut(&Lua, A) -> Result<R> + 'static,
994    {
995        let func = RefCell::new(func);
996        self.create_function(move |lua, args| {
997            let mut func = func.try_borrow_mut().map_err(|_| {
998                LuaError::runtime(format_args!("mutable Rust callback is already borrowed"))
999            })?;
1000            func(lua, args)
1001        })
1002    }
1003
1004    fn create_registered_function(&self, callable: LuaRustFunction) -> Result<Function> {
1005        let root = self.with_state(|state| {
1006            let trampoline = rust_callback_trampoline as lua_vm::state::LuaCFunction;
1007            let idx = {
1008                let mut global = state.global_mut();
1009                match global.c_functions.iter().position(|existing| {
1010                    existing
1011                        .as_bare()
1012                        .is_some_and(|existing| std::ptr::fn_addr_eq(existing, trampoline))
1013                }) {
1014                    Some(idx) => idx,
1015                    None => {
1016                        let idx = global.c_functions.len();
1017                        global.c_functions.push(LuaCallable::bare(trampoline));
1018                        idx
1019                    }
1020                }
1021            };
1022            let raw = with_heap_guard(state, || {
1023                let callback_payload = GcRef::new(RawLuaUserData {
1024                    data: Box::new([]),
1025                    uv: Vec::new(),
1026                    metatable: RefCell::new(None),
1027                    host_value: RefCell::new(Some(
1028                        Rc::new(RustCallbackCell { function: callable }) as Rc<dyn Any>,
1029                    )),
1030                });
1031                RawLuaValue::Function(RawLuaClosure::C(GcRef::new(RawLuaCClosure {
1032                    func: idx,
1033                    upvalues: vec![RawLuaValue::UserData(callback_payload)],
1034                })))
1035            });
1036            let key = state.external_root_value(raw);
1037            state.gc().check_step();
1038            RootedValue {
1039                lua: self.clone(),
1040                key,
1041            }
1042        });
1043        Ok(Function { root })
1044    }
1045
1046    fn create_userdata_method<T, A, R, F>(&self, method: F) -> Result<Function>
1047    where
1048        T: UserData,
1049        A: FromLuaMulti + 'static,
1050        R: IntoLuaMulti + 'static,
1051        F: Fn(&Lua, &T, A) -> Result<R> + 'static,
1052    {
1053        let lua_weak = Rc::downgrade(&self.inner);
1054        let callable: LuaRustFunction = Rc::new(move |state| {
1055            let lua = match lua_weak.upgrade() {
1056                Some(inner) => Lua { inner },
1057                None => {
1058                    return Err(LuaError::runtime(format_args!(
1059                        "Lua callback fired after the state was dropped"
1060                    )))
1061                }
1062            };
1063            match catch_unwind(AssertUnwindSafe(|| {
1064                let (userdata, args) = callback_userdata_args(state, &lua)?;
1065                let args = A::from_lua_multi(args, &lua)?;
1066                let cell = lua.userdata_cell::<T>(&userdata)?;
1067                let value = cell.value.try_borrow().map_err(|_| {
1068                    LuaError::runtime(format_args!("userdata is already mutably borrowed"))
1069                })?;
1070                let returns = method(&lua, &value, args)?;
1071                let returns = returns.into_lua_multi(&lua)?;
1072                push_callback_returns(state, &lua, returns)
1073            })) {
1074                Ok(result) => result,
1075                Err(_) => Err(LuaError::runtime(format_args!(
1076                    "Rust userdata method panicked"
1077                ))),
1078            }
1079        });
1080        self.create_registered_function(callable)
1081    }
1082
1083    fn create_userdata_method_mut<T, A, R, F>(&self, method: F) -> Result<Function>
1084    where
1085        T: UserData,
1086        A: FromLuaMulti + 'static,
1087        R: IntoLuaMulti + 'static,
1088        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static,
1089    {
1090        let lua_weak = Rc::downgrade(&self.inner);
1091        let callable: LuaRustFunction = Rc::new(move |state| {
1092            let lua = match lua_weak.upgrade() {
1093                Some(inner) => Lua { inner },
1094                None => {
1095                    return Err(LuaError::runtime(format_args!(
1096                        "Lua callback fired after the state was dropped"
1097                    )))
1098                }
1099            };
1100            match catch_unwind(AssertUnwindSafe(|| {
1101                let (userdata, args) = callback_userdata_args(state, &lua)?;
1102                let args = A::from_lua_multi(args, &lua)?;
1103                let cell = lua.userdata_cell::<T>(&userdata)?;
1104                let mut value = cell
1105                    .value
1106                    .try_borrow_mut()
1107                    .map_err(|_| LuaError::runtime(format_args!("userdata is already borrowed")))?;
1108                let returns = method(&lua, &mut value, args)?;
1109                let returns = returns.into_lua_multi(&lua)?;
1110                push_callback_returns(state, &lua, returns)
1111            })) {
1112                Ok(result) => result,
1113                Err(_) => Err(LuaError::runtime(format_args!(
1114                    "Rust userdata method panicked"
1115                ))),
1116            }
1117        });
1118        self.create_registered_function(callable)
1119    }
1120
1121    pub fn create_userdata<T>(&self, data: T) -> Result<AnyUserData>
1122    where
1123        T: UserData,
1124    {
1125        let type_id = TypeId::of::<T>();
1126        let cached = self
1127            .inner
1128            .userdata_metatables
1129            .borrow()
1130            .get(&type_id)
1131            .cloned();
1132        let metatable = match cached {
1133            Some(metatable) => metatable,
1134            None => {
1135                let mut methods = UserDataMethodRegistry::<T>::new(self);
1136                T::add_methods(&mut methods);
1137                T::add_meta_methods(&mut methods);
1138                let metatable = methods.build_metatable()?;
1139                self.inner
1140                    .userdata_metatables
1141                    .borrow_mut()
1142                    .insert(type_id, metatable.clone());
1143                metatable
1144            }
1145        };
1146        self.attach_userdata(data, metatable)
1147    }
1148
1149    /// Wrap `data` in a fresh Lua userdata that shares `metatable` (built once per
1150    /// type by [`Lua::create_userdata`]). Only the per-value data cell is allocated
1151    /// here; the binding closures live on the shared, cached metatable.
1152    fn attach_userdata<T: UserData>(
1153        &self,
1154        data: T,
1155        metatable: GcRef<RawLuaTable>,
1156    ) -> Result<AnyUserData> {
1157        let cell: Rc<dyn Any> = Rc::new(UserDataCell {
1158            value: RefCell::new(data),
1159        });
1160        let host_value = cell.clone();
1161        let root = self.with_state(|state| {
1162            let userdata = with_heap_guard(state, || {
1163                GcRef::new(RawLuaUserData {
1164                    data: Box::new([]),
1165                    uv: Vec::new(),
1166                    metatable: RefCell::new(None),
1167                    host_value: RefCell::new(None),
1168                })
1169            });
1170            userdata.set_metatable(Some(metatable));
1171            userdata.set_host_value(Some(cell));
1172            let key = state.external_root_value(RawLuaValue::UserData(userdata));
1173            RootedValue {
1174                lua: self.clone(),
1175                key,
1176            }
1177        });
1178        Ok(AnyUserData {
1179            root,
1180            host_value: Some(host_value),
1181        })
1182    }
1183
1184    /// Run `f` with a fresh [`Scope`]; any [`AnyUserData`] created via the
1185    /// scope is invalidated when `f` returns, so leaked references fail
1186    /// cleanly instead of using-after-the-borrow-ended.
1187    ///
1188    /// ```
1189    /// use lua_rs_runtime::{Lua, UserData, UserDataMethods};
1190    ///
1191    /// struct Counter { value: i64 }
1192    ///
1193    /// impl UserData for Counter {
1194    ///     fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
1195    ///         methods.add_method_mut("inc", |_lua, this, delta: i64| {
1196    ///             this.value += delta;
1197    ///             Ok(this.value)
1198    ///         });
1199    ///     }
1200    /// }
1201    ///
1202    /// let lua = Lua::new();
1203    /// let mut counter = Counter { value: 0 };
1204    ///
1205    /// lua.scope(|scope| {
1206    ///     let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
1207    ///     lua.globals().set("c", &ud)?;
1208    ///     lua.load("c:inc(5); c:inc(7)").exec()
1209    /// }).unwrap();
1210    ///
1211    /// assert_eq!(counter.value, 12);
1212    ///
1213    /// // The script can stash the userdata on a global and try to use it
1214    /// // later, but the call cleanly errors instead of touching the
1215    /// // dropped `&mut counter`:
1216    /// lua.scope(|scope| {
1217    ///     let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
1218    ///     lua.globals().set("leaked", &ud)
1219    /// }).unwrap();
1220    /// assert!(lua.load("leaked:inc(1)").exec().is_err());
1221    /// ```
1222    pub fn scope<F, R>(&self, f: F) -> Result<R>
1223    where
1224        F: for<'scope> FnOnce(&Scope<'scope>) -> Result<R>,
1225    {
1226        let scope = Scope::new();
1227        let result = f(&scope);
1228        // `scope` drops here, invalidating every cell it created. After this
1229        // point any Lua call that reaches a scoped userdata sees `ptr = None`
1230        // and errors.
1231        drop(scope);
1232        result
1233    }
1234
1235    /// Build (or reuse) the per-`TypeId` *scoped* metatable for `T`. Same
1236    /// metatable serves both `Scope::create_userdata_ref_mut` userdata and
1237    /// `AnyUserData::delegate` sub-userdata of type `T`, because the
1238    /// dispatch closures are cell-variant-polymorphic via
1239    /// `dispatch_scoped_borrow*`.
1240    fn scoped_metatable_for<T>(&self) -> Result<GcRef<RawLuaTable>>
1241    where
1242        T: UserData,
1243    {
1244        let type_id = TypeId::of::<T>();
1245        let cached = self
1246            .inner
1247            .userdata_scoped_metatables
1248            .borrow()
1249            .get(&type_id)
1250            .cloned();
1251        if let Some(mt) = cached {
1252            return Ok(mt);
1253        }
1254        let mut methods = UserDataMethodRegistry::<T>::new_scoped(self);
1255        T::add_methods(&mut methods);
1256        T::add_meta_methods(&mut methods);
1257        let mt = methods.build_metatable()?;
1258        self.inner
1259            .userdata_scoped_metatables
1260            .borrow_mut()
1261            .insert(type_id, mt.clone());
1262        Ok(mt)
1263    }
1264
1265    /// Attach the scoped metatable for `T` to a fresh userdata whose
1266    /// `host_value` is the given `ScopedCell<T>`.
1267    fn create_scoped_userdata<T>(&self, cell: Rc<ScopedCell<T>>) -> Result<AnyUserData>
1268    where
1269        T: UserData,
1270    {
1271        let metatable = self.scoped_metatable_for::<T>()?;
1272        self.attach_scoped_userdata::<T>(cell, metatable)
1273    }
1274
1275    /// Same as `create_scoped_userdata` but the `host_value` is a
1276    /// `DelegatedCell<S>`. The metatable is the same per-`TypeId` cached
1277    /// metatable for `S`; dispatch handles both cell variants.
1278    fn create_delegated_userdata<S>(&self, cell: Rc<DelegatedCell<S>>) -> Result<AnyUserData>
1279    where
1280        S: UserData,
1281    {
1282        let metatable = self.scoped_metatable_for::<S>()?;
1283        let host_value: Rc<dyn Any> = cell;
1284        let root = self.with_state(|state| {
1285            let userdata = with_heap_guard(state, || {
1286                GcRef::new(RawLuaUserData {
1287                    data: Box::new([]),
1288                    uv: Vec::new(),
1289                    metatable: RefCell::new(None),
1290                    host_value: RefCell::new(None),
1291                })
1292            });
1293            userdata.set_metatable(Some(metatable));
1294            userdata.set_host_value(Some(host_value.clone()));
1295            let key = state.external_root_value(RawLuaValue::UserData(userdata));
1296            RootedValue {
1297                lua: self.clone(),
1298                key,
1299            }
1300        });
1301        Ok(AnyUserData {
1302            root,
1303            host_value: Some(host_value),
1304        })
1305    }
1306
1307    /// Same shape as [`Self::attach_userdata`] but the `host_value` is the
1308    /// `ScopedCell` rather than a fresh `UserDataCell`.
1309    fn attach_scoped_userdata<T>(
1310        &self,
1311        cell: Rc<ScopedCell<T>>,
1312        metatable: GcRef<RawLuaTable>,
1313    ) -> Result<AnyUserData>
1314    where
1315        T: UserData,
1316    {
1317        let host_value: Rc<dyn Any> = cell;
1318        let root = self.with_state(|state| {
1319            let userdata = with_heap_guard(state, || {
1320                GcRef::new(RawLuaUserData {
1321                    data: Box::new([]),
1322                    uv: Vec::new(),
1323                    metatable: RefCell::new(None),
1324                    host_value: RefCell::new(None),
1325                })
1326            });
1327            userdata.set_metatable(Some(metatable));
1328            userdata.set_host_value(Some(host_value.clone()));
1329            let key = state.external_root_value(RawLuaValue::UserData(userdata));
1330            RootedValue {
1331                lua: self.clone(),
1332                key,
1333            }
1334        });
1335        Ok(AnyUserData {
1336            root,
1337            host_value: Some(host_value),
1338        })
1339    }
1340
1341    /// Polymorphic borrow over the cell variants reachable by a scoped
1342    /// userdata: `Rc<ScopedCell<T>>` (created via
1343    /// `Scope::create_userdata_ref_mut`) and `Rc<DelegatedCell<T>>`
1344    /// (created via `AnyUserData::delegate`).
1345    ///
1346    /// Each variant has its own borrow protocol, but from the caller's
1347    /// perspective both produce a `&T` (or `&mut T`) for the duration of
1348    /// the closure. The result is threaded back out via an `Option` slot
1349    /// to satisfy `FnMut`'s constraint on the inner callback. The slot is
1350    /// always populated by the enter path before it returns.
1351    fn dispatch_scoped_borrow<T, F, R>(
1352        &self,
1353        userdata: &AnyUserData,
1354        f: F,
1355    ) -> Result<R>
1356    where
1357        T: 'static,
1358        F: FnOnce(&T) -> Result<R>,
1359    {
1360        let host = userdata
1361            .host_value
1362            .as_ref()
1363            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1364
1365        if let Ok(scoped) = Rc::clone(host).downcast::<ScopedCell<T>>() {
1366            let borrow = scoped.try_borrow()?;
1367            return f(&*borrow);
1368        }
1369
1370        if let Ok(delegated) = Rc::clone(host).downcast::<DelegatedCell<T>>() {
1371            let mut slot: Option<Result<R>> = None;
1372            let mut f_slot = Some(f);
1373            delegated.enter_ref(&mut |t| {
1374                if let Some(f) = f_slot.take() {
1375                    slot = Some(f(t));
1376                }
1377            })?;
1378            return slot.expect("delegated enter_ref must invoke its callback");
1379        }
1380
1381        Err(LuaError::runtime(format_args!(
1382            "scoped userdata type mismatch"
1383        )))
1384    }
1385
1386    fn dispatch_scoped_borrow_mut<T, F, R>(
1387        &self,
1388        userdata: &AnyUserData,
1389        f: F,
1390    ) -> Result<R>
1391    where
1392        T: 'static,
1393        F: FnOnce(&mut T) -> Result<R>,
1394    {
1395        let host = userdata
1396            .host_value
1397            .as_ref()
1398            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1399
1400        if let Ok(scoped) = Rc::clone(host).downcast::<ScopedCell<T>>() {
1401            let mut borrow = scoped.try_borrow_mut()?;
1402            return f(&mut *borrow);
1403        }
1404
1405        if let Ok(delegated) = Rc::clone(host).downcast::<DelegatedCell<T>>() {
1406            let mut slot: Option<Result<R>> = None;
1407            let mut f_slot = Some(f);
1408            delegated.enter_mut(&mut |t| {
1409                if let Some(f) = f_slot.take() {
1410                    slot = Some(f(t));
1411                }
1412            })?;
1413            return slot.expect("delegated enter_mut must invoke its callback");
1414        }
1415
1416        Err(LuaError::runtime(format_args!(
1417            "scoped userdata type mismatch"
1418        )))
1419    }
1420
1421    /// Scoped variants of the four `create_userdata_method*` constructors. Each
1422    /// uses `dispatch_scoped_borrow*` so the same registered metatable serves
1423    /// both `Scope::create_userdata_ref_mut` userdata and
1424    /// `AnyUserData::delegate` sub-userdata.
1425    fn create_scoped_userdata_method<T, A, R, F>(&self, method: F) -> Result<Function>
1426    where
1427        T: UserData,
1428        A: FromLuaMulti + 'static,
1429        R: IntoLuaMulti + 'static,
1430        F: Fn(&Lua, &T, A) -> Result<R> + 'static,
1431    {
1432        let lua_weak = Rc::downgrade(&self.inner);
1433        let callable: LuaRustFunction = Rc::new(move |state| {
1434            let lua = match lua_weak.upgrade() {
1435                Some(inner) => Lua { inner },
1436                None => {
1437                    return Err(LuaError::runtime(format_args!(
1438                        "Lua callback fired after the state was dropped"
1439                    )))
1440                }
1441            };
1442            match catch_unwind(AssertUnwindSafe(|| {
1443                let (userdata, args) = callback_userdata_args(state, &lua)?;
1444                let args = A::from_lua_multi(args, &lua)?;
1445                let returns = lua.dispatch_scoped_borrow::<T, _, _>(&userdata, |t| {
1446                    method(&lua, t, args)
1447                })?;
1448                let returns = returns.into_lua_multi(&lua)?;
1449                push_callback_returns(state, &lua, returns)
1450            })) {
1451                Ok(result) => result,
1452                Err(_) => Err(LuaError::runtime(format_args!(
1453                    "Rust userdata method panicked"
1454                ))),
1455            }
1456        });
1457        self.create_registered_function(callable)
1458    }
1459
1460    fn create_scoped_userdata_method_mut<T, A, R, F>(&self, method: F) -> Result<Function>
1461    where
1462        T: UserData,
1463        A: FromLuaMulti + 'static,
1464        R: IntoLuaMulti + 'static,
1465        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static,
1466    {
1467        let lua_weak = Rc::downgrade(&self.inner);
1468        let callable: LuaRustFunction = Rc::new(move |state| {
1469            let lua = match lua_weak.upgrade() {
1470                Some(inner) => Lua { inner },
1471                None => {
1472                    return Err(LuaError::runtime(format_args!(
1473                        "Lua callback fired after the state was dropped"
1474                    )))
1475                }
1476            };
1477            match catch_unwind(AssertUnwindSafe(|| {
1478                let (userdata, args) = callback_userdata_args(state, &lua)?;
1479                let args = A::from_lua_multi(args, &lua)?;
1480                let returns = lua.dispatch_scoped_borrow_mut::<T, _, _>(&userdata, |t| {
1481                    method(&lua, t, args)
1482                })?;
1483                let returns = returns.into_lua_multi(&lua)?;
1484                push_callback_returns(state, &lua, returns)
1485            })) {
1486                Ok(result) => result,
1487                Err(_) => Err(LuaError::runtime(format_args!(
1488                    "Rust userdata method panicked"
1489                ))),
1490            }
1491        });
1492        self.create_registered_function(callable)
1493    }
1494
1495    /// Materialize a [`Function`] whose body dispatches through a
1496    /// [`ScopedFnCell`]. The cell is closed over by the `LuaRustFunction`
1497    /// trampoline; reads of `cell.ptr` are guarded inside `try_call`, so once
1498    /// the originating [`Scope`] drops, every subsequent invocation surfaces
1499    /// "no longer valid" instead of touching the released closure.
1500    fn create_scoped_function(&self, cell: Rc<ScopedFnCell>) -> Result<Function> {
1501        let lua_weak = Rc::downgrade(&self.inner);
1502        let callable: LuaRustFunction = Rc::new(move |state| {
1503            let lua = match lua_weak.upgrade() {
1504                Some(inner) => Lua { inner },
1505                None => {
1506                    return Err(LuaError::runtime(format_args!(
1507                        "Lua callback fired after the state was dropped"
1508                    )))
1509                }
1510            };
1511            match catch_unwind(AssertUnwindSafe(|| {
1512                let args = callback_args(state, &lua)?;
1513                let returns = cell.try_call(&lua, args)?;
1514                push_callback_returns(state, &lua, returns)
1515            })) {
1516                Ok(result) => result,
1517                Err(_) => Err(LuaError::runtime(format_args!(
1518                    "scoped Rust callback panicked"
1519                ))),
1520            }
1521        });
1522        self.create_registered_function(callable)
1523    }
1524
1525    /// Run a full garbage-collection cycle.
1526    pub fn gc_collect(&self) {
1527        self.with_state(|state| state.gc().full_collect());
1528    }
1529}
1530
1531pub struct Chunk {
1532    lua: Lua,
1533    source: Vec<u8>,
1534    name: Vec<u8>,
1535}
1536
1537impl Chunk {
1538    pub fn set_name(mut self, name: impl AsRef<[u8]>) -> Self {
1539        self.name = name.as_ref().to_vec();
1540        self
1541    }
1542
1543    pub fn exec(self) -> Result<()> {
1544        self.lua
1545            .with_state(|state| exec_state(state, &self.source, &self.name))
1546    }
1547
1548    pub fn eval<T: FromLuaMulti>(self) -> Result<T> {
1549        let raws = self.lua.with_state(|state| {
1550            let saved_top = state.top_idx();
1551            let status = load_buffer(state, &self.source, &self.name)?;
1552            if status != 0 {
1553                let err = state.pop();
1554                state.set_top_idx(saved_top);
1555                return Err(LuaError::from_value(err));
1556            }
1557            match lua_vm::api::pcall_k(state, 0, T::NRESULTS, 0, 0, None) {
1558                Ok(_) => {
1559                    let nresults = if T::NRESULTS < 0 {
1560                        state.top_idx().0.saturating_sub(saved_top.0) as i32
1561                    } else {
1562                        T::NRESULTS
1563                    };
1564                    let mut values = Vec::with_capacity(nresults as usize);
1565                    for _ in 0..nresults {
1566                        values.push(state.pop());
1567                    }
1568                    values.reverse();
1569                    state.set_top_idx(saved_top);
1570                    Ok(values)
1571                }
1572                Err(err) => {
1573                    state.set_top_idx(saved_top);
1574                    Err(err)
1575                }
1576            }
1577        })?;
1578        let values = raws
1579            .into_iter()
1580            .map(|raw| Value::from_raw(&self.lua, raw))
1581            .collect::<Result<Vec<_>>>()?;
1582        T::from_lua_multi(values, &self.lua)
1583    }
1584}
1585
1586#[derive(Debug)]
1587struct RootedValue {
1588    lua: Lua,
1589    key: ExternalRootKey,
1590}
1591
1592impl RootedValue {
1593    fn raw(&self) -> Result<RawLuaValue> {
1594        self.lua
1595            .with_state(|state| state.external_rooted_value(self.key))
1596            .ok_or_else(stale_handle_error)
1597    }
1598
1599    fn raw_for_lua(&self, lua: &Lua, state: &LuaState) -> Result<RawLuaValue> {
1600        if !Rc::ptr_eq(&self.lua.inner, &lua.inner) {
1601            return Err(LuaError::runtime(format_args!(
1602                "Lua handle belongs to a different state"
1603            )));
1604        }
1605        state
1606            .external_rooted_value(self.key)
1607            .ok_or_else(stale_handle_error)
1608    }
1609}
1610
1611impl Clone for RootedValue {
1612    fn clone(&self) -> Self {
1613        let raw = self.raw().expect("rooted Lua handle should not be stale");
1614        self.lua.root_raw(raw)
1615    }
1616}
1617
1618impl Drop for RootedValue {
1619    fn drop(&mut self) {
1620        self.lua.unroot_external_key(self.key);
1621    }
1622}
1623
1624/// Dynamically typed owned Lua value.
1625#[derive(Debug, Clone)]
1626pub enum Value {
1627    Nil,
1628    Boolean(bool),
1629    Integer(i64),
1630    Number(f64),
1631    String(LuaString),
1632    Table(Table),
1633    Function(Function),
1634    UserData(AnyUserData),
1635    LightUserData(*mut c_void),
1636    Thread(Thread),
1637}
1638
1639impl Value {
1640    fn from_raw(lua: &Lua, raw: RawLuaValue) -> Result<Self> {
1641        lua.with_state(|state| Self::from_raw_in_state(lua, state, raw))
1642    }
1643
1644    fn from_raw_in_state(lua: &Lua, state: &mut LuaState, raw: RawLuaValue) -> Result<Self> {
1645        Ok(match raw {
1646            RawLuaValue::Nil => Value::Nil,
1647            RawLuaValue::Bool(v) => Value::Boolean(v),
1648            RawLuaValue::Int(v) => Value::Integer(v),
1649            RawLuaValue::Float(v) => Value::Number(v),
1650            RawLuaValue::Str(v) => Value::String(LuaString {
1651                root: lua.root_raw_in_state(state, RawLuaValue::Str(v)),
1652            }),
1653            RawLuaValue::Table(v) => Value::Table(Table {
1654                root: lua.root_raw_in_state(state, RawLuaValue::Table(v)),
1655            }),
1656            RawLuaValue::Function(v) => Value::Function(Function {
1657                root: lua.root_raw_in_state(state, RawLuaValue::Function(v)),
1658            }),
1659            RawLuaValue::UserData(v) => {
1660                let host_value = v.host_value();
1661                Value::UserData(AnyUserData {
1662                    root: lua.root_raw_in_state(state, RawLuaValue::UserData(v)),
1663                    host_value,
1664                })
1665            }
1666            RawLuaValue::LightUserData(v) => Value::LightUserData(v),
1667            RawLuaValue::Thread(v) => Value::Thread(Thread {
1668                root: lua.root_raw_in_state(state, RawLuaValue::Thread(v)),
1669            }),
1670        })
1671    }
1672
1673    fn to_raw_for_lua(&self, lua: &Lua, state: &LuaState) -> Result<RawLuaValue> {
1674        match self {
1675            Value::Nil => Ok(RawLuaValue::Nil),
1676            Value::Boolean(v) => Ok(RawLuaValue::Bool(*v)),
1677            Value::Integer(v) => Ok(RawLuaValue::Int(*v)),
1678            Value::Number(v) => Ok(RawLuaValue::Float(*v)),
1679            Value::String(v) => v.root.raw_for_lua(lua, state),
1680            Value::Table(v) => v.root.raw_for_lua(lua, state),
1681            Value::Function(v) => v.root.raw_for_lua(lua, state),
1682            Value::UserData(v) => v.root.raw_for_lua(lua, state),
1683            Value::LightUserData(v) => Ok(RawLuaValue::LightUserData(*v)),
1684            Value::Thread(v) => v.root.raw_for_lua(lua, state),
1685        }
1686    }
1687}
1688
1689#[derive(Debug, Clone)]
1690pub struct Table {
1691    root: RootedValue,
1692}
1693
1694impl Table {
1695    fn raw_table(&self) -> Result<GcRef<RawLuaTable>> {
1696        match self.root.raw()? {
1697            RawLuaValue::Table(table) => Ok(table),
1698            other => Err(type_error_raw(&other, "table")),
1699        }
1700    }
1701
1702    pub fn get<K, V>(&self, key: K) -> Result<V>
1703    where
1704        K: IntoLua,
1705        V: FromLua,
1706    {
1707        let lua = self.root.lua.clone();
1708        let key = key.into_lua(&lua)?;
1709        let value_raw = lua.with_state(|state| {
1710            let key_raw = key.to_raw_for_lua(&lua, state)?;
1711            let table_raw = self.root.raw_for_lua(&lua, state)?;
1712            state.table_get_with_tm(&table_raw, &key_raw)
1713        })?;
1714        let value = Value::from_raw(&lua, value_raw)?;
1715        V::from_lua(value, &lua)
1716    }
1717
1718    pub fn set<K, V>(&self, key: K, value: V) -> Result<()>
1719    where
1720        K: IntoLua,
1721        V: IntoLua,
1722    {
1723        let lua = self.root.lua.clone();
1724        let key = key.into_lua(&lua)?;
1725        let value = value.into_lua(&lua)?;
1726        lua.with_state(|state| {
1727            let key_raw = key.to_raw_for_lua(&lua, state)?;
1728            let value_raw = value.to_raw_for_lua(&lua, state)?;
1729            let table_raw = self.root.raw_for_lua(&lua, state)?;
1730            state.table_set_with_tm(&table_raw, key_raw, value_raw)
1731        })
1732    }
1733
1734    pub fn len(&self) -> Result<u64> {
1735        Ok(self.raw_table()?.getn())
1736    }
1737}
1738
1739#[derive(Debug, Clone)]
1740pub struct Function {
1741    root: RootedValue,
1742}
1743
1744impl Function {
1745    pub fn call<A, R>(&self, args: A) -> Result<R>
1746    where
1747        A: IntoLuaMulti,
1748        R: FromLuaMulti,
1749    {
1750        let lua = self.root.lua.clone();
1751        let args = args.into_lua_multi(&lua)?;
1752        let result_raws = lua.with_state(|state| {
1753            let arg_raws = args
1754                .iter()
1755                .map(|value| value.to_raw_for_lua(&lua, state))
1756                .collect::<Result<Vec<_>>>()?;
1757            let function_raw = self.root.raw_for_lua(&lua, state)?;
1758            let saved_top = state.top_idx();
1759            state.push(function_raw);
1760            for arg in &arg_raws {
1761                state.push(*arg);
1762            }
1763            match lua_vm::api::pcall_k(state, arg_raws.len() as i32, R::NRESULTS, 0, 0, None) {
1764                Ok(_) => {
1765                    let nresults = if R::NRESULTS < 0 {
1766                        state.top_idx().0.saturating_sub(saved_top.0) as i32
1767                    } else {
1768                        R::NRESULTS
1769                    };
1770                    let mut results = Vec::with_capacity(nresults as usize);
1771                    for _ in 0..nresults {
1772                        results.push(state.pop());
1773                    }
1774                    results.reverse();
1775                    state.set_top_idx(saved_top);
1776                    Ok(results)
1777                }
1778                Err(err) => {
1779                    state.set_top_idx(saved_top);
1780                    Err(err)
1781                }
1782            }
1783        })?;
1784        let values = result_raws
1785            .into_iter()
1786            .map(|raw| Value::from_raw(&lua, raw))
1787            .collect::<Result<Vec<_>>>()?;
1788        R::from_lua_multi(values, &lua)
1789    }
1790}
1791
1792#[derive(Debug, Clone)]
1793pub struct LuaString {
1794    root: RootedValue,
1795}
1796
1797impl LuaString {
1798    fn raw_string(&self) -> Result<GcRef<RawLuaString>> {
1799        match self.root.raw()? {
1800            RawLuaValue::Str(string) => Ok(string),
1801            other => Err(type_error_raw(&other, "string")),
1802        }
1803    }
1804
1805    pub fn as_bytes(&self) -> Result<Vec<u8>> {
1806        Ok(self.raw_string()?.as_bytes().to_vec())
1807    }
1808
1809    pub fn to_str(&self) -> Result<String> {
1810        let bytes = self.as_bytes()?;
1811        String::from_utf8(bytes)
1812            .map_err(|_| LuaError::runtime(format_args!("string is not valid UTF-8")))
1813    }
1814}
1815
1816#[derive(Clone)]
1817pub struct AnyUserData {
1818    root: RootedValue,
1819    host_value: Option<Rc<dyn Any>>,
1820}
1821
1822impl fmt::Debug for AnyUserData {
1823    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1824        f.debug_struct("AnyUserData")
1825            .field("root", &self.root)
1826            .field("has_host_value", &self.host_value.is_some())
1827            .finish()
1828    }
1829}
1830
1831impl AnyUserData {
1832    fn host_cell<T: 'static>(&self) -> Result<&UserDataCell<T>> {
1833        let host = self
1834            .host_value
1835            .as_deref()
1836            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1837        host.downcast_ref::<UserDataCell<T>>()
1838            .ok_or_else(|| LuaError::runtime(format_args!("userdata type mismatch")))
1839    }
1840
1841    pub fn borrow<T>(&self) -> Result<Ref<'_, T>>
1842    where
1843        T: 'static,
1844    {
1845        self.host_cell::<T>()?
1846            .value
1847            .try_borrow()
1848            .map_err(|_| LuaError::runtime(format_args!("userdata is already mutably borrowed")))
1849    }
1850
1851    pub fn borrow_mut<T>(&self) -> Result<RefMut<'_, T>>
1852    where
1853        T: 'static,
1854    {
1855        self.host_cell::<T>()?
1856            .value
1857            .try_borrow_mut()
1858            .map_err(|_| LuaError::runtime(format_args!("userdata is already borrowed")))
1859    }
1860
1861    pub fn with_borrow<T, R>(&self, f: impl FnOnce(&T) -> R) -> Result<R>
1862    where
1863        T: 'static,
1864    {
1865        let value = self.borrow::<T>()?;
1866        Ok(f(&value))
1867    }
1868
1869    pub fn with_borrow_mut<T, R>(&self, f: impl FnOnce(&mut T) -> R) -> Result<R>
1870    where
1871        T: 'static,
1872    {
1873        let mut value = self.borrow_mut::<T>()?;
1874        Ok(f(&mut value))
1875    }
1876
1877    /// Downcast `host_value` to a [`ScopedCell<T>`] reference. Mirrors
1878    /// [`Self::host_cell`] but for userdata created via [`Scope::create_userdata`].
1879    fn host_scoped_cell<T: 'static>(&self) -> Result<&ScopedCell<T>> {
1880        let host = self
1881            .host_value
1882            .as_deref()
1883            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1884        host.downcast_ref::<ScopedCell<T>>()
1885            .ok_or_else(|| LuaError::runtime(format_args!("scoped userdata type mismatch")))
1886    }
1887
1888    /// Rust-side shared borrow of a [`Scope::create_userdata`] payload. Routes
1889    /// through the scoped cell, so calls after the scope has dropped fail with
1890    /// the same "no longer valid" error a Lua method call would see, instead
1891    /// of returning a stale reference.
1892    pub fn scoped_borrow<T, R>(&self, f: impl FnOnce(&T) -> R) -> Result<R>
1893    where
1894        T: 'static,
1895    {
1896        let cell = self.host_scoped_cell::<T>()?;
1897        let guard = cell.try_borrow()?;
1898        Ok(f(&*guard))
1899    }
1900
1901    /// Rust-side exclusive borrow of a [`Scope::create_userdata`] payload. Same
1902    /// invalidation guarantees as [`Self::scoped_borrow`].
1903    pub fn scoped_borrow_mut<T, R>(&self, f: impl FnOnce(&mut T) -> R) -> Result<R>
1904    where
1905        T: 'static,
1906    {
1907        let cell = self.host_scoped_cell::<T>()?;
1908        let mut guard = cell.try_borrow_mut()?;
1909        Ok(f(&mut *guard))
1910    }
1911
1912    /// Create a sub-userdata in the same scope that re-acquires `&mut S`
1913    /// from this userdata's payload via `accessor` on every method call.
1914    /// The sub-userdata holds no long-lived `&mut S`: every Lua method call
1915    /// borrows the parent (mut), applies `accessor`, runs the method,
1916    /// releases. If a script tries to call a parent method while inside a
1917    /// sub-userdata method body, the inner `try_borrow_mut` surfaces the
1918    /// same "already borrowed" error path scoped cells already use.
1919    ///
1920    /// Receiver must be a [`Scope::create_userdata_ref_mut`] userdata of
1921    /// type `P`, or another delegated userdata of type `P` (chains
1922    /// compose).
1923    ///
1924    /// Scope invalidation propagates: when the originating scope drops,
1925    /// both the parent and every delegated descendant become invalid.
1926    pub fn delegate<P, S, F>(&self, lua: &Lua, accessor: F) -> Result<AnyUserData>
1927    where
1928        P: UserData,
1929        S: UserData,
1930        F: Fn(&mut P) -> &mut S + 'static,
1931    {
1932        let host = self
1933            .host_value
1934            .as_ref()
1935            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1936
1937        // Two parent variants are allowed: a direct `ScopedCell<P>` from
1938        // `Scope::create_userdata_ref_mut`, or another `DelegatedCell<P>`
1939        // for multi-level chains.
1940        if let Ok(parent_cell) = Rc::clone(host).downcast::<ScopedCell<P>>() {
1941            let parent_for_closure = Rc::clone(&parent_cell);
1942            let enter: Box<dyn Fn(&mut dyn FnMut(&mut S)) -> Result<()>> =
1943                Box::new(move |f| {
1944                    let mut guard = parent_for_closure.try_borrow_mut()?;
1945                    f(accessor(&mut *guard));
1946                    Ok(())
1947                });
1948            let cell = Rc::new(DelegatedCell::<S> {
1949                enter: RefCell::new(Some(DelegateEnter::Mut(enter))),
1950            });
1951            return lua.create_delegated_userdata::<S>(cell);
1952        }
1953
1954        if let Ok(parent_cell) = Rc::clone(host).downcast::<DelegatedCell<P>>() {
1955            let parent_for_closure = Rc::clone(&parent_cell);
1956            let enter: Box<dyn Fn(&mut dyn FnMut(&mut S)) -> Result<()>> =
1957                Box::new(move |f| {
1958                    parent_for_closure.enter_mut(&mut |p| {
1959                        f(accessor(p));
1960                    })
1961                });
1962            let cell = Rc::new(DelegatedCell::<S> {
1963                enter: RefCell::new(Some(DelegateEnter::Mut(enter))),
1964            });
1965            return lua.create_delegated_userdata::<S>(cell);
1966        }
1967
1968        Err(LuaError::runtime(format_args!(
1969            "delegate: receiver is not a scoped userdata of the expected type"
1970        )))
1971    }
1972
1973    /// Shared counterpart to [`Self::delegate`]. The accessor takes `&P` and
1974    /// returns `&S`, the parent is borrowed shared per call, and the resulting
1975    /// sub-userdata is read-only: a mutating method on it fails with a clean
1976    /// runtime error. Used for `&self -> &S` accessors.
1977    pub fn delegate_ref<P, S, F>(&self, lua: &Lua, accessor: F) -> Result<AnyUserData>
1978    where
1979        P: UserData,
1980        S: UserData,
1981        F: Fn(&P) -> &S + 'static,
1982    {
1983        let host = self
1984            .host_value
1985            .as_ref()
1986            .ok_or_else(|| LuaError::runtime(format_args!("missing Rust userdata payload")))?;
1987
1988        if let Ok(parent_cell) = Rc::clone(host).downcast::<ScopedCell<P>>() {
1989            let parent_for_closure = Rc::clone(&parent_cell);
1990            let enter: Box<dyn Fn(&mut dyn FnMut(&S)) -> Result<()>> = Box::new(move |f| {
1991                let guard = parent_for_closure.try_borrow()?;
1992                f(accessor(&*guard));
1993                Ok(())
1994            });
1995            let cell = Rc::new(DelegatedCell::<S> {
1996                enter: RefCell::new(Some(DelegateEnter::Ref(enter))),
1997            });
1998            return lua.create_delegated_userdata::<S>(cell);
1999        }
2000
2001        if let Ok(parent_cell) = Rc::clone(host).downcast::<DelegatedCell<P>>() {
2002            let parent_for_closure = Rc::clone(&parent_cell);
2003            let enter: Box<dyn Fn(&mut dyn FnMut(&S)) -> Result<()>> = Box::new(move |f| {
2004                parent_for_closure.enter_ref(&mut |p| {
2005                    f(accessor(p));
2006                })
2007            });
2008            let cell = Rc::new(DelegatedCell::<S> {
2009                enter: RefCell::new(Some(DelegateEnter::Ref(enter))),
2010            });
2011            return lua.create_delegated_userdata::<S>(cell);
2012        }
2013
2014        Err(LuaError::runtime(format_args!(
2015            "delegate_ref: receiver is not a scoped userdata of the expected type"
2016        )))
2017    }
2018}
2019
2020#[derive(Debug, Clone)]
2021pub struct Thread {
2022    root: RootedValue,
2023}
2024
2025/// Variable argument or return list converted element-by-element.
2026///
2027/// This mirrors mlua's `Variadic<T>` enough for dynamic callback bridges:
2028/// `create_function(|_, args: Variadic<Value>| ...)` receives all Lua
2029/// arguments, and returning `Variadic<T>` pushes all contained values.
2030#[derive(Debug, Clone, Default, PartialEq, Eq)]
2031pub struct Variadic<T>(Vec<T>);
2032
2033impl<T> Variadic<T> {
2034    pub const fn new() -> Self {
2035        Self(Vec::new())
2036    }
2037
2038    pub fn with_capacity(capacity: usize) -> Self {
2039        Self(Vec::with_capacity(capacity))
2040    }
2041
2042    pub fn into_vec(self) -> Vec<T> {
2043        self.0
2044    }
2045}
2046
2047impl<T> Deref for Variadic<T> {
2048    type Target = Vec<T>;
2049
2050    fn deref(&self) -> &Self::Target {
2051        &self.0
2052    }
2053}
2054
2055impl<T> DerefMut for Variadic<T> {
2056    fn deref_mut(&mut self) -> &mut Self::Target {
2057        &mut self.0
2058    }
2059}
2060
2061impl<T> From<Vec<T>> for Variadic<T> {
2062    fn from(value: Vec<T>) -> Self {
2063        Self(value)
2064    }
2065}
2066
2067impl<T> From<Variadic<T>> for Vec<T> {
2068    fn from(value: Variadic<T>) -> Self {
2069        value.0
2070    }
2071}
2072
2073impl<T> FromIterator<T> for Variadic<T> {
2074    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
2075        Self(Vec::from_iter(iter))
2076    }
2077}
2078
2079impl<T> IntoIterator for Variadic<T> {
2080    type Item = T;
2081    type IntoIter = std::vec::IntoIter<T>;
2082
2083    fn into_iter(self) -> Self::IntoIter {
2084        self.0.into_iter()
2085    }
2086}
2087
2088pub trait UserData: 'static {
2089    fn add_methods<M: UserDataMethods<Self>>(_methods: &mut M)
2090    where
2091        Self: Sized,
2092    {
2093    }
2094
2095    fn add_meta_methods<M: UserDataMethods<Self>>(_methods: &mut M)
2096    where
2097        Self: Sized,
2098    {
2099    }
2100}
2101
2102pub trait UserDataMethods<T: UserData> {
2103    fn add_method<A, R, F>(&mut self, name: &str, method: F)
2104    where
2105        A: FromLuaMulti + 'static,
2106        R: IntoLuaMulti + 'static,
2107        F: Fn(&Lua, &T, A) -> Result<R> + 'static;
2108
2109    fn add_method_mut<A, R, F>(&mut self, name: &str, method: F)
2110    where
2111        A: FromLuaMulti + 'static,
2112        R: IntoLuaMulti + 'static,
2113        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static;
2114
2115    fn add_meta_method<A, R, F>(&mut self, metamethod: MetaMethod, method: F)
2116    where
2117        A: FromLuaMulti + 'static,
2118        R: IntoLuaMulti + 'static,
2119        F: Fn(&Lua, &T, A) -> Result<R> + 'static;
2120
2121    fn add_meta_method_mut<A, R, F>(&mut self, metamethod: MetaMethod, method: F)
2122    where
2123        A: FromLuaMulti + 'static,
2124        R: IntoLuaMulti + 'static,
2125        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static;
2126
2127    /// Register a getter for `obj.name`. The runtime composes all field getters,
2128    /// the method table, and any raw `__index` into a single `__index` so fields
2129    /// and methods coexist (lookup order: field, then method, then raw `__index`).
2130    fn add_field_method_get<R, F>(&mut self, name: &str, getter: F)
2131    where
2132        R: IntoLuaMulti + 'static,
2133        F: Fn(&Lua, &T) -> Result<R> + 'static;
2134
2135    /// Register a setter for `obj.name = value`. Assigning a field with no setter
2136    /// (or an unknown field) errors unless a raw `__newindex` handles it.
2137    fn add_field_method_set<A, F>(&mut self, name: &str, setter: F)
2138    where
2139        A: FromLuaMulti + 'static,
2140        F: Fn(&Lua, &mut T, A) -> Result<()> + 'static;
2141
2142    /// Register a "function-shape" method whose callback does not extract the
2143    /// typed `&T` automatically. The userdata handle (and any other args) is
2144    /// passed to the closure as a regular [`FromLuaMulti`] tuple, so `A` is
2145    /// usually `(AnyUserData, X, Y, ...)`.
2146    ///
2147    /// Equivalent to mlua's `UserDataMethods::add_function`. The main reason
2148    /// to reach for this over [`Self::add_method`] is when the callback body
2149    /// needs the [`AnyUserData`] handle for the receiver — most commonly to
2150    /// build a sub-userdata via [`AnyUserData::delegate`].
2151    fn add_function<A, R, F>(&mut self, name: &str, function: F)
2152    where
2153        A: FromLuaMulti + 'static,
2154        R: IntoLuaMulti + 'static,
2155        F: Fn(&Lua, A) -> Result<R> + 'static;
2156
2157    /// `FnMut` variant of [`Self::add_function`]. Re-entrant calls into the
2158    /// same closure are rejected with an "already borrowed" runtime error.
2159    fn add_function_mut<A, R, F>(&mut self, name: &str, function: F)
2160    where
2161        A: FromLuaMulti + 'static,
2162        R: IntoLuaMulti + 'static,
2163        F: FnMut(&Lua, A) -> Result<R> + 'static;
2164}
2165
2166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2167pub enum MetaMethod {
2168    Index,
2169    NewIndex,
2170    Add,
2171    Sub,
2172    Mul,
2173    Div,
2174    Mod,
2175    Pow,
2176    Unm,
2177    Len,
2178    Eq,
2179    Lt,
2180    Le,
2181    Concat,
2182    Call,
2183    ToString,
2184    Pairs,
2185}
2186
2187impl MetaMethod {
2188    fn name(self) -> &'static str {
2189        match self {
2190            MetaMethod::Index => "__index",
2191            MetaMethod::NewIndex => "__newindex",
2192            MetaMethod::Add => "__add",
2193            MetaMethod::Sub => "__sub",
2194            MetaMethod::Mul => "__mul",
2195            MetaMethod::Div => "__div",
2196            MetaMethod::Mod => "__mod",
2197            MetaMethod::Pow => "__pow",
2198            MetaMethod::Unm => "__unm",
2199            MetaMethod::Len => "__len",
2200            MetaMethod::Eq => "__eq",
2201            MetaMethod::Lt => "__lt",
2202            MetaMethod::Le => "__le",
2203            MetaMethod::Concat => "__concat",
2204            MetaMethod::Call => "__call",
2205            MetaMethod::ToString => "__tostring",
2206            MetaMethod::Pairs => "__pairs",
2207        }
2208    }
2209}
2210
2211/// Root `value` on the state for as long as the state itself lives.
2212///
2213/// The returned [`ExternalRootKey`] is intentionally discarded: this helper is
2214/// the explicit name for the "cached per-type metadata" rooting pattern used by
2215/// [`UserDataMethodRegistry::build_metatable`] (the metatable itself, the
2216/// field-getter / method / field-setter tables, and any raw `__index`/`__newindex`
2217/// referenced by the composed dispatch closures). Those values must stay
2218/// reachable for the state's whole lifetime and only ever free together with the
2219/// state. Do not call this for any value you want the GC to be able to collect
2220/// later: it is by design an un-undoable root.
2221fn root_for_state_lifetime(state: &mut LuaState, value: RawLuaValue) {
2222    let _ = state.external_root_value(value);
2223}
2224
2225/// Whether the registry wires methods through `create_userdata_method*` (owned
2226/// `T` in a `RefCell`) or through `create_scoped_userdata_method*`
2227/// (`Rc<ScopedCell<T>>` with a validity-checked pointer). The build_metatable
2228/// step is identical for both.
2229#[derive(Clone, Copy)]
2230enum RegistryMode {
2231    Owned,
2232    Scoped,
2233}
2234
2235struct UserDataMethodRegistry<'lua, T: UserData> {
2236    lua: &'lua Lua,
2237    mode: RegistryMode,
2238    methods: Vec<(String, Function)>,
2239    meta_methods: Vec<(MetaMethod, Function)>,
2240    fields_get: Vec<(String, Function)>,
2241    fields_set: Vec<(String, Function)>,
2242    error: Option<LuaError>,
2243    _marker: std::marker::PhantomData<T>,
2244}
2245
2246impl<'lua, T: UserData> UserDataMethodRegistry<'lua, T> {
2247    fn new(lua: &'lua Lua) -> Self {
2248        Self::with_mode(lua, RegistryMode::Owned)
2249    }
2250
2251    fn new_scoped(lua: &'lua Lua) -> Self {
2252        Self::with_mode(lua, RegistryMode::Scoped)
2253    }
2254
2255    fn with_mode(lua: &'lua Lua, mode: RegistryMode) -> Self {
2256        Self {
2257            lua,
2258            mode,
2259            methods: Vec::new(),
2260            meta_methods: Vec::new(),
2261            fields_get: Vec::new(),
2262            fields_set: Vec::new(),
2263            error: None,
2264            _marker: std::marker::PhantomData,
2265        }
2266    }
2267
2268    fn record(&mut self, result: Result<Function>, insert: impl FnOnce(&mut Self, Function)) {
2269        if self.error.is_some() {
2270            return;
2271        }
2272        match result {
2273            Ok(function) => insert(self, function),
2274            Err(err) => self.error = Some(err),
2275        }
2276    }
2277
2278    /// Build this type's metatable once: a method table plus any meta-methods,
2279    /// returning the raw table handle permanently rooted in the external-root set
2280    /// so it can be cached and shared by every value of the type.
2281    fn build_metatable(mut self) -> Result<GcRef<RawLuaTable>> {
2282        if let Some(err) = self.error.take() {
2283            return Err(err);
2284        }
2285
2286        let lua = self.lua;
2287
2288        let method_table = lua.create_table()?;
2289        for (name, function) in &self.methods {
2290            method_table.set(name.as_str(), function)?;
2291        }
2292
2293        let field_getters = lua.create_table()?;
2294        for (name, function) in &self.fields_get {
2295            field_getters.set(name.as_str(), function)?;
2296        }
2297        let field_setters = lua.create_table()?;
2298        for (name, function) in &self.fields_set {
2299            field_setters.set(name.as_str(), function)?;
2300        }
2301
2302        // Raw __index/__newindex are escape hatches that compose as the final
2303        // fallback; every other meta-method is set directly.
2304        let metatable = lua.create_table()?;
2305        let mut raw_index: Option<Function> = None;
2306        let mut raw_newindex: Option<Function> = None;
2307        for (metamethod, function) in &self.meta_methods {
2308            match metamethod {
2309                MetaMethod::Index => raw_index = Some(function.clone()),
2310                MetaMethod::NewIndex => raw_newindex = Some(function.clone()),
2311                other => {
2312                    metatable.set(other.name(), function)?;
2313                }
2314            }
2315        }
2316
2317        // __index: field getter, then method, then raw __index.
2318        //
2319        // - fields → must compose (field → method → raw via a single closure)
2320        // - raw_index + methods (no fields) → must compose (method → raw)
2321        // - raw_index only (no fields, no methods) → set raw __index directly,
2322        //   skipping the composed closure entirely. This is the common shape
2323        //   for bridges that bind reflected state via a raw `add_meta_method`
2324        //   (e.g. bms-lua-rs's `LuaRef`) and the lookup is on the hot path.
2325        // - method-only → method_table as __index (existing fast path)
2326        //
2327        // The composed closure deliberately captures raw `GcRef`/`RawLuaValue`
2328        // handles, not high-level `Table`/`Function`: each high-level wrapper
2329        // holds a `RootedValue` with a strong `Rc<LuaInner>`, which would cycle
2330        // through the heap-resident closure back to the state and leak it on
2331        // drop. Raw handles are rooted permanently via
2332        // [`root_for_state_lifetime`], and `Table`/`Function` views are rebuilt
2333        // per call from the closure's `&lua`.
2334        let has_fields_get = !self.fields_get.is_empty();
2335        let has_methods = !self.methods.is_empty();
2336        let needs_index_composition = has_fields_get || (raw_index.is_some() && has_methods);
2337
2338        if needs_index_composition {
2339            let (getters_raw, methods_raw, raw_index_raw) = lua.with_state(|state| {
2340                let g = match field_getters.root.raw_for_lua(lua, state)? {
2341                    RawLuaValue::Table(g) => g,
2342                    v => return Err(type_error_raw(&v, "table")),
2343                };
2344                root_for_state_lifetime(state, RawLuaValue::Table(g.clone()));
2345                let m = match method_table.root.raw_for_lua(lua, state)? {
2346                    RawLuaValue::Table(m) => m,
2347                    v => return Err(type_error_raw(&v, "table")),
2348                };
2349                root_for_state_lifetime(state, RawLuaValue::Table(m.clone()));
2350                let r = match &raw_index {
2351                    Some(f) => {
2352                        let rv = f.root.raw_for_lua(lua, state)?;
2353                        root_for_state_lifetime(state, rv.clone());
2354                        Some(rv)
2355                    }
2356                    None => None,
2357                };
2358                Ok::<_, LuaError>((g, m, r))
2359            })?;
2360            let index_fn = lua.create_function(move |lua, (ud, key): (Value, Value)| {
2361                let getters = Table {
2362                    root: lua.root_raw(RawLuaValue::Table(getters_raw.clone())),
2363                };
2364                let methods = Table {
2365                    root: lua.root_raw(RawLuaValue::Table(methods_raw.clone())),
2366                };
2367                if let Value::Function(getter) = getters.get::<_, Value>(key.clone())? {
2368                    return getter.call::<_, Value>(ud);
2369                }
2370                let method = methods.get::<_, Value>(key.clone())?;
2371                if !matches!(method, Value::Nil) {
2372                    return Ok(method);
2373                }
2374                if let Some(raw_idx) = &raw_index_raw {
2375                    let raw_fn = Function {
2376                        root: lua.root_raw(raw_idx.clone()),
2377                    };
2378                    return raw_fn.call::<_, Value>((ud, key));
2379                }
2380                Ok(Value::Nil)
2381            })?;
2382            metatable.set(MetaMethod::Index.name(), &index_fn)?;
2383        } else if let Some(raw) = raw_index.as_ref() {
2384            metatable.set(MetaMethod::Index.name(), raw)?;
2385        } else {
2386            metatable.set(MetaMethod::Index.name(), &method_table)?;
2387        }
2388
2389        // __newindex: field setter, then raw __newindex, else error. Same
2390        // composed-vs-pass-through choice as __index above.
2391        let has_fields_set = !self.fields_set.is_empty();
2392
2393        if has_fields_set {
2394            let (setters_raw, raw_newindex_raw) = lua.with_state(|state| {
2395                let s = match field_setters.root.raw_for_lua(lua, state)? {
2396                    RawLuaValue::Table(s) => s,
2397                    v => return Err(type_error_raw(&v, "table")),
2398                };
2399                root_for_state_lifetime(state, RawLuaValue::Table(s.clone()));
2400                let r = match &raw_newindex {
2401                    Some(f) => {
2402                        let rv = f.root.raw_for_lua(lua, state)?;
2403                        root_for_state_lifetime(state, rv.clone());
2404                        Some(rv)
2405                    }
2406                    None => None,
2407                };
2408                Ok::<_, LuaError>((s, r))
2409            })?;
2410            let newindex_fn =
2411                lua.create_function(move |lua, (ud, key, value): (Value, Value, Value)| {
2412                    let setters = Table {
2413                        root: lua.root_raw(RawLuaValue::Table(setters_raw.clone())),
2414                    };
2415                    if let Value::Function(setter) = setters.get::<_, Value>(key.clone())? {
2416                        return setter.call::<_, Value>((ud, value));
2417                    }
2418                    if let Some(raw) = &raw_newindex_raw {
2419                        let raw_fn = Function {
2420                            root: lua.root_raw(raw.clone()),
2421                        };
2422                        return raw_fn.call::<_, Value>((ud, key, value));
2423                    }
2424                    Err(LuaError::runtime(format_args!(
2425                        "cannot assign to unknown or read-only userdata field"
2426                    )))
2427                })?;
2428            metatable.set(MetaMethod::NewIndex.name(), &newindex_fn)?;
2429        } else if let Some(raw) = raw_newindex.as_ref() {
2430            metatable.set(MetaMethod::NewIndex.name(), raw)?;
2431        }
2432
2433        self.lua.with_state(|state| {
2434            let metatable_raw = metatable.root.raw_for_lua(self.lua, state)?;
2435            let RawLuaValue::Table(metatable) = metatable_raw else {
2436                return Err(type_error_raw(&metatable_raw, "table"));
2437            };
2438            root_for_state_lifetime(state, RawLuaValue::Table(metatable.clone()));
2439            Ok(metatable)
2440        })
2441    }
2442}
2443
2444impl<T: UserData> UserDataMethods<T> for UserDataMethodRegistry<'_, T> {
2445    fn add_method<A, R, F>(&mut self, name: &str, method: F)
2446    where
2447        A: FromLuaMulti + 'static,
2448        R: IntoLuaMulti + 'static,
2449        F: Fn(&Lua, &T, A) -> Result<R> + 'static,
2450    {
2451        let name = name.to_string();
2452        let result = match self.mode {
2453            RegistryMode::Owned => self.lua.create_userdata_method(method),
2454            RegistryMode::Scoped => self.lua.create_scoped_userdata_method(method),
2455        };
2456        self.record(result, move |this, function| {
2457            this.methods.push((name, function));
2458        });
2459    }
2460
2461    fn add_method_mut<A, R, F>(&mut self, name: &str, method: F)
2462    where
2463        A: FromLuaMulti + 'static,
2464        R: IntoLuaMulti + 'static,
2465        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static,
2466    {
2467        let name = name.to_string();
2468        let result = match self.mode {
2469            RegistryMode::Owned => self.lua.create_userdata_method_mut(method),
2470            RegistryMode::Scoped => self.lua.create_scoped_userdata_method_mut(method),
2471        };
2472        self.record(result, move |this, function| {
2473            this.methods.push((name, function));
2474        });
2475    }
2476
2477    fn add_meta_method<A, R, F>(&mut self, metamethod: MetaMethod, method: F)
2478    where
2479        A: FromLuaMulti + 'static,
2480        R: IntoLuaMulti + 'static,
2481        F: Fn(&Lua, &T, A) -> Result<R> + 'static,
2482    {
2483        let result = match self.mode {
2484            RegistryMode::Owned => self.lua.create_userdata_method(method),
2485            RegistryMode::Scoped => self.lua.create_scoped_userdata_method(method),
2486        };
2487        self.record(result, move |this, function| {
2488            this.meta_methods.push((metamethod, function));
2489        });
2490    }
2491
2492    fn add_meta_method_mut<A, R, F>(&mut self, metamethod: MetaMethod, method: F)
2493    where
2494        A: FromLuaMulti + 'static,
2495        R: IntoLuaMulti + 'static,
2496        F: Fn(&Lua, &mut T, A) -> Result<R> + 'static,
2497    {
2498        let result = match self.mode {
2499            RegistryMode::Owned => self.lua.create_userdata_method_mut(method),
2500            RegistryMode::Scoped => self.lua.create_scoped_userdata_method_mut(method),
2501        };
2502        self.record(result, move |this, function| {
2503            this.meta_methods.push((metamethod, function));
2504        });
2505    }
2506
2507    fn add_field_method_get<R, F>(&mut self, name: &str, getter: F)
2508    where
2509        R: IntoLuaMulti + 'static,
2510        F: Fn(&Lua, &T) -> Result<R> + 'static,
2511    {
2512        let name = name.to_string();
2513        let wrapped = move |lua: &Lua, this: &T, ()| getter(lua, this);
2514        let result = match self.mode {
2515            RegistryMode::Owned => self.lua.create_userdata_method(wrapped),
2516            RegistryMode::Scoped => self.lua.create_scoped_userdata_method(wrapped),
2517        };
2518        self.record(result, move |this, function| {
2519            this.fields_get.push((name, function));
2520        });
2521    }
2522
2523    fn add_field_method_set<A, F>(&mut self, name: &str, setter: F)
2524    where
2525        A: FromLuaMulti + 'static,
2526        F: Fn(&Lua, &mut T, A) -> Result<()> + 'static,
2527    {
2528        let name = name.to_string();
2529        let wrapped = move |lua: &Lua, this: &mut T, arg: A| setter(lua, this, arg);
2530        let result = match self.mode {
2531            RegistryMode::Owned => self.lua.create_userdata_method_mut(wrapped),
2532            RegistryMode::Scoped => self.lua.create_scoped_userdata_method_mut(wrapped),
2533        };
2534        self.record(result, move |this, function| {
2535            this.fields_set.push((name, function));
2536        });
2537    }
2538
2539    fn add_function<A, R, F>(&mut self, name: &str, function: F)
2540    where
2541        A: FromLuaMulti + 'static,
2542        R: IntoLuaMulti + 'static,
2543        F: Fn(&Lua, A) -> Result<R> + 'static,
2544    {
2545        let name = name.to_string();
2546        // Function-shape entries don't extract `&T` from the receiver, so
2547        // they reuse the existing top-level `Lua::create_function` directly
2548        // for both Owned and Scoped registry modes.
2549        let result = self.lua.create_function(function);
2550        self.record(result, move |this, function| {
2551            this.methods.push((name, function));
2552        });
2553    }
2554
2555    fn add_function_mut<A, R, F>(&mut self, name: &str, function: F)
2556    where
2557        A: FromLuaMulti + 'static,
2558        R: IntoLuaMulti + 'static,
2559        F: FnMut(&Lua, A) -> Result<R> + 'static,
2560    {
2561        let name = name.to_string();
2562        let result = self.lua.create_function_mut(function);
2563        self.record(result, move |this, function| {
2564            this.methods.push((name, function));
2565        });
2566    }
2567}
2568
2569pub trait IntoLua {
2570    fn into_lua(self, lua: &Lua) -> Result<Value>;
2571}
2572
2573pub trait FromLua: Sized {
2574    fn from_lua(value: Value, lua: &Lua) -> Result<Self>;
2575}
2576
2577pub trait IntoLuaMulti {
2578    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>>;
2579}
2580
2581pub trait FromLuaMulti: Sized {
2582    const NRESULTS: i32;
2583
2584    fn from_lua_multi(values: Vec<Value>, lua: &Lua) -> Result<Self>;
2585}
2586
2587impl IntoLua for Value {
2588    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2589        Ok(self)
2590    }
2591}
2592
2593impl IntoLua for &Value {
2594    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2595        Ok(self.clone())
2596    }
2597}
2598
2599impl FromLua for Value {
2600    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2601        Ok(value)
2602    }
2603}
2604
2605impl IntoLua for bool {
2606    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2607        Ok(Value::Boolean(self))
2608    }
2609}
2610
2611impl FromLua for bool {
2612    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2613        match value {
2614            Value::Boolean(v) => Ok(v),
2615            other => Err(type_error_value(&other, "boolean")),
2616        }
2617    }
2618}
2619
2620impl IntoLua for i64 {
2621    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2622        Ok(Value::Integer(self))
2623    }
2624}
2625
2626impl FromLua for i64 {
2627    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2628        match value {
2629            Value::Integer(v) => Ok(v),
2630            Value::Number(v) if v.fract() == 0.0 && v.is_finite() => Ok(v as i64),
2631            other => Err(type_error_value(&other, "integer")),
2632        }
2633    }
2634}
2635
2636impl IntoLua for i32 {
2637    fn into_lua(self, lua: &Lua) -> Result<Value> {
2638        i64::from(self).into_lua(lua)
2639    }
2640}
2641
2642impl FromLua for i32 {
2643    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2644        let v = i64::from_lua(value, lua)?;
2645        i32::try_from(v).map_err(|_| LuaError::runtime(format_args!("integer out of range")))
2646    }
2647}
2648
2649impl IntoLua for usize {
2650    fn into_lua(self, lua: &Lua) -> Result<Value> {
2651        let v = i64::try_from(self)
2652            .map_err(|_| LuaError::runtime(format_args!("integer out of range")))?;
2653        v.into_lua(lua)
2654    }
2655}
2656
2657impl FromLua for usize {
2658    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2659        let v = i64::from_lua(value, lua)?;
2660        usize::try_from(v).map_err(|_| LuaError::runtime(format_args!("integer out of range")))
2661    }
2662}
2663
2664impl IntoLua for u64 {
2665    fn into_lua(self, lua: &Lua) -> Result<Value> {
2666        let v = i64::try_from(self)
2667            .map_err(|_| LuaError::runtime(format_args!("integer out of range")))?;
2668        v.into_lua(lua)
2669    }
2670}
2671
2672impl FromLua for u64 {
2673    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2674        let v = i64::from_lua(value, lua)?;
2675        u64::try_from(v).map_err(|_| LuaError::runtime(format_args!("integer out of range")))
2676    }
2677}
2678
2679impl IntoLua for u32 {
2680    fn into_lua(self, lua: &Lua) -> Result<Value> {
2681        u64::from(self).into_lua(lua)
2682    }
2683}
2684
2685impl FromLua for u32 {
2686    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2687        let v = u64::from_lua(value, lua)?;
2688        u32::try_from(v).map_err(|_| LuaError::runtime(format_args!("integer out of range")))
2689    }
2690}
2691
2692impl IntoLua for f64 {
2693    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2694        Ok(Value::Number(self))
2695    }
2696}
2697
2698impl FromLua for f64 {
2699    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2700        match value {
2701            Value::Integer(v) => Ok(v as f64),
2702            Value::Number(v) => Ok(v),
2703            other => Err(type_error_value(&other, "number")),
2704        }
2705    }
2706}
2707
2708impl IntoLua for &str {
2709    fn into_lua(self, lua: &Lua) -> Result<Value> {
2710        Ok(Value::String(lua.create_string(self.as_bytes())?))
2711    }
2712}
2713
2714impl IntoLua for String {
2715    fn into_lua(self, lua: &Lua) -> Result<Value> {
2716        Ok(Value::String(lua.create_string(self.into_bytes())?))
2717    }
2718}
2719
2720impl FromLua for String {
2721    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2722        match value {
2723            Value::String(s) => s.to_str(),
2724            other => Err(type_error_value(&other, "string")),
2725        }
2726    }
2727}
2728
2729impl IntoLua for &[u8] {
2730    fn into_lua(self, lua: &Lua) -> Result<Value> {
2731        Ok(Value::String(lua.create_string(self)?))
2732    }
2733}
2734
2735impl IntoLua for LuaString {
2736    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2737        Ok(Value::String(self))
2738    }
2739}
2740
2741impl IntoLua for &LuaString {
2742    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2743        Ok(Value::String(self.clone()))
2744    }
2745}
2746
2747impl FromLua for LuaString {
2748    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2749        match value {
2750            Value::String(v) => Ok(v),
2751            other => Err(type_error_value(&other, "string")),
2752        }
2753    }
2754}
2755
2756impl IntoLua for Table {
2757    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2758        Ok(Value::Table(self))
2759    }
2760}
2761
2762impl IntoLua for &Table {
2763    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2764        Ok(Value::Table(self.clone()))
2765    }
2766}
2767
2768impl FromLua for Table {
2769    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2770        match value {
2771            Value::Table(v) => Ok(v),
2772            other => Err(type_error_value(&other, "table")),
2773        }
2774    }
2775}
2776
2777impl IntoLua for Function {
2778    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2779        Ok(Value::Function(self))
2780    }
2781}
2782
2783impl IntoLua for &Function {
2784    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2785        Ok(Value::Function(self.clone()))
2786    }
2787}
2788
2789impl FromLua for Function {
2790    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2791        match value {
2792            Value::Function(v) => Ok(v),
2793            other => Err(type_error_value(&other, "function")),
2794        }
2795    }
2796}
2797
2798impl IntoLua for AnyUserData {
2799    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2800        Ok(Value::UserData(self))
2801    }
2802}
2803
2804impl IntoLua for &AnyUserData {
2805    fn into_lua(self, _lua: &Lua) -> Result<Value> {
2806        Ok(Value::UserData(self.clone()))
2807    }
2808}
2809
2810impl FromLua for AnyUserData {
2811    fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
2812        match value {
2813            Value::UserData(v) => Ok(v),
2814            other => Err(type_error_value(&other, "userdata")),
2815        }
2816    }
2817}
2818
2819impl<T> IntoLua for T
2820where
2821    T: UserData,
2822{
2823    fn into_lua(self, lua: &Lua) -> Result<Value> {
2824        Ok(Value::UserData(lua.create_userdata(self)?))
2825    }
2826}
2827
2828impl<T> IntoLua for Option<T>
2829where
2830    T: IntoLua,
2831{
2832    fn into_lua(self, lua: &Lua) -> Result<Value> {
2833        match self {
2834            Some(value) => value.into_lua(lua),
2835            None => Ok(Value::Nil),
2836        }
2837    }
2838}
2839
2840impl<T> FromLua for Option<T>
2841where
2842    T: FromLua,
2843{
2844    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2845        match value {
2846            Value::Nil => Ok(None),
2847            other => T::from_lua(other, lua).map(Some),
2848        }
2849    }
2850}
2851
2852impl<T> IntoLua for Vec<T>
2853where
2854    T: IntoLua,
2855{
2856    fn into_lua(self, lua: &Lua) -> Result<Value> {
2857        let table = lua.create_table()?;
2858        for (idx, value) in self.into_iter().enumerate() {
2859            table.set((idx + 1) as i64, value)?;
2860        }
2861        Ok(Value::Table(table))
2862    }
2863}
2864
2865impl<T> FromLua for Vec<T>
2866where
2867    T: FromLua,
2868{
2869    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2870        let table = Table::from_lua(value, lua)?;
2871        let raw = table.raw_table()?;
2872        let len = raw.getn();
2873        let mut out = Vec::with_capacity(len as usize);
2874        for idx in 1..=len {
2875            let value = Value::from_raw(lua, raw.get_int(idx as i64))?;
2876            out.push(T::from_lua(value, lua)?);
2877        }
2878        Ok(out)
2879    }
2880}
2881
2882impl<K, V> IntoLua for HashMap<K, V>
2883where
2884    K: IntoLua,
2885    V: IntoLua,
2886{
2887    fn into_lua(self, lua: &Lua) -> Result<Value> {
2888        let table = lua.create_table()?;
2889        for (key, value) in self {
2890            table.set(key, value)?;
2891        }
2892        Ok(Value::Table(table))
2893    }
2894}
2895
2896impl<K, V> FromLua for HashMap<K, V>
2897where
2898    K: FromLua + Eq + Hash,
2899    V: FromLua,
2900{
2901    fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
2902        let table = Table::from_lua(value, lua)?;
2903        let raw = table.raw_table()?;
2904        let mut out = HashMap::new();
2905        let mut result = Ok(());
2906        raw.for_each_entry(|key, value| {
2907            if result.is_err() {
2908                return;
2909            }
2910            result = (|| {
2911                let key = Value::from_raw(lua, *key)?;
2912                let value = Value::from_raw(lua, *value)?;
2913                out.insert(K::from_lua(key, lua)?, V::from_lua(value, lua)?);
2914                Ok(())
2915            })();
2916        });
2917        result?;
2918        Ok(out)
2919    }
2920}
2921
2922impl<T> IntoLuaMulti for Variadic<T>
2923where
2924    T: IntoLua,
2925{
2926    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
2927        self.into_iter().map(|value| value.into_lua(lua)).collect()
2928    }
2929}
2930
2931impl<T> FromLuaMulti for Variadic<T>
2932where
2933    T: FromLua,
2934{
2935    const NRESULTS: i32 = -1;
2936
2937    fn from_lua_multi(values: Vec<Value>, lua: &Lua) -> Result<Self> {
2938        values
2939            .into_iter()
2940            .map(|value| T::from_lua(value, lua))
2941            .collect()
2942    }
2943}
2944
2945impl IntoLuaMulti for () {
2946    fn into_lua_multi(self, _lua: &Lua) -> Result<Vec<Value>> {
2947        Ok(Vec::new())
2948    }
2949}
2950
2951impl<T> IntoLuaMulti for T
2952where
2953    T: IntoLua,
2954{
2955    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
2956        Ok(vec![self.into_lua(lua)?])
2957    }
2958}
2959
2960impl<A, B> IntoLuaMulti for (A, B)
2961where
2962    A: IntoLua,
2963    B: IntoLua,
2964{
2965    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
2966        Ok(vec![self.0.into_lua(lua)?, self.1.into_lua(lua)?])
2967    }
2968}
2969
2970impl<A, T> IntoLuaMulti for (A, Variadic<T>)
2971where
2972    A: IntoLua,
2973    T: IntoLua,
2974{
2975    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
2976        let mut values = vec![self.0.into_lua(lua)?];
2977        values.extend(self.1.into_lua_multi(lua)?);
2978        Ok(values)
2979    }
2980}
2981
2982impl<A, B, C> IntoLuaMulti for (A, B, C)
2983where
2984    A: IntoLua,
2985    B: IntoLua,
2986    C: IntoLua,
2987{
2988    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
2989        Ok(vec![
2990            self.0.into_lua(lua)?,
2991            self.1.into_lua(lua)?,
2992            self.2.into_lua(lua)?,
2993        ])
2994    }
2995}
2996
2997impl<A, B, T> IntoLuaMulti for (A, B, Variadic<T>)
2998where
2999    A: IntoLua,
3000    B: IntoLua,
3001    T: IntoLua,
3002{
3003    fn into_lua_multi(self, lua: &Lua) -> Result<Vec<Value>> {
3004        let mut values = vec![self.0.into_lua(lua)?, self.1.into_lua(lua)?];
3005        values.extend(self.2.into_lua_multi(lua)?);
3006        Ok(values)
3007    }
3008}
3009
3010impl FromLuaMulti for () {
3011    const NRESULTS: i32 = 0;
3012
3013    fn from_lua_multi(_values: Vec<Value>, _lua: &Lua) -> Result<Self> {
3014        Ok(())
3015    }
3016}
3017
3018impl<T> FromLuaMulti for T
3019where
3020    T: FromLua,
3021{
3022    const NRESULTS: i32 = 1;
3023
3024    fn from_lua_multi(mut values: Vec<Value>, lua: &Lua) -> Result<Self> {
3025        let value = if values.is_empty() {
3026            Value::Nil
3027        } else {
3028            values.remove(0)
3029        };
3030        T::from_lua(value, lua)
3031    }
3032}
3033
3034impl<A, B> FromLuaMulti for (A, B)
3035where
3036    A: FromLua,
3037    B: FromLua,
3038{
3039    const NRESULTS: i32 = 2;
3040
3041    fn from_lua_multi(mut values: Vec<Value>, lua: &Lua) -> Result<Self> {
3042        let first = if values.is_empty() {
3043            Value::Nil
3044        } else {
3045            values.remove(0)
3046        };
3047        let second = if values.is_empty() {
3048            Value::Nil
3049        } else {
3050            values.remove(0)
3051        };
3052        Ok((A::from_lua(first, lua)?, B::from_lua(second, lua)?))
3053    }
3054}
3055
3056impl<A, T> FromLuaMulti for (A, Variadic<T>)
3057where
3058    A: FromLua,
3059    T: FromLua,
3060{
3061    const NRESULTS: i32 = -1;
3062
3063    fn from_lua_multi(mut values: Vec<Value>, lua: &Lua) -> Result<Self> {
3064        let first = if values.is_empty() {
3065            Value::Nil
3066        } else {
3067            values.remove(0)
3068        };
3069        Ok((
3070            A::from_lua(first, lua)?,
3071            Variadic::from_lua_multi(values, lua)?,
3072        ))
3073    }
3074}
3075
3076impl<A, B, C> FromLuaMulti for (A, B, C)
3077where
3078    A: FromLua,
3079    B: FromLua,
3080    C: FromLua,
3081{
3082    const NRESULTS: i32 = 3;
3083
3084    fn from_lua_multi(mut values: Vec<Value>, lua: &Lua) -> Result<Self> {
3085        let first = if values.is_empty() {
3086            Value::Nil
3087        } else {
3088            values.remove(0)
3089        };
3090        let second = if values.is_empty() {
3091            Value::Nil
3092        } else {
3093            values.remove(0)
3094        };
3095        let third = if values.is_empty() {
3096            Value::Nil
3097        } else {
3098            values.remove(0)
3099        };
3100        Ok((
3101            A::from_lua(first, lua)?,
3102            B::from_lua(second, lua)?,
3103            C::from_lua(third, lua)?,
3104        ))
3105    }
3106}
3107
3108impl<A, B, T> FromLuaMulti for (A, B, Variadic<T>)
3109where
3110    A: FromLua,
3111    B: FromLua,
3112    T: FromLua,
3113{
3114    const NRESULTS: i32 = -1;
3115
3116    fn from_lua_multi(mut values: Vec<Value>, lua: &Lua) -> Result<Self> {
3117        let first = if values.is_empty() {
3118            Value::Nil
3119        } else {
3120            values.remove(0)
3121        };
3122        let second = if values.is_empty() {
3123            Value::Nil
3124        } else {
3125            values.remove(0)
3126        };
3127        Ok((
3128            A::from_lua(first, lua)?,
3129            B::from_lua(second, lua)?,
3130            Variadic::from_lua_multi(values, lua)?,
3131        ))
3132    }
3133}
3134
3135fn rust_callback_trampoline(state: &mut LuaState) -> Result<usize> {
3136    let func_idx = state.current_call_info().func;
3137    let callback = match state.get_at(func_idx) {
3138        RawLuaValue::Function(RawLuaClosure::C(closure)) => {
3139            let Some(RawLuaValue::UserData(userdata)) = closure.upvalues.first() else {
3140                return Err(LuaError::runtime(format_args!(
3141                    "missing Rust callback payload"
3142                )));
3143            };
3144            let host = userdata
3145                .host_value()
3146                .ok_or_else(|| LuaError::runtime(format_args!("missing Rust callback payload")))?;
3147            host.downcast::<RustCallbackCell>().map_err(|_| {
3148                LuaError::runtime(format_args!("Rust callback payload type mismatch"))
3149            })?
3150        }
3151        _ => {
3152            return Err(LuaError::runtime(format_args!(
3153                "Rust callback trampoline called without C closure"
3154            )));
3155        }
3156    };
3157    (callback.function)(state)
3158}
3159
3160fn with_heap_guard<R>(state: &LuaState, f: impl FnOnce() -> R) -> R {
3161    let _heap_guard = heap_guard(state);
3162    f()
3163}
3164
3165fn heap_guard(state: &LuaState) -> lua_gc::HeapGuard {
3166    let global = state.global();
3167    lua_gc::HeapGuard::push(&global.heap)
3168}
3169
3170fn callback_args(state: &mut LuaState, lua: &Lua) -> Result<Vec<Value>> {
3171    let func_idx = state.current_call_info().func;
3172    let nargs = state.top_idx().0.saturating_sub(func_idx.0 + 1);
3173    let mut args = Vec::with_capacity(nargs as usize);
3174    for i in 0..nargs {
3175        let raw = state.get_at(func_idx + 1 + i as i32);
3176        args.push(Value::from_raw_in_state(lua, state, raw)?);
3177    }
3178    Ok(args)
3179}
3180
3181fn callback_userdata_args(state: &mut LuaState, lua: &Lua) -> Result<(AnyUserData, Vec<Value>)> {
3182    let mut args = callback_args(state, lua)?;
3183    if args.is_empty() {
3184        return Err(LuaError::runtime(format_args!(
3185            "userdata method missing self argument"
3186        )));
3187    }
3188    let userdata = AnyUserData::from_lua(args.remove(0), lua)?;
3189    Ok((userdata, args))
3190}
3191
3192fn push_callback_returns(state: &mut LuaState, lua: &Lua, returns: Vec<Value>) -> Result<usize> {
3193    let mut count = 0usize;
3194    for value in returns {
3195        let raw = value.to_raw_for_lua(lua, state)?;
3196        state.push(raw);
3197        count += 1;
3198    }
3199    Ok(count)
3200}
3201
3202fn stale_handle_error() -> LuaError {
3203    LuaError::runtime(format_args!("stale Lua handle"))
3204}
3205
3206fn type_error_raw(value: &RawLuaValue, expected: &str) -> LuaError {
3207    LuaError::runtime(format_args!(
3208        "{} expected, got {}",
3209        expected,
3210        value.type_name()
3211    ))
3212}
3213
3214fn type_error_value(value: &Value, expected: &str) -> LuaError {
3215    let got = match value {
3216        Value::Nil => "nil",
3217        Value::Boolean(_) => "boolean",
3218        Value::Integer(_) | Value::Number(_) => "number",
3219        Value::String(_) => "string",
3220        Value::Table(_) => "table",
3221        Value::Function(_) => "function",
3222        Value::UserData(_) | Value::LightUserData(_) => "userdata",
3223        Value::Thread(_) => "thread",
3224    };
3225    LuaError::runtime(format_args!("{} expected, got {}", expected, got))
3226}
3227
3228/// A Lua state with parser and standard libraries installed.
3229pub struct LuaRuntime {
3230    state: LuaState,
3231}
3232
3233impl LuaRuntime {
3234    /// Create a Lua runtime with parser and standard libraries installed.
3235    ///
3236    /// This installs no explicit host hooks. For a strict sandbox, construct
3237    /// with [`LuaRuntime::with_hooks`] and audit the native compatibility
3238    /// fallbacks in `lua-stdlib`.
3239    pub fn new() -> Result<Self> {
3240        Self::with_hooks(HostHooks::default())
3241    }
3242
3243    /// Create a Lua runtime with the supplied host capabilities.
3244    pub fn with_hooks(hooks: HostHooks) -> Result<Self> {
3245        let mut state = new_state().ok_or(LuaError::Memory)?;
3246        install_parser_hook(&mut state);
3247        hooks.install(&mut state);
3248        open_libs(&mut state)?;
3249        Ok(Self { state })
3250    }
3251
3252    pub fn state(&self) -> &LuaState {
3253        &self.state
3254    }
3255
3256    pub fn state_mut(&mut self) -> &mut LuaState {
3257        &mut self.state
3258    }
3259
3260    pub fn into_state(self) -> LuaState {
3261        self.state
3262    }
3263
3264    pub fn into_lua(self) -> Lua {
3265        Lua::from_initialized_state(self.state, LuaVersion::default())
3266    }
3267
3268    /// Load and execute a Lua source chunk.
3269    pub fn exec(&mut self, source: &[u8], name: &[u8]) -> Result<()> {
3270        exec_state(&mut self.state, source, name)
3271    }
3272
3273    /// Apply sandbox limits to this runtime — the lower-level equivalent of
3274    /// [`Lua::install_sandbox`]. Strips the configured globals and installs the
3275    /// runtime-wide instruction/memory budget (enforced on every thread,
3276    /// uncatchable). Use [`sandbox_tripped`](Self::sandbox_tripped) after a run
3277    /// to learn which limit, if any, stopped it, and
3278    /// [`sandbox_reset`](Self::sandbox_reset) to refill the budget before the
3279    /// next run.
3280    pub fn install_sandbox(&mut self, config: SandboxConfig) -> Result<()> {
3281        apply_sandbox_config(&mut self.state, &config)
3282    }
3283
3284    /// Which sandbox limit (if any) aborted the most recent run.
3285    pub fn sandbox_tripped(&self) -> Option<TripReason> {
3286        trip_reason_from_code(self.state.sandbox_tripped_code())
3287    }
3288
3289    /// Refill the instruction budget to its configured limit and clear the trip
3290    /// flag, so the same runtime can run another chunk.
3291    pub fn sandbox_reset(&self) {
3292        self.state.sandbox_reset();
3293    }
3294}
3295
3296fn exec_state(state: &mut LuaState, source: &[u8], name: &[u8]) -> Result<()> {
3297    let status = load_buffer(state, source, name)?;
3298    if status != 0 {
3299        let err = state.pop();
3300        return Err(LuaError::from_value(err));
3301    }
3302    lua_vm::api::pcall_k(state, 0, 0, 0, 0, None)?;
3303    Ok(())
3304}
3305
3306pub fn install_parser_hook(state: &mut LuaState) {
3307    state.global_mut().parser_hook = Some(parser_hook);
3308}
3309
3310fn parser_hook(
3311    state: &mut LuaState,
3312    source: &[u8],
3313    name: &[u8],
3314    firstchar: i32,
3315) -> Result<GcRef<LuaLClosure>> {
3316    let _heap_guard = heap_guard(state);
3317    let proto = lua_parse::parse(
3318        state,
3319        lua_parse::DynData::default(),
3320        source,
3321        name,
3322        firstchar,
3323    )?;
3324    let nupvals = proto.upvalues.len();
3325    let mut upvals = Vec::with_capacity(nupvals);
3326    for _ in 0..nupvals {
3327        upvals.push(std::cell::Cell::new(GcRef::new(UpVal::closed(
3328            RawLuaValue::Nil,
3329        ))));
3330    }
3331    Ok(GcRef::new(LuaLClosure {
3332        proto: GcRef::new(*proto),
3333        upvals,
3334    }))
3335}
3336
3337// ────────────────────────────── Sandboxing ──────────────────────────────
3338//
3339// Bounded, untrusted execution for embedders. Three independent controls:
3340//
3341//   1. Instruction budget   — abort after N executed VM instructions.
3342//   2. Memory ceiling        — abort once GC-tracked bytes exceed a limit.
3343//   3. Capability stripping   — remove dangerous globals (`os.execute`, `io`,
3344//                               `load`, `require`, …) from the environment.
3345//
3346// (1) and (2) are enforced by a runtime-wide budget stored in the shared
3347// `GlobalState` (`SandboxLimits`). `install_sandbox_limits` arms the VM
3348// count-hook mask on every thread — including coroutines, via `preinit_thread`
3349// — and the VM charges the shared budget once per `check_interval` instructions
3350// directly in `trace_exec`. When a limit is crossed the VM returns a `LuaError`
3351// that unwinds the dispatch loop and surfaces to the embedder as an ordinary
3352// runtime error from `exec`/`eval`/`call`. Because the budget is shared and
3353// every thread is armed, code inside `coroutine.wrap(...)` is metered too.
3354//
3355// Cost: when no sandbox is active the count mask is unset, `trap` stays false,
3356// and the dispatch loop is byte-for-byte unchanged — zero overhead. Inside a
3357// sandbox, the VM pays the standard count-hook cost (a per-instruction trap
3358// dispatch); `check_interval` trades enforcement precision, not throughput.
3359//
3360// Enforcement granularity is `check_interval` instructions: a budget trips
3361// within `check_interval` of the true limit, and memory is sampled at the same
3362// cadence — so a single allocation between two samples (e.g. `string.rep` with
3363// a huge count) can momentarily exceed the ceiling before the next check sees
3364// it. A hard, per-allocation memory cap would require enforcement inside
3365// `Heap::allocate`; that is the natural next step.
3366
3367/// Why a sandboxed run was aborted.
3368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3369pub enum TripReason {
3370    /// The instruction budget reached zero.
3371    Instructions,
3372    /// GC-tracked memory exceeded the configured ceiling.
3373    Memory,
3374}
3375
3376/// A live handle to a sandbox's budget. The budget itself lives in the
3377/// runtime's shared `GlobalState`, so it spans every thread (main and
3378/// coroutines); this handle just reads and resets it through the `Lua`.
3379#[derive(Clone)]
3380pub struct Sandbox {
3381    lua: Lua,
3382}
3383
3384impl Sandbox {
3385    /// Instructions left before the budget trips, or `None` if no instruction
3386    /// limit was configured.
3387    pub fn instructions_remaining(&self) -> Option<u64> {
3388        self.lua.with_state(|state| {
3389            if state.sandbox_instr_limited() {
3390                Some(state.sandbox_instr_remaining())
3391            } else {
3392                None
3393            }
3394        })
3395    }
3396
3397    /// Instructions consumed so far (rounded to the check interval), or `None`
3398    /// if no instruction limit was configured.
3399    pub fn instructions_used(&self) -> Option<u64> {
3400        self.lua.with_state(|state| {
3401            if state.sandbox_instr_limited() {
3402                Some(state.sandbox_instr_limit() - state.sandbox_instr_remaining())
3403            } else {
3404                None
3405            }
3406        })
3407    }
3408
3409    /// Why the last run aborted, if it was the sandbox that stopped it.
3410    pub fn tripped(&self) -> Option<TripReason> {
3411        self.lua
3412            .with_state(|state| trip_reason_from_code(state.sandbox_tripped_code()))
3413    }
3414
3415    /// Refill the instruction budget to its configured limit and clear the
3416    /// tripped flag. Call before re-running a chunk in the same `Lua` state.
3417    pub fn reset(&self) {
3418        self.lua.with_state(|state| state.sandbox_reset());
3419    }
3420}
3421
3422/// Configuration for [`Lua::sandboxed`].
3423#[derive(Debug, Clone)]
3424pub struct SandboxConfig {
3425    /// Maximum VM instructions a run may execute. `None` = unlimited.
3426    pub instruction_limit: Option<u64>,
3427    /// Maximum GC-tracked bytes. `None` = unlimited.
3428    pub memory_limit_bytes: Option<usize>,
3429    /// Instructions between budget/memory checks. Lower = tighter enforcement,
3430    /// higher hook overhead. Clamped to at least 1.
3431    pub check_interval: u32,
3432    /// Global paths to delete before running, e.g. `b"os.execute"` or `b"io"`.
3433    /// A `.`-separated path nils a field of a sub-table; a bare name nils a
3434    /// top-level global.
3435    pub remove_globals: Vec<Vec<u8>>,
3436}
3437
3438impl SandboxConfig {
3439    /// A strict default: 10M instructions, 64 MiB, and removal of the
3440    /// code-loading and host-access globals. Tune fields as needed.
3441    pub fn strict() -> Self {
3442        Self {
3443            instruction_limit: Some(10_000_000),
3444            memory_limit_bytes: Some(64 * 1024 * 1024),
3445            check_interval: 1000,
3446            remove_globals: lua_stdlib::sandbox::STRICT_REMOVED_GLOBALS
3447                .iter()
3448                .map(|s| s.to_vec())
3449                .collect(),
3450        }
3451    }
3452}
3453
3454impl Default for SandboxConfig {
3455    fn default() -> Self {
3456        Self::strict()
3457    }
3458}
3459
3460fn strip_globals(state: &mut LuaState, names: &[Vec<u8>]) -> Result<()> {
3461    let refs: Vec<&[u8]> = names.iter().map(|n| n.as_slice()).collect();
3462    lua_stdlib::sandbox::strip_globals(state, &refs)
3463}
3464
3465/// Apply a [`SandboxConfig`] to a raw state: strip the configured globals and,
3466/// if any runtime limit is set, install the runtime-wide budget. Shared by
3467/// [`Lua::install_sandbox`] and [`LuaRuntime::install_sandbox`].
3468fn apply_sandbox_config(state: &mut LuaState, config: &SandboxConfig) -> Result<()> {
3469    strip_globals(state, &config.remove_globals)?;
3470    if config.instruction_limit.is_some() || config.memory_limit_bytes.is_some() {
3471        let interval = config.check_interval.max(1) as i32;
3472        state.install_sandbox_limits(interval, config.instruction_limit, config.memory_limit_bytes);
3473    }
3474    Ok(())
3475}
3476
3477/// Map the raw sandbox trip code held in `GlobalState` to a [`TripReason`].
3478fn trip_reason_from_code(code: u8) -> Option<TripReason> {
3479    match code {
3480        lua_vm::state::SANDBOX_TRIP_INSTRUCTIONS => Some(TripReason::Instructions),
3481        lua_vm::state::SANDBOX_TRIP_MEMORY => Some(TripReason::Memory),
3482        _ => None,
3483    }
3484}
3485
3486impl Lua {
3487    /// Create a Lua runtime with no host capabilities (no file, process, or
3488    /// dynamic-library hooks), the configured globals stripped, and an
3489    /// instruction/memory budget installed. Returns the runtime and a
3490    /// [`Sandbox`] handle for inspecting and resetting the budget.
3491    pub fn sandboxed(config: SandboxConfig) -> Result<(Self, Sandbox)> {
3492        let lua = Self::with_hooks(HostHooks::default())?;
3493        let sandbox = lua.install_sandbox(config)?;
3494        Ok((lua, sandbox))
3495    }
3496
3497    /// Apply sandbox limits to this runtime: strip the configured globals and,
3498    /// if any runtime limit is set, install the runtime-wide budget. The budget
3499    /// lives in the shared `GlobalState` and is enforced natively in the VM on
3500    /// every thread, so code inside coroutines is metered too. Use this when
3501    /// you want to grant *some* host capabilities (build the `Lua` with selected
3502    /// [`HostHooks`]) but still bound execution.
3503    pub fn install_sandbox(&self, config: SandboxConfig) -> Result<Sandbox> {
3504        self.with_state(|state| apply_sandbox_config(state, &config))?;
3505        Ok(Sandbox { lua: self.clone() })
3506    }
3507}
3508
3509#[cfg(test)]
3510mod tests {
3511    use super::*;
3512    use std::cell::Cell;
3513
3514    fn external_root_count(lua: &Lua) -> usize {
3515        lua.with_state(|state| state.global().external_roots.len())
3516    }
3517
3518    struct Counter {
3519        value: i64,
3520    }
3521
3522    impl UserData for Counter {
3523        fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
3524            methods.add_method("get", |_lua, this, ()| Ok(this.value));
3525            methods.add_method_mut("inc", |_lua, this, delta: i64| {
3526                this.value += delta;
3527                Ok(this.value)
3528            });
3529        }
3530    }
3531
3532    struct PropertyBag {
3533        value: i64,
3534    }
3535
3536    impl UserData for PropertyBag {
3537        fn add_meta_methods<M: UserDataMethods<Self>>(methods: &mut M) {
3538            methods.add_meta_method(MetaMethod::Index, |_lua, this, key: String| {
3539                if key == "value" {
3540                    Ok(Value::Integer(this.value))
3541                } else {
3542                    Ok(Value::Nil)
3543                }
3544            });
3545            methods.add_meta_method_mut(
3546                MetaMethod::NewIndex,
3547                |_lua, this, (key, value): (String, i64)| {
3548                    if key != "value" {
3549                        return Err(LuaError::runtime(format_args!("unknown property")));
3550                    }
3551                    this.value = value;
3552                    Ok(())
3553                },
3554            );
3555        }
3556    }
3557
3558    #[test]
3559    fn default_lua_is_v54_and_reports_version() {
3560        let lua = Lua::new();
3561        assert_eq!(lua.version(), LuaVersion::V54);
3562        let v: String = lua.globals().get("_VERSION").unwrap();
3563        assert_eq!(v, "Lua 5.4");
3564    }
3565
3566    #[test]
3567    fn new_versioned_threads_version_to_version_global() {
3568        let lua = Lua::new_versioned(LuaVersion::V53);
3569        assert_eq!(lua.version(), LuaVersion::V53);
3570        let v: String = lua.globals().get("_VERSION").unwrap();
3571        assert_eq!(v, "Lua 5.3");
3572        let from_lua: String = lua.load("return _VERSION").eval().unwrap();
3573        assert_eq!(from_lua, "Lua 5.3");
3574    }
3575
3576    #[test]
3577    fn rooted_table_clone_and_drop_manage_root_slots() {
3578        let lua = Lua::new();
3579        assert_eq!(external_root_count(&lua), 0);
3580
3581        let table = lua.create_table().expect("table should allocate");
3582        assert_eq!(external_root_count(&lua), 1);
3583
3584        let cloned = table.clone();
3585        assert_eq!(external_root_count(&lua), 2);
3586
3587        drop(table);
3588        assert_eq!(external_root_count(&lua), 1);
3589
3590        cloned.set("answer", 42_i64).expect("set should succeed");
3591        lua.gc_collect();
3592        assert_eq!(
3593            cloned.get::<_, i64>("answer").expect("get should succeed"),
3594            42
3595        );
3596
3597        drop(cloned);
3598        assert_eq!(external_root_count(&lua), 0);
3599    }
3600
3601    #[test]
3602    fn table_values_survive_forced_collection_between_operations() {
3603        let lua = Lua::new();
3604        let table = lua.create_table().expect("table should allocate");
3605
3606        lua.gc_collect();
3607        table.set("k", "v").expect("set should succeed");
3608        table.set(1_i64, "array").expect("array set should succeed");
3609        lua.gc_collect();
3610
3611        let value: String = table.get("k").expect("get should succeed");
3612        assert_eq!(value, "v");
3613        assert_eq!(table.len().expect("len should succeed"), 1);
3614    }
3615
3616    #[test]
3617    fn chunk_exec_eval_and_function_call_use_rooted_handles() {
3618        let lua = Lua::new();
3619        lua.load("function add(a, b) return a + b end")
3620            .set_name("test")
3621            .exec()
3622            .expect("chunk should execute");
3623
3624        let globals = lua.globals();
3625        let add: Function = globals.get("add").expect("function should exist");
3626        let result: i64 = add.call((20_i64, 22_i64)).expect("call should work");
3627        assert_eq!(result, 42);
3628
3629        let eval_result: i64 = lua
3630            .load("return add(1, 2)")
3631            .eval()
3632            .expect("eval should work");
3633        assert_eq!(eval_result, 3);
3634    }
3635
3636    #[test]
3637    fn rust_callback_captures_state_and_reenters_lua() {
3638        let lua = Lua::new();
3639        lua.load("function twice(v) return v * 2 end")
3640            .exec()
3641            .expect("chunk should execute");
3642
3643        let globals = lua.globals();
3644        let twice: Function = globals.get("twice").expect("function should exist");
3645        let calls = Rc::new(Cell::new(0));
3646        let calls_for_callback = calls.clone();
3647
3648        let callback = lua
3649            .create_function(move |_lua, value: i64| {
3650                calls_for_callback.set(calls_for_callback.get() + 1);
3651                let doubled: i64 = twice.call(value)?;
3652                Ok(doubled + 1)
3653            })
3654            .expect("callback should create");
3655        globals
3656            .set("from_rust", callback)
3657            .expect("callback should register");
3658
3659        let result: i64 = lua
3660            .load("return from_rust(20)")
3661            .eval()
3662            .expect("callback should run");
3663        assert_eq!(result, 41);
3664        assert_eq!(calls.get(), 1);
3665    }
3666
3667    #[test]
3668    fn rust_callback_accepts_and_returns_collectable_values() {
3669        let lua = Lua::new();
3670        let globals = lua.globals();
3671        let callback = lua
3672            .create_function(|lua, name: String| {
3673                let table = lua.create_table()?;
3674                table.set("name", name)?;
3675                Ok(table)
3676            })
3677            .expect("callback should create");
3678        globals
3679            .set("make_record", callback)
3680            .expect("callback should register");
3681
3682        let result: String = lua
3683            .load("return make_record('lua-rs').name")
3684            .eval()
3685            .expect("callback should return table");
3686        assert_eq!(result, "lua-rs");
3687    }
3688
3689    #[test]
3690    fn rust_callback_mut_tracks_state() {
3691        let lua = Lua::new();
3692        let globals = lua.globals();
3693        let mut next = 0_i64;
3694        let callback = lua
3695            .create_function_mut(move |_lua, delta: i64| {
3696                next += delta;
3697                Ok(next)
3698            })
3699            .expect("callback should create");
3700        globals
3701            .set("next", callback)
3702            .expect("callback should register");
3703
3704        let result: (i64, i64) = lua
3705            .load("return next(2), next(5)")
3706            .eval()
3707            .expect("callback should run");
3708        assert_eq!(result, (2, 7));
3709    }
3710
3711    #[test]
3712    fn dropped_rust_callback_releases_captured_handles_after_gc() {
3713        let lua = Lua::new();
3714        let table = lua.create_table().expect("table should allocate");
3715        table.set("value", 42_i64).expect("set should succeed");
3716        assert_eq!(external_root_count(&lua), 1);
3717
3718        let callback = {
3719            let captured = table.clone();
3720            lua.create_function(move |_lua, ()| captured.get::<_, i64>("value"))
3721                .expect("callback should create")
3722        };
3723        assert_eq!(external_root_count(&lua), 3);
3724
3725        drop(callback);
3726        lua.gc_collect();
3727        assert_eq!(external_root_count(&lua), 1);
3728        assert_eq!(table.get::<_, i64>("value").expect("table should live"), 42);
3729    }
3730
3731    #[test]
3732    fn metatable_is_built_once_per_type() {
3733        use std::sync::atomic::{AtomicUsize, Ordering};
3734        static BUILDS: AtomicUsize = AtomicUsize::new(0);
3735
3736        struct Widget {
3737            n: i64,
3738        }
3739        impl UserData for Widget {
3740            fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
3741                BUILDS.fetch_add(1, Ordering::SeqCst);
3742                methods.add_method("n", |_lua, this, ()| Ok(this.n));
3743            }
3744        }
3745
3746        let lua = Lua::new();
3747        let a = lua.create_userdata(Widget { n: 1 }).expect("first");
3748        let b = lua.create_userdata(Widget { n: 2 }).expect("second");
3749        let c = lua.create_userdata(Widget { n: 3 }).expect("third");
3750
3751        // Built exactly once despite three values of the same type.
3752        assert_eq!(BUILDS.load(Ordering::SeqCst), 1);
3753
3754        // Each value still carries its own data and dispatches correctly.
3755        let globals = lua.globals();
3756        globals.set("a", &a).unwrap();
3757        globals.set("b", &b).unwrap();
3758        globals.set("c", &c).unwrap();
3759        let sum: i64 = lua.load("return a:n() + b:n() + c:n()").eval().unwrap();
3760        assert_eq!(sum, 6);
3761    }
3762
3763    /// Reproducer for the callback-to-`Lua` reference cycle:
3764    /// `create_userdata_method` captures a strong `Lua` (`Rc<LuaInner>`) into each
3765    /// callback closure, the closure lives in a heap GC object owned by `LuaState`,
3766    /// and `LuaState` is owned by `LuaInner` — so dropping every external `Lua`
3767    /// handle still leaves the closures holding a strong `Rc<LuaInner>` to the
3768    /// state that owns them. Per-type metatable caching makes this permanent for
3769    /// any type a userdata is ever created for.
3770    ///
3771    /// This test holds a `Weak<LuaInner>`, drops every external `Lua`, and asserts
3772    /// the inner has actually been freed. It fails today and is what the
3773    /// `Weak`-capture fix in the callback constructors is meant to make pass.
3774    #[test]
3775    fn lua_state_frees_after_userdata_with_methods_is_dropped() {
3776        use std::rc::Rc;
3777
3778        let weak_inner = {
3779            let lua = Lua::new();
3780            let weak = Rc::downgrade(&lua.inner);
3781            // Create + drop a userdata of a type that registers methods. This
3782            // primes the per-type metatable cache and installs method closures
3783            // that capture `Lua` strongly.
3784            let _ = lua
3785                .create_userdata(Counter { value: 1 })
3786                .expect("userdata should create");
3787            weak
3788        };
3789
3790        assert!(
3791            weak_inner.upgrade().is_none(),
3792            "LuaInner is still alive after every external Lua handle dropped: \
3793             internal callback closures hold a strong Rc<LuaInner>, leaking the state"
3794        );
3795    }
3796
3797    /// Same cycle issue as above, on the `create_function` path: the Rust
3798    /// callback closure used to capture a strong `Lua`, so a function that
3799    /// outlived all external handles would keep the state pinned.
3800    #[test]
3801    fn lua_state_frees_after_create_function_handle_drops() {
3802        use std::rc::Rc;
3803
3804        let weak_inner = {
3805            let lua = Lua::new();
3806            let weak = Rc::downgrade(&lua.inner);
3807            let _f = lua
3808                .create_function(|_, ()| Ok(()))
3809                .expect("create_function should succeed");
3810            weak
3811        };
3812
3813        assert!(
3814            weak_inner.upgrade().is_none(),
3815            "LuaInner is still alive after the only Lua handle dropped: \
3816             the create_function callback held a strong Rc<LuaInner>"
3817        );
3818    }
3819
3820    /// Field-bearing types take the composed `__index` path in `build_metatable`,
3821    /// where the composing closure is itself passed to `create_function` and
3822    /// captures the field-getter table, method table, and optional raw
3823    /// `__index` function. Each of those is a `Table` or `Function` whose
3824    /// `RootedValue` holds a strong `Rc<LuaInner>`. Even with the outer
3825    /// `Weak` fix, that user closure still leaks the state.
3826    #[test]
3827    fn lua_state_frees_after_userdata_with_fields_drops() {
3828        use std::rc::Rc;
3829
3830        struct Point {
3831            x: f64,
3832        }
3833        impl UserData for Point {
3834            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
3835                m.add_field_method_get("x", |_, this| Ok(this.x));
3836                m.add_field_method_set("x", |_, this, v: f64| {
3837                    this.x = v;
3838                    Ok(())
3839                });
3840            }
3841        }
3842
3843        let weak_inner = {
3844            let lua = Lua::new();
3845            let weak = Rc::downgrade(&lua.inner);
3846            let _ = lua
3847                .create_userdata(Point { x: 1.0 })
3848                .expect("userdata should create");
3849            weak
3850        };
3851
3852        assert!(
3853            weak_inner.upgrade().is_none(),
3854            "LuaInner leaked via the composed __index/__newindex closures: \
3855             they capture Table/Function values whose RootedValue holds a \
3856             strong Rc<LuaInner>"
3857        );
3858    }
3859
3860    /// Maximal mixed shape: field getter + field setter + regular method +
3861    /// raw `__index` + raw `__newindex` all on one type. Exercises every
3862    /// branch of the composed dispatch and every permanently rooted handle.
3863    /// If a future change reintroduces a captured wrapper anywhere in the
3864    /// composition path, this is the test most likely to catch it.
3865    #[test]
3866    fn lua_state_frees_with_fields_methods_and_raw_meta() {
3867        use std::rc::Rc;
3868
3869        struct Mixed {
3870            x: f64,
3871            log: Vec<String>,
3872        }
3873        impl UserData for Mixed {
3874            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
3875                m.add_field_method_get("x", |_, this| Ok(this.x));
3876                m.add_field_method_set("x", |_, this, v: f64| {
3877                    this.x = v;
3878                    Ok(())
3879                });
3880                m.add_method("log_len", |_, this, ()| Ok(this.log.len() as i64));
3881                m.add_method_mut("push_log", |_, this, s: String| {
3882                    this.log.push(s);
3883                    Ok(())
3884                });
3885                m.add_meta_method(MetaMethod::Index, |_, _this, key: String| {
3886                    Ok(::std::format!("dynamic:{key}"))
3887                });
3888                m.add_meta_method_mut(
3889                    MetaMethod::NewIndex,
3890                    |_, _this, (_k, _v): (String, Value)| Ok(()),
3891                );
3892            }
3893        }
3894
3895        let weak_inner = {
3896            let lua = Lua::new();
3897            let weak = Rc::downgrade(&lua.inner);
3898            let _ = lua
3899                .create_userdata(Mixed {
3900                    x: 1.0,
3901                    log: Vec::new(),
3902                })
3903                .expect("create");
3904            weak
3905        };
3906
3907        assert!(
3908            weak_inner.upgrade().is_none(),
3909            "maximal-composition userdata leaked LuaInner: \
3910             check the composed __index / __newindex captures"
3911        );
3912    }
3913
3914    /// The composed `__index` allocates two or three temporary external roots
3915    /// per call (for the per-call `Table`/`Function` views) and relies on
3916    /// `pending_external_unroots` being flushed by the next `with_state`. If
3917    /// that plumbing ever breaks, every field read silently leaks a root. Hammer
3918    /// it in a loop and assert `external_roots.len()` returns to baseline.
3919    #[test]
3920    fn composed_dispatch_does_not_accumulate_external_roots() {
3921        struct Probe {
3922            x: i64,
3923        }
3924        impl UserData for Probe {
3925            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
3926                m.add_field_method_get("x", |_, this| Ok(this.x));
3927            }
3928        }
3929
3930        let lua = Lua::new();
3931        lua.globals()
3932            .set("v", lua.create_userdata(Probe { x: 1 }).unwrap())
3933            .unwrap();
3934        let baseline = external_root_count(&lua);
3935
3936        for _ in 0..1000 {
3937            let _: i64 = lua.load("return v.x").eval().unwrap();
3938        }
3939        // The last iteration's temp roots queue for unroot on exit of its
3940        // outer with_state; force one more so the flush definitely runs.
3941        let after = external_root_count(&lua);
3942
3943        assert!(
3944            after <= baseline + 2,
3945            "external roots grew under composed __index churn: baseline={baseline} after={after}"
3946        );
3947    }
3948
3949    /// A Rust userdata method takes a Lua `Function` and calls it. Exercises
3950    /// the Weak<LuaInner> upgrade plus the `active_state` reentrancy pointer
3951    /// together. The bms-lua-rs reflection bridge hits this shape on every
3952    /// component access; an existing test covers `create_function` reentry but
3953    /// not the userdata-method path.
3954    #[test]
3955    fn userdata_method_can_reenter_lua_from_callback() {
3956        struct Calc;
3957        impl UserData for Calc {
3958            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
3959                m.add_method("apply", |_lua, _this, f: Function| {
3960                    let r: i64 = f.call(7_i64)?;
3961                    Ok(r + 1)
3962                });
3963            }
3964        }
3965
3966        let lua = Lua::new();
3967        lua.globals()
3968            .set("c", lua.create_userdata(Calc).unwrap())
3969            .unwrap();
3970        let r: i64 = lua
3971            .load("return c:apply(function(n) return n * 2 end)")
3972            .eval()
3973            .unwrap();
3974        assert_eq!(r, 15);
3975    }
3976
3977    /// Two `Lua::new()` instances must each build their own metatable for the
3978    /// same Rust type. Counts calls to `add_methods` across both states and
3979    /// asserts each state builds independently while still de-duplicating
3980    /// within its own scope.
3981    #[test]
3982    fn metatable_cache_is_per_lua_state() {
3983        use std::sync::atomic::{AtomicUsize, Ordering};
3984        static BUILDS: AtomicUsize = AtomicUsize::new(0);
3985
3986        struct Marker {
3987            v: i64,
3988        }
3989        impl UserData for Marker {
3990            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
3991                BUILDS.fetch_add(1, Ordering::SeqCst);
3992                m.add_method("v", |_, this, ()| Ok(this.v));
3993            }
3994        }
3995
3996        let start = BUILDS.load(Ordering::SeqCst);
3997
3998        let lua_a = Lua::new();
3999        let _a1 = lua_a.create_userdata(Marker { v: 1 }).unwrap();
4000        assert_eq!(BUILDS.load(Ordering::SeqCst) - start, 1, "state A first build");
4001        let _a2 = lua_a.create_userdata(Marker { v: 2 }).unwrap();
4002        assert_eq!(BUILDS.load(Ordering::SeqCst) - start, 1, "state A reuses cache");
4003
4004        let lua_b = Lua::new();
4005        let _b1 = lua_b.create_userdata(Marker { v: 3 }).unwrap();
4006        assert_eq!(BUILDS.load(Ordering::SeqCst) - start, 2, "state B is independent");
4007
4008        let _a3 = lua_a.create_userdata(Marker { v: 4 }).unwrap();
4009        assert_eq!(BUILDS.load(Ordering::SeqCst) - start, 2, "state A still cached");
4010    }
4011
4012    /// Field beats method when names collide. The composed `__index` looks up
4013    /// field getters before the method table; pin that order so a future
4014    /// refactor of the dispatch closure does not silently swap precedence.
4015    #[test]
4016    fn field_shadows_method_of_same_name() {
4017        struct Shadow {
4018            x: i64,
4019        }
4020        impl UserData for Shadow {
4021            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
4022                m.add_field_method_get("x", |_, this| Ok(this.x));
4023                m.add_method("x", |_, _this, ()| Ok(999_i64));
4024            }
4025        }
4026
4027        let lua = Lua::new();
4028        lua.globals()
4029            .set("v", lua.create_userdata(Shadow { x: 42 }).unwrap())
4030            .unwrap();
4031
4032        let r: i64 = lua.load("return v.x").eval().unwrap();
4033        assert_eq!(r, 42, "the field getter should beat the method of the same name");
4034    }
4035
4036    /// Direct Lua-side proof the cache is real: two userdata of the same type
4037    /// share the same metatable object as observed by `getmetatable`. If the
4038    /// cache regressed to per-value metatables this returns false.
4039    #[test]
4040    fn cached_metatable_is_shared_across_values_in_lua() {
4041        struct Twin;
4042        impl UserData for Twin {
4043            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
4044                m.add_method("ping", |_, _this, ()| Ok(1_i64));
4045            }
4046        }
4047
4048        let lua = Lua::new();
4049        lua.globals()
4050            .set("a", lua.create_userdata(Twin).unwrap())
4051            .unwrap();
4052        lua.globals()
4053            .set("b", lua.create_userdata(Twin).unwrap())
4054            .unwrap();
4055
4056        let same: bool = lua
4057            .load("return getmetatable(a) == getmetatable(b)")
4058            .eval()
4059            .unwrap();
4060        assert!(same, "cached metatable must be shared across values of the same type");
4061    }
4062
4063    #[test]
4064    fn fields_and_methods_coexist() {
4065        struct Vec2 {
4066            x: f64,
4067            y: f64,
4068        }
4069        impl UserData for Vec2 {
4070            fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
4071                m.add_field_method_get("x", |_, this| Ok(this.x));
4072                m.add_field_method_get("y", |_, this| Ok(this.y));
4073                m.add_field_method_set("x", |_, this, v: f64| {
4074                    this.x = v;
4075                    Ok(())
4076                });
4077                m.add_field_method_set("y", |_, this, v: f64| {
4078                    this.y = v;
4079                    Ok(())
4080                });
4081                m.add_method("length", |_, this, ()| {
4082                    Ok((this.x * this.x + this.y * this.y).sqrt())
4083                });
4084                m.add_method_mut("scale", |_, this, k: f64| {
4085                    this.x *= k;
4086                    this.y *= k;
4087                    Ok(())
4088                });
4089            }
4090        }
4091
4092        let lua = Lua::new();
4093        let v = lua.create_userdata(Vec2 { x: 3.0, y: 4.0 }).unwrap();
4094        lua.globals().set("v", &v).unwrap();
4095
4096        // method call and field reads on the same value
4097        assert_eq!(lua.load("return v:length()").eval::<f64>().unwrap(), 5.0);
4098        assert_eq!(lua.load("return v.x + v.y").eval::<f64>().unwrap(), 7.0);
4099
4100        // field write
4101        lua.load("v.x = 6").exec().unwrap();
4102        assert_eq!(lua.load("return v.x").eval::<f64>().unwrap(), 6.0);
4103
4104        // method mutation is visible through field reads
4105        lua.load("v:scale(2)").exec().unwrap();
4106        assert_eq!(lua.load("return v.x").eval::<f64>().unwrap(), 12.0);
4107        assert_eq!(lua.load("return v.y").eval::<f64>().unwrap(), 8.0);
4108
4109        // unknown field assignment errors
4110        assert!(lua.load("v.z = 1").exec().is_err());
4111    }
4112
4113    #[test]
4114    fn userdata_methods_dispatch_and_track_borrows() {
4115        let lua = Lua::new();
4116        let globals = lua.globals();
4117        let counter = lua
4118            .create_userdata(Counter { value: 1 })
4119            .expect("userdata should create");
4120        globals
4121            .set("counter", &counter)
4122            .expect("userdata should register");
4123
4124        let result: i64 = lua
4125            .load("counter:inc(5); return counter:get()")
4126            .eval()
4127            .expect("methods should dispatch");
4128        assert_eq!(result, 6);
4129        assert_eq!(
4130            counter
4131                .with_borrow::<Counter, _>(|counter| counter.value)
4132                .expect("borrow should work"),
4133            6
4134        );
4135
4136        {
4137            let borrowed = counter
4138                .borrow::<Counter>()
4139                .expect("borrow guard should work");
4140            assert_eq!(borrowed.value, 6);
4141        }
4142
4143        {
4144            let mut borrowed = counter
4145                .borrow_mut::<Counter>()
4146                .expect("mutable borrow guard should work");
4147            borrowed.value = 9;
4148        }
4149
4150        assert_eq!(
4151            lua.load("return counter:get()")
4152                .eval::<i64>()
4153                .expect("method should see guard mutation"),
4154            9
4155        );
4156    }
4157
4158    #[test]
4159    fn userdata_payload_survives_gc_while_lua_holds_userdata() {
4160        let lua = Lua::new();
4161        let globals = lua.globals();
4162        let counter = lua
4163            .create_userdata(Counter { value: 10 })
4164            .expect("userdata should create");
4165        globals
4166            .set("counter", counter)
4167            .expect("userdata should register");
4168
4169        lua.gc_collect();
4170        let result: i64 = lua
4171            .load("counter:inc(2); collectgarbage('collect'); return counter:get()")
4172            .eval()
4173            .expect("userdata should survive collection");
4174        assert_eq!(result, 12);
4175    }
4176
4177    #[test]
4178    fn userdata_runtime_borrow_conflict_returns_lua_error() {
4179        let lua = Lua::new();
4180        let globals = lua.globals();
4181        let counter = lua
4182            .create_userdata(Counter { value: 1 })
4183            .expect("userdata should create");
4184        globals
4185            .set("counter", &counter)
4186            .expect("userdata should register");
4187
4188        let failed = counter
4189            .with_borrow::<Counter, _>(|_| lua.load("return counter:inc(1)").eval::<i64>().is_err())
4190            .expect("outer borrow should succeed");
4191        assert!(
4192            failed,
4193            "mutable method should fail while immutable borrow is held"
4194        );
4195        assert_eq!(
4196            counter
4197                .with_borrow::<Counter, _>(|counter| counter.value)
4198                .expect("borrow should work"),
4199            1
4200        );
4201    }
4202
4203    #[test]
4204    fn userdata_index_and_newindex_metamethods_dispatch() {
4205        let lua = Lua::new();
4206        let globals = lua.globals();
4207        let bag = lua
4208            .create_userdata(PropertyBag { value: 7 })
4209            .expect("userdata should create");
4210        globals.set("bag", &bag).expect("userdata should register");
4211
4212        let result: i64 = lua
4213            .load("bag.value = 42; return bag.value")
4214            .eval()
4215            .expect("metamethods should dispatch");
4216        assert_eq!(result, 42);
4217        assert_eq!(
4218            bag.with_borrow::<PropertyBag, _>(|bag| bag.value)
4219                .expect("borrow should work"),
4220            42
4221        );
4222    }
4223
4224    #[test]
4225    fn userdata_values_convert_directly_with_into_lua() {
4226        let lua = Lua::new();
4227        let globals = lua.globals();
4228        globals
4229            .set("counter", Counter { value: 3 })
4230            .expect("userdata should convert through IntoLua");
4231
4232        let result: i64 = lua
4233            .load("counter:inc(4); return counter:get()")
4234            .eval()
4235            .expect("converted userdata should dispatch methods");
4236        assert_eq!(result, 7);
4237    }
4238
4239    #[test]
4240    fn variadic_args_and_returns_convert_all_values() {
4241        let lua = Lua::new();
4242        let globals = lua.globals();
4243
4244        let sum = lua
4245            .create_function(|_lua, values: Variadic<i64>| Ok(values.iter().sum::<i64>()))
4246            .expect("variadic callback should create");
4247        globals.set("sum", sum).expect("callback should register");
4248        let result: i64 = lua
4249            .load("return sum(3, 2, 5)")
4250            .eval()
4251            .expect("variadic callback should run");
4252        assert_eq!(result, 10);
4253
4254        let echo = lua
4255            .create_function(|_lua, values: Variadic<Value>| Ok(values))
4256            .expect("variadic return callback should create");
4257        globals.set("echo", echo).expect("callback should register");
4258        let result: (i64, i64, i64) = lua
4259            .load("return echo(1, 2, 3)")
4260            .eval()
4261            .expect("variadic returns should stay separate");
4262        assert_eq!(result, (1, 2, 3));
4263
4264        let values: Variadic<i64> = lua
4265            .load("return 4, 5, 6")
4266            .eval()
4267            .expect("variadic eval should collect all returns");
4268        assert_eq!(values.into_vec(), vec![4, 5, 6]);
4269    }
4270
4271    #[test]
4272    fn vectors_maps_and_triple_returns_convert_through_tables() {
4273        let lua = Lua::new();
4274        let globals = lua.globals();
4275
4276        globals
4277            .set("list", vec![1_i64, 2, 3])
4278            .expect("vector should convert to table");
4279        let second: i64 = lua
4280            .load("return list[2]")
4281            .eval()
4282            .expect("table should be readable from Lua");
4283        assert_eq!(second, 2);
4284
4285        let list: Vec<i64> = lua
4286            .load("return {4, 5, 6}")
4287            .eval()
4288            .expect("table should convert to vector");
4289        assert_eq!(list, vec![4, 5, 6]);
4290
4291        let mut map = HashMap::new();
4292        map.insert("left".to_string(), 10_i64);
4293        map.insert("right".to_string(), 20_i64);
4294        globals
4295            .set("map", map)
4296            .expect("map should convert to table");
4297        let sum: i64 = lua
4298            .load("return map.left + map.right")
4299            .eval()
4300            .expect("map table should be readable from Lua");
4301        assert_eq!(sum, 30);
4302
4303        let map: HashMap<String, i64> = lua
4304            .load("return {alpha = 3, beta = 9}")
4305            .eval()
4306            .expect("table should convert to map");
4307        assert_eq!(map.get("alpha"), Some(&3));
4308        assert_eq!(map.get("beta"), Some(&9));
4309
4310        let triple: (i64, i64, i64) = lua
4311            .load("return 1, 2, 3")
4312            .eval()
4313            .expect("triple returns should convert");
4314        assert_eq!(triple, (1, 2, 3));
4315    }
4316
4317    /// Pull the human-readable message out of a `LuaError::Runtime(LuaValue::Str)`.
4318    /// The default `Display` for `LuaError` just defers to `Debug`, which prints
4319    /// `Runtime(Str(GcRef(Gc(0x…))))` for runtime errors that were raised through
4320    /// Lua. The actual string lives behind the GcRef; this helper digs it out so
4321    /// assertions can check the message text directly.
4322    fn runtime_error_message(err: &LuaError) -> String {
4323        match err {
4324            LuaError::Runtime(v) | LuaError::Syntax(v) => match v {
4325                RawLuaValue::Str(s) => String::from_utf8_lossy(s.as_bytes()).into_owned(),
4326                other => format!("{other:?}"),
4327            },
4328            other => format!("{other:?}"),
4329        }
4330    }
4331
4332    /// Helper userdata for scope tests: carries a single mutable field so a
4333    /// `&mut Counter` borrow handed to Lua can be observed from Rust after the
4334    /// scope ends. Distinct from the module-level `Counter` only to keep the
4335    /// owned-vs-scoped paths from sharing fixtures.
4336    struct ScopedCounter {
4337        value: i64,
4338        calls: Cell<u32>,
4339    }
4340
4341    impl UserData for ScopedCounter {
4342        fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
4343            methods.add_method("get", |_lua, this, ()| {
4344                this.calls.set(this.calls.get() + 1);
4345                Ok(this.value)
4346            });
4347            methods.add_method_mut("inc", |_lua, this, delta: i64| {
4348                this.value += delta;
4349                Ok(this.value)
4350            });
4351            methods.add_method("calls", |_lua, this, ()| Ok(this.calls.get() as i64));
4352            methods.add_method("call_get_via_global", |lua, _this, ()| {
4353                lua.load("return c:get()").eval::<i64>()
4354            });
4355            methods.add_method_mut("inc_via_global", |lua, this, ()| {
4356                this.value += 1;
4357                lua.load("return c:get()").eval::<i64>()
4358            });
4359        }
4360    }
4361
4362    struct ScopedBag {
4363        value: i64,
4364    }
4365
4366    impl UserData for ScopedBag {
4367        fn add_meta_methods<M: UserDataMethods<Self>>(methods: &mut M) {
4368            methods.add_meta_method(MetaMethod::Index, |_lua, this, key: String| {
4369                if key == "value" {
4370                    Ok(Value::Integer(this.value))
4371                } else {
4372                    Ok(Value::Nil)
4373                }
4374            });
4375            methods.add_meta_method_mut(
4376                MetaMethod::NewIndex,
4377                |_lua, this, (key, value): (String, i64)| {
4378                    if key != "value" {
4379                        return Err(LuaError::runtime(format_args!("unknown property")));
4380                    }
4381                    this.value = value;
4382                    Ok(())
4383                },
4384            );
4385        }
4386    }
4387
4388    struct ScopedFielded {
4389        n: i64,
4390    }
4391
4392    impl UserData for ScopedFielded {
4393        fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
4394            methods.add_field_method_get("n", |_lua, this| Ok(this.n));
4395            methods.add_field_method_set("n", |_lua, this, new: i64| {
4396                this.n = new;
4397                Ok(())
4398            });
4399        }
4400    }
4401
4402    /// Smoke test for [`Lua::scope`]: a `&mut ScopedCounter` borrow lives on
4403    /// the Rust stack, gets handed to Lua as a userdata for the duration of a
4404    /// scope body, and the original is mutated through it.
4405    #[test]
4406    fn scope_userdata_dispatches_method_calls_against_borrow() {
4407        let lua = Lua::new();
4408        let mut counter = ScopedCounter {
4409            value: 10,
4410            calls: Cell::new(0),
4411        };
4412
4413        let observed: i64 = lua
4414            .scope(|scope| {
4415                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4416                lua.globals().set("c", &ud)?;
4417                lua.load("return c:get()").eval::<i64>()
4418            })
4419            .expect("scope body should succeed");
4420        assert_eq!(observed, 10);
4421        assert_eq!(counter.value, 10);
4422        assert_eq!(counter.calls.get(), 1);
4423    }
4424
4425    /// Mutations through a scoped `&mut T` method must be visible to the Rust
4426    /// owner after the scope returns. This is the central reason for the API:
4427    /// `&mut World` etc. need to round-trip cleanly.
4428    #[test]
4429    fn scope_userdata_mut_method_propagates_to_external_borrow() {
4430        let lua = Lua::new();
4431        let mut counter = ScopedCounter {
4432            value: 0,
4433            calls: Cell::new(0),
4434        };
4435
4436        lua.scope(|scope| {
4437            let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4438            lua.globals().set("c", &ud)?;
4439            lua.load("c:inc(5); c:inc(7)").exec()
4440        })
4441        .expect("scope body should succeed");
4442        assert_eq!(counter.value, 12);
4443    }
4444
4445    /// Headline safety property: any AnyUserData that leaks past its scope
4446    /// must fail cleanly (Lua runtime error), not touch the freed `&mut` slot.
4447    /// We persist the leaked userdata on `globals` precisely to model the
4448    /// adversarial case from the issue: a script squirrels away a `&mut World`
4449    /// and tries to use it later.
4450    #[test]
4451    fn scope_userdata_invalidated_after_scope_returns_runtime_error() {
4452        let lua = Lua::new();
4453        let mut counter = ScopedCounter {
4454            value: 99,
4455            calls: Cell::new(0),
4456        };
4457
4458        lua.scope(|scope| {
4459            let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4460            lua.globals().set("leaked", &ud)?;
4461            Ok(())
4462        })
4463        .expect("scope body should succeed");
4464
4465        let err = lua
4466            .load("return leaked:get()")
4467            .eval::<i64>()
4468            .expect_err("scoped userdata must be unusable after scope ends");
4469        let msg = runtime_error_message(&err);
4470        assert!(
4471            msg.contains("no longer valid") || msg.contains("scope has ended"),
4472            "expected invalidation error, got: {msg}"
4473        );
4474    }
4475
4476    /// Even a `pcall`-wrapped post-scope invocation must surface a Lua-level
4477    /// error rather than crashing. Models the case where the script tries to
4478    /// recover from the failure.
4479    #[test]
4480    fn scope_userdata_invalidated_is_recoverable_via_pcall() {
4481        let lua = Lua::new();
4482        let mut counter = ScopedCounter {
4483            value: 5,
4484            calls: Cell::new(0),
4485        };
4486
4487        lua.scope(|scope| {
4488            let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4489            lua.globals().set("leaked", &ud)?;
4490            Ok(())
4491        })
4492        .expect("scope body should succeed");
4493
4494        let (ok, _err_msg): (bool, String) = lua
4495            .load("local ok, e = pcall(function() return leaked:get() end); return ok, tostring(e)")
4496            .eval()
4497            .expect("pcall harness should produce two values");
4498        assert!(!ok, "post-scope call must fail");
4499    }
4500
4501    /// Re-entry from inside a `&mut` method body into Lua that calls another
4502    /// method on the *same* scoped userdata must be rejected at the second
4503    /// borrow attempt, not produce aliasing `&mut`s. This is the aliasing
4504    /// concern called out in the design.
4505    #[test]
4506    fn scope_userdata_reentrant_borrow_during_mut_method_returns_error() {
4507        let lua = Lua::new();
4508        let mut counter = ScopedCounter {
4509            value: 0,
4510            calls: Cell::new(0),
4511        };
4512
4513        let err = lua
4514            .scope(|scope| {
4515                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4516                lua.globals().set("c", &ud)?;
4517                lua.load("return c:inc_via_global()").eval::<i64>()
4518            })
4519            .expect_err("re-entry while mut-borrowed must fail");
4520        let msg = runtime_error_message(&err);
4521        assert!(
4522            msg.contains("already") && msg.contains("borrowed"),
4523            "expected borrow-conflict error, got: {msg}"
4524        );
4525        assert_eq!(counter.value, 1, "outer mutation persists despite inner failure");
4526    }
4527
4528    /// Two shared borrows of the same scoped cell must be compatible: a
4529    /// `:get()` re-entering Lua to call `:get()` again should succeed.
4530    #[test]
4531    fn scope_userdata_reentrant_shared_borrows_are_compatible() {
4532        let lua = Lua::new();
4533        let mut counter = ScopedCounter {
4534            value: 17,
4535            calls: Cell::new(0),
4536        };
4537
4538        let observed: i64 = lua
4539            .scope(|scope| {
4540                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4541                lua.globals().set("c", &ud)?;
4542                lua.load("return c:call_get_via_global()").eval::<i64>()
4543            })
4544            .expect("nested shared borrows should succeed");
4545        assert_eq!(observed, 17);
4546        assert_eq!(counter.calls.get(), 1);
4547    }
4548
4549    /// Field methods route through `create_scoped_userdata_method`/`_mut` via
4550    /// the registry's `RegistryMode::Scoped` branch. Verifies that path is
4551    /// wired correctly for both get and set.
4552    #[test]
4553    fn scope_userdata_field_methods_get_and_set() {
4554        let lua = Lua::new();
4555        let mut bag = ScopedFielded { n: 3 };
4556
4557        let read_back: i64 = lua
4558            .scope(|scope| {
4559                let ud = scope.create_userdata_ref_mut(&lua, &mut bag)?;
4560                lua.globals().set("f", &ud)?;
4561                lua.load("f.n = f.n + 39; return f.n").eval::<i64>()
4562            })
4563            .expect("field methods should dispatch");
4564        assert_eq!(read_back, 42);
4565        assert_eq!(bag.n, 42);
4566    }
4567
4568    /// Meta-methods (`__index`/`__newindex` written by hand on a type) must
4569    /// also route through the scoped path.
4570    #[test]
4571    fn scope_userdata_meta_methods_dispatch() {
4572        let lua = Lua::new();
4573        let mut bag = ScopedBag { value: 100 };
4574
4575        let read: i64 = lua
4576            .scope(|scope| {
4577                let ud = scope.create_userdata_ref_mut(&lua, &mut bag)?;
4578                lua.globals().set("b", &ud)?;
4579                lua.load("b.value = 200; return b.value").eval::<i64>()
4580            })
4581            .expect("scoped meta-methods should dispatch");
4582        assert_eq!(read, 200);
4583        assert_eq!(bag.value, 200);
4584    }
4585
4586    /// Multiple scoped userdatas of the *same* type in one scope are
4587    /// independent: each call routes to the correct cell.
4588    #[test]
4589    fn scope_userdata_multiple_borrows_same_type_in_one_scope() {
4590        let lua = Lua::new();
4591        let mut a = ScopedCounter {
4592            value: 1,
4593            calls: Cell::new(0),
4594        };
4595        let mut b = ScopedCounter {
4596            value: 100,
4597            calls: Cell::new(0),
4598        };
4599
4600        lua.scope(|scope| {
4601            let ua = scope.create_userdata_ref_mut(&lua, &mut a)?;
4602            let ub = scope.create_userdata_ref_mut(&lua, &mut b)?;
4603            lua.globals().set("a", &ua)?;
4604            lua.globals().set("b", &ub)?;
4605            lua.load("a:inc(10); b:inc(1)").exec()
4606        })
4607        .expect("scope body should succeed");
4608        assert_eq!(a.value, 11);
4609        assert_eq!(b.value, 101);
4610    }
4611
4612    /// Different types in one scope share the scope's invalidation but live
4613    /// in independent metatables; both must work.
4614    #[test]
4615    fn scope_userdata_different_types_coexist_in_one_scope() {
4616        let lua = Lua::new();
4617        let mut counter = ScopedCounter {
4618            value: 0,
4619            calls: Cell::new(0),
4620        };
4621        let mut bag = ScopedBag { value: 0 };
4622
4623        lua.scope(|scope| {
4624            let uc = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4625            let ub = scope.create_userdata_ref_mut(&lua, &mut bag)?;
4626            lua.globals().set("c", &uc)?;
4627            lua.globals().set("b", &ub)?;
4628            lua.load("c:inc(7); b.value = 13").exec()
4629        })
4630        .expect("scope body should succeed");
4631        assert_eq!(counter.value, 7);
4632        assert_eq!(bag.value, 13);
4633    }
4634
4635    /// `Lua::scope` threads its closure's return value out — used for
4636    /// extracting Lua results without leaking them through globals.
4637    #[test]
4638    fn scope_userdata_scope_returns_closure_value() {
4639        let lua = Lua::new();
4640        let mut counter = ScopedCounter {
4641            value: 4,
4642            calls: Cell::new(0),
4643        };
4644
4645        let doubled: i64 = lua
4646            .scope(|scope| {
4647                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4648                lua.globals().set("c", &ud)?;
4649                lua.load("return c:inc(c:get())").eval::<i64>()
4650            })
4651            .expect("scope body should succeed");
4652        assert_eq!(doubled, 8);
4653        assert_eq!(counter.value, 8);
4654    }
4655
4656    /// A scoped userdata invalidated by its scope still keeps the
4657    /// `host_value` Rc alive on the userdata; calling it from a *different*
4658    /// `Lua` instance (which doesn't own this cell) is independently rejected
4659    /// by `scoped_userdata_cell`'s state check. We cannot fully test the
4660    /// cross-state case because `globals().set` requires the same Lua, but we
4661    /// can verify the cached scoped metatable is per-state: building a fresh
4662    /// `Lua` doesn't see the prior state's metatable cache.
4663    #[test]
4664    fn scope_userdata_metatable_cache_is_per_state() {
4665        let lua_a = Lua::new();
4666        let lua_b = Lua::new();
4667        let mut a = ScopedCounter {
4668            value: 1,
4669            calls: Cell::new(0),
4670        };
4671        let mut b = ScopedCounter {
4672            value: 2,
4673            calls: Cell::new(0),
4674        };
4675
4676        lua_a
4677            .scope(|scope| {
4678                let _ud = scope.create_userdata_ref_mut(&lua_a, &mut a)?;
4679                Ok(())
4680            })
4681            .expect("scope on A should succeed");
4682        lua_b
4683            .scope(|scope| {
4684                let _ud = scope.create_userdata_ref_mut(&lua_b, &mut b)?;
4685                Ok(())
4686            })
4687            .expect("scope on B should succeed");
4688
4689        let cache_a_len = lua_a.inner.userdata_scoped_metatables.borrow().len();
4690        let cache_b_len = lua_b.inner.userdata_scoped_metatables.borrow().len();
4691        assert_eq!(cache_a_len, 1);
4692        assert_eq!(cache_b_len, 1);
4693    }
4694
4695    /// The scoped-metatable cache must not be repopulated on every scope:
4696    /// a second scope of the same type re-uses the metatable built by the
4697    /// first. Confirms the `match cached { Some(mt) => mt, None => ... }`
4698    /// branch in `create_scoped_userdata`.
4699    #[test]
4700    fn scope_userdata_metatable_is_built_once_per_type() {
4701        let lua = Lua::new();
4702        let mut a = ScopedCounter {
4703            value: 0,
4704            calls: Cell::new(0),
4705        };
4706        let mut b = ScopedCounter {
4707            value: 0,
4708            calls: Cell::new(0),
4709        };
4710
4711        lua.scope(|scope| {
4712            let _ud = scope.create_userdata_ref_mut(&lua, &mut a)?;
4713            Ok(())
4714        })
4715        .expect("first scope should succeed");
4716        let after_first = lua.inner.userdata_scoped_metatables.borrow().len();
4717
4718        lua.scope(|scope| {
4719            let _ud = scope.create_userdata_ref_mut(&lua, &mut b)?;
4720            Ok(())
4721        })
4722        .expect("second scope should succeed");
4723        let after_second = lua.inner.userdata_scoped_metatables.borrow().len();
4724
4725        assert_eq!(after_first, 1);
4726        assert_eq!(after_second, 1);
4727    }
4728
4729    /// Rust-side shared borrow of a scoped userdata works inside the scope.
4730    #[test]
4731    fn scope_userdata_rust_side_scoped_borrow_inside_scope() {
4732        let lua = Lua::new();
4733        let mut counter = ScopedCounter {
4734            value: 21,
4735            calls: Cell::new(0),
4736        };
4737
4738        let observed = lua
4739            .scope(|scope| {
4740                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4741                ud.scoped_borrow::<ScopedCounter, _>(|c| c.value)
4742            })
4743            .expect("scoped_borrow should succeed inside scope");
4744        assert_eq!(observed, 21);
4745    }
4746
4747    /// Rust-side mut borrow of a scoped userdata mutates the source.
4748    #[test]
4749    fn scope_userdata_rust_side_scoped_borrow_mut_inside_scope() {
4750        let lua = Lua::new();
4751        let mut counter = ScopedCounter {
4752            value: 0,
4753            calls: Cell::new(0),
4754        };
4755
4756        lua.scope(|scope| {
4757            let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4758            ud.scoped_borrow_mut::<ScopedCounter, _>(|c| c.value = 5)
4759        })
4760        .expect("scoped_borrow_mut should succeed");
4761        assert_eq!(counter.value, 5);
4762    }
4763
4764    /// The headline FFI-side guarantee: an `AnyUserData` smuggled out of its
4765    /// scope cannot hand out a `&T` from Rust either. Cell invalidation drives
4766    /// both sides; this test pins it down on the Rust side.
4767    #[test]
4768    fn scope_userdata_rust_side_borrow_after_scope_errors() {
4769        let lua = Lua::new();
4770        let mut counter = ScopedCounter {
4771            value: 7,
4772            calls: Cell::new(0),
4773        };
4774
4775        let leaked: AnyUserData = lua
4776            .scope(|scope| scope.create_userdata_ref_mut(&lua, &mut counter))
4777            .expect("scope body should succeed");
4778
4779        let err = leaked
4780            .scoped_borrow::<ScopedCounter, _>(|c| c.value)
4781            .expect_err("post-scope Rust borrow must fail");
4782        let msg = runtime_error_message(&err);
4783        assert!(
4784            msg.contains("no longer valid") || msg.contains("scope has ended"),
4785            "expected invalidation error, got: {msg}"
4786        );
4787
4788        let err = leaked
4789            .scoped_borrow_mut::<ScopedCounter, _>(|c| c.value = 99)
4790            .expect_err("post-scope Rust mut-borrow must fail");
4791        let msg = runtime_error_message(&err);
4792        assert!(
4793            msg.contains("no longer valid") || msg.contains("scope has ended"),
4794            "expected invalidation error, got: {msg}"
4795        );
4796
4797        assert_eq!(counter.value, 7, "the borrow must not have been touched");
4798    }
4799
4800    /// The owned `AnyUserData::borrow`/`with_borrow` path is for
4801    /// `Lua::create_userdata` (Rc<UserDataCell<T>> host); calling it against a
4802    /// scoped userdata downcasts cleanly to None and errors. This is a safety
4803    /// claim worth pinning explicitly: the owned path cannot accidentally
4804    /// reach into a scoped cell.
4805    #[test]
4806    fn scope_userdata_owned_borrow_path_rejects_scoped_cells() {
4807        let lua = Lua::new();
4808        let mut counter = ScopedCounter {
4809            value: 1,
4810            calls: Cell::new(0),
4811        };
4812
4813        let err = lua
4814            .scope(|scope| {
4815                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
4816                Ok(ud.with_borrow::<ScopedCounter, _>(|c| c.value))
4817            })
4818            .expect("scope body should succeed")
4819            .expect_err("owned borrow path must not reach a scoped cell");
4820        let msg = runtime_error_message(&err);
4821        assert!(
4822            msg.contains("type mismatch"),
4823            "expected type-mismatch error, got: {msg}"
4824        );
4825    }
4826
4827    /// And the reverse: an owned (`Lua::create_userdata`) AnyUserData rejects
4828    /// `scoped_borrow`. Confirms the two paths are isolated.
4829    #[test]
4830    fn scope_userdata_scoped_borrow_rejects_owned_cells() {
4831        let lua = Lua::new();
4832        let ud = lua
4833            .create_userdata(ScopedCounter {
4834                value: 5,
4835                calls: Cell::new(0),
4836            })
4837            .expect("owned userdata should create");
4838
4839        let err = ud
4840            .scoped_borrow::<ScopedCounter, _>(|c| c.value)
4841            .expect_err("scoped borrow must not reach an owned cell");
4842        let msg = runtime_error_message(&err);
4843        assert!(
4844            msg.contains("type mismatch"),
4845            "expected type-mismatch error, got: {msg}"
4846        );
4847    }
4848
4849    /// `scope.create_function` accepts a closure that captures by reference
4850    /// from the surrounding stack frame; calling it from Lua sees the live
4851    /// borrow. Mirrors the userdata-side basic test, but for closures.
4852    #[test]
4853    fn scope_function_captures_borrow_and_is_callable_from_lua() {
4854        let lua = Lua::new();
4855        let mut acc: i64 = 0;
4856
4857        let total: i64 = lua
4858            .scope(|scope| {
4859                let f = scope.create_function_mut(&lua, |_lua, n: i64| {
4860                    acc += n;
4861                    Ok(acc)
4862                })?;
4863                lua.globals().set("add", &f)?;
4864                lua.load("add(2); add(3); return add(5)").eval::<i64>()
4865            })
4866            .expect("scoped function should dispatch");
4867        assert_eq!(total, 10);
4868        assert_eq!(acc, 10);
4869    }
4870
4871    /// The closure body sees borrowed state across multiple invocations
4872    /// inside one scope — verifies the closure isn't being re-built per call.
4873    #[test]
4874    fn scope_function_calls_share_one_closure() {
4875        let lua = Lua::new();
4876        let counts = Cell::new(0u32);
4877
4878        lua.scope(|scope| {
4879            let f = scope.create_function(&lua, |_lua, ()| {
4880                counts.set(counts.get() + 1);
4881                Ok(())
4882            })?;
4883            lua.globals().set("tick", &f)?;
4884            lua.load("for _ = 1, 4 do tick() end").exec()
4885        })
4886        .expect("scope should succeed");
4887        assert_eq!(counts.get(), 4);
4888    }
4889
4890    /// Headline safety property for functions: a `Function` smuggled past its
4891    /// scope must error cleanly when called, not reach into the dropped
4892    /// closure.
4893    #[test]
4894    fn scope_function_invalidated_after_scope_returns_runtime_error() {
4895        let lua = Lua::new();
4896        let mut acc: i64 = 0;
4897
4898        lua.scope(|scope| {
4899            let f = scope.create_function_mut(&lua, |_lua, n: i64| {
4900                acc += n;
4901                Ok(acc)
4902            })?;
4903            lua.globals().set("add", &f)?;
4904            lua.load("add(1)").exec()
4905        })
4906        .expect("scope body should succeed");
4907        assert_eq!(acc, 1);
4908
4909        let err = lua
4910            .load("return add(100)")
4911            .eval::<i64>()
4912            .expect_err("post-scope call must fail");
4913        let msg = runtime_error_message(&err);
4914        assert!(
4915            msg.contains("no longer valid") || msg.contains("scope has ended"),
4916            "expected invalidation error, got: {msg}"
4917        );
4918        assert_eq!(acc, 1, "the closure's borrow must not have been touched");
4919    }
4920
4921    /// FnMut re-entry: if the closure calls back into Lua which calls itself,
4922    /// the inner `try_borrow_mut` on the closure's `RefCell` must reject the
4923    /// nested call rather than producing aliasing `&mut` captures.
4924    #[test]
4925    fn scope_function_reentrant_fnmut_is_rejected() {
4926        let lua = Lua::new();
4927        let mut count: i64 = 0;
4928
4929        let err = lua
4930            .scope(|scope| {
4931                let f = scope.create_function_mut(&lua, |lua, ()| {
4932                    count += 1;
4933                    if count < 2 {
4934                        lua.load("recurse()").exec()?;
4935                    }
4936                    Ok(())
4937                })?;
4938                lua.globals().set("recurse", &f)?;
4939                lua.load("recurse()").exec()
4940            })
4941            .expect_err("re-entrant FnMut must error");
4942        let msg = runtime_error_message(&err);
4943        assert!(
4944            msg.contains("already borrowed"),
4945            "expected FnMut-conflict error, got: {msg}"
4946        );
4947    }
4948
4949    /// Pairing test: a scoped userdata and a scoped function in the same
4950    /// scope can both borrow from the same stack frame (different parts of
4951    /// it). Models the Bevy use case: `&mut World` userdata plus a few
4952    /// closures that look at adjacent locals.
4953    #[test]
4954    fn scope_function_and_userdata_in_same_scope() {
4955        let lua = Lua::new();
4956        let mut bag = ScopedFielded { n: 0 };
4957        let log = Cell::new(0i64);
4958
4959        lua.scope(|scope| {
4960            let ud = scope.create_userdata_ref_mut(&lua, &mut bag)?;
4961            let logger = scope.create_function(&lua, |_lua, n: i64| {
4962                log.set(log.get() + n);
4963                Ok(())
4964            })?;
4965            lua.globals().set("b", &ud)?;
4966            lua.globals().set("log", &logger)?;
4967            lua.load("b.n = 42; log(b.n); log(b.n + 1)").exec()
4968        })
4969        .expect("mixed scope body should succeed");
4970        assert_eq!(bag.n, 42);
4971        assert_eq!(log.get(), 85);
4972    }
4973
4974    /// Even if the scope body errors before returning, the scoped function is
4975    /// still invalidated so a follow-up Lua call cannot resurrect the dead
4976    /// closure.
4977    #[test]
4978    fn scope_function_invalidated_even_when_body_errors() {
4979        let lua = Lua::new();
4980        let value = Cell::new(5i64);
4981
4982        let _err = lua
4983            .scope(|scope| -> Result<()> {
4984                let f = scope.create_function(&lua, |_lua, ()| Ok(value.get()))?;
4985                lua.globals().set("get", &f)?;
4986                Err(LuaError::runtime(format_args!("aborting")))
4987            })
4988            .expect_err("scope body should propagate error");
4989
4990        let err = lua
4991            .load("return get()")
4992            .eval::<i64>()
4993            .expect_err("function must be invalidated after error-exit scope");
4994        let msg = runtime_error_message(&err);
4995        assert!(
4996            msg.contains("no longer valid") || msg.contains("scope has ended"),
4997            "expected invalidation error, got: {msg}"
4998        );
4999    }
5000
5001    /// Many functions in one scope, all calling into shared borrowed state.
5002    /// Stresses the invalidator list ordering: every closure must remain
5003    /// callable until the scope ends, and all are invalidated together.
5004    #[test]
5005    fn scope_function_many_closures_in_one_scope() {
5006        let lua = Lua::new();
5007        let total = Cell::new(0i64);
5008        let total_ref = &total;
5009
5010        lua.scope(|scope| {
5011            for i in 1..=8 {
5012                let f = scope.create_function(&lua, move |_lua, ()| {
5013                    total_ref.set(total_ref.get() + i);
5014                    Ok(())
5015                })?;
5016                lua.globals().set(format!("f{}", i).as_str(), &f)?;
5017            }
5018            lua.load("f1(); f2(); f3(); f4(); f5(); f6(); f7(); f8()").exec()
5019        })
5020        .expect("scope with many closures should succeed");
5021        assert_eq!(total.get(), 36);
5022    }
5023
5024    /// If the closure body returns an error, the scope still drops and
5025    /// invalidates everything it created. We confirm by then using the
5026    /// leaked global from a follow-up call — it must report invalidated, not
5027    /// stale-but-alive.
5028    #[test]
5029    fn scope_userdata_invalidated_even_when_body_errors() {
5030        let lua = Lua::new();
5031        let mut counter = ScopedCounter {
5032            value: 1,
5033            calls: Cell::new(0),
5034        };
5035
5036        let err = lua
5037            .scope(|scope| -> Result<()> {
5038                let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
5039                lua.globals().set("c", &ud)?;
5040                Err(LuaError::runtime(format_args!("aborting scope")))
5041            })
5042            .expect_err("scope body should propagate error");
5043        let _ = err;
5044
5045        let leaked_err = lua
5046            .load("return c:get()")
5047            .eval::<i64>()
5048            .expect_err("leaked userdata must still be invalidated");
5049        let msg = runtime_error_message(&leaked_err);
5050        assert!(
5051            msg.contains("no longer valid") || msg.contains("scope has ended"),
5052            "expected invalidation error after scope-with-error, got: {msg}"
5053        );
5054    }
5055
5056    /// Cloning an `AnyUserData` produces two handles to the same scope cell.
5057    /// Invalidation runs against the cell, so a clone that escapes via a
5058    /// global must fail at the same point a direct handle would. Pins the
5059    /// "every reference to the same cell sees invalidation together"
5060    /// invariant.
5061    #[test]
5062    fn scope_userdata_cloned_handles_invalidate_together() {
5063        let lua = Lua::new();
5064        let mut counter = ScopedCounter {
5065            value: 9,
5066            calls: Cell::new(0),
5067        };
5068
5069        lua.scope(|scope| {
5070            let ud = scope.create_userdata_ref_mut(&lua, &mut counter)?;
5071            let clone = ud.clone();
5072            lua.globals().set("a", &ud)?;
5073            lua.globals().set("b", &clone)?;
5074            lua.load("assert(a:get() == 9); assert(b:get() == 9)").exec()
5075        })
5076        .expect("scope body should succeed");
5077
5078        let err_a = lua
5079            .load("return a:get()")
5080            .eval::<i64>()
5081            .expect_err("original handle must error post-scope");
5082        let err_b = lua
5083            .load("return b:get()")
5084            .eval::<i64>()
5085            .expect_err("cloned handle must error post-scope");
5086        assert!(runtime_error_message(&err_a).contains("no longer valid"));
5087        assert!(runtime_error_message(&err_b).contains("no longer valid"));
5088    }
5089
5090    /// Nested `Lua::scope` calls: cells created in the inner scope invalidate
5091    /// when the inner returns; cells in the outer remain live until the outer
5092    /// returns. Pins that scope cells don't leak across siblings/parents.
5093    #[test]
5094    fn scope_userdata_nested_scopes_isolated() {
5095        let lua = Lua::new();
5096        let mut outer_counter = ScopedCounter {
5097            value: 1,
5098            calls: Cell::new(0),
5099        };
5100        let mut inner_counter = ScopedCounter {
5101            value: 100,
5102            calls: Cell::new(0),
5103        };
5104
5105        lua.scope(|outer| {
5106            let o = outer.create_userdata_ref_mut(&lua, &mut outer_counter)?;
5107            lua.globals().set("outer", &o)?;
5108
5109            lua.scope(|inner| {
5110                let i = inner.create_userdata_ref_mut(&lua, &mut inner_counter)?;
5111                lua.globals().set("inner", &i)?;
5112                lua.load("assert(outer:get() == 1); assert(inner:get() == 100)").exec()
5113            })?;
5114
5115            // Inner ended. `inner` global is dead, but `outer` is still live.
5116            let inner_err = lua
5117                .load("return inner:get()")
5118                .eval::<i64>()
5119                .expect_err("inner userdata must be dead after inner scope");
5120            assert!(runtime_error_message(&inner_err).contains("no longer valid"));
5121
5122            let outer_alive: i64 = lua
5123                .load("return outer:get()")
5124                .eval()
5125                .expect("outer userdata must still be alive in outer scope");
5126            assert_eq!(outer_alive, 1);
5127            Ok(())
5128        })
5129        .expect("scope body should succeed");
5130
5131        // Outer ended; both should now be dead.
5132        let err = lua
5133            .load("return outer:get()")
5134            .eval::<i64>()
5135            .expect_err("outer userdata must be dead after outer scope");
5136        assert!(runtime_error_message(&err).contains("no longer valid"));
5137    }
5138
5139    // -- Direct exercises of the unsafe machinery, no Lua state --
5140    //
5141    // These tests bypass the full `Lua::scope` plumbing and poke `ScopedCell`
5142    // / `ScopedFnCell` directly. They exist so `cargo miri test scope_cell_`
5143    // can validate the scope unsafe surface in isolation. The full suite
5144    // still routes through the rest of the runtime, which currently has
5145    // pre-existing aliasing violations under Miri (lua-gc raw-pointer
5146    // patterns, unrelated to scope); these direct tests are the
5147    // miri-runnable subset.
5148
5149    #[test]
5150    fn scope_cell_shared_then_shared_succeeds() {
5151        let mut data = 17_i32;
5152        let cell = ScopedCell::<i32>::new(&mut data);
5153
5154        let a = cell.try_borrow().expect("first shared borrow");
5155        let b = cell.try_borrow().expect("second shared borrow");
5156        assert_eq!(*a, 17);
5157        assert_eq!(*b, 17);
5158        drop(a);
5159        drop(b);
5160
5161        cell.invalidate();
5162        assert!(cell.try_borrow().is_err(), "post-invalidate must fail");
5163    }
5164
5165    #[test]
5166    fn scope_cell_mut_then_shared_fails() {
5167        let mut data = 5_i32;
5168        let cell = ScopedCell::<i32>::new(&mut data);
5169
5170        let mut m = cell.try_borrow_mut().expect("first mut borrow");
5171        *m = 42;
5172        let s = cell.try_borrow();
5173        assert!(s.is_err(), "shared borrow while mut-held must fail");
5174        drop(m);
5175
5176        let s = cell.try_borrow().expect("shared borrow after mut release");
5177        assert_eq!(*s, 42);
5178    }
5179
5180    #[test]
5181    fn scope_cell_shared_then_mut_fails() {
5182        let mut data = 99_i32;
5183        let cell = ScopedCell::<i32>::new(&mut data);
5184
5185        let s = cell.try_borrow().expect("first shared borrow");
5186        let m = cell.try_borrow_mut();
5187        assert!(m.is_err(), "mut borrow while shared-held must fail");
5188        drop(s);
5189
5190        let mut m = cell.try_borrow_mut().expect("mut borrow after shared release");
5191        *m = 100;
5192        drop(m);
5193        assert_eq!(data, 100);
5194    }
5195
5196    #[test]
5197    fn scope_cell_invalidate_after_drop_of_guards_is_clean() {
5198        let mut data = String::from("hi");
5199        let cell = ScopedCell::<String>::new(&mut data);
5200        {
5201            let guard = cell.try_borrow().expect("borrow");
5202            assert_eq!(&*guard, "hi");
5203        }
5204        cell.invalidate();
5205        assert!(cell.try_borrow().is_err());
5206        assert!(cell.try_borrow_mut().is_err());
5207    }
5208
5209    #[test]
5210    fn scope_cell_drop_guard_decrements_borrow_count() {
5211        let mut data = 0_i32;
5212        let cell = ScopedCell::<i32>::new(&mut data);
5213        {
5214            let _a = cell.try_borrow().expect("a");
5215            let _b = cell.try_borrow().expect("b");
5216            assert!(cell.try_borrow_mut().is_err());
5217        }
5218        cell.try_borrow_mut().expect("mut borrow once guards drop");
5219    }
5220
5221    #[test]
5222    fn scope_fn_cell_dispatches_and_invalidates() {
5223        let counter = Cell::new(0i64);
5224        let adapter: Box<dyn Fn(&Lua, Vec<Value>) -> Result<Vec<Value>>> =
5225            Box::new(|_lua, _args| Ok(Vec::new()));
5226        let cell = Rc::new(ScopedFnCell {
5227            boxed: RefCell::new(Some(adapter)),
5228        });
5229
5230        let lua = Lua::new();
5231        cell.try_call(&lua, Vec::new()).expect("pre-invalidate call");
5232        counter.set(counter.get() + 1);
5233
5234        cell.invalidate();
5235
5236        let err = cell
5237            .try_call(&lua, Vec::new())
5238            .expect_err("post-invalidate call must fail");
5239        let msg = runtime_error_message(&err);
5240        assert!(msg.contains("no longer valid"), "got: {msg}");
5241        assert_eq!(counter.get(), 1);
5242    }
5243}