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