Skip to main content

luaur_rt/
scope.rs

1//! The [`Scope`] type: lifetime-bounded callbacks and userdata.
2//!
3//! Mirrors `mlua::Scope`. Constructed by [`Lua::scope`], a `Scope` lets you
4//! create Lua callbacks and userdata that borrow **non-`'static`** data from the
5//! enclosing stack frame. When the `scope` call returns (normally, via `?`, or
6//! through a panic), every object the scope created is *invalidated*: its boxed
7//! Rust closure / wrapped data is dropped (ending the borrows) and the
8//! underlying Lua object is neutralised so any later use from Lua errors with
9//! [`Error::CallbackDestructed`] / [`Error::UserDataDestructed`] instead of
10//! touching freed memory.
11//!
12//! ## The soundness argument (the unsafe core)
13//!
14//! [`Scope::create_function`] accepts a closure `F: Fn(&Lua, A) -> Result<R> +
15//! 'scope` — i.e. **not** `'static`. luaur-rt's normal callback machinery
16//! ([`create_callback_function`]) stores a `'static`
17//! [`BoxedCallback`](crate::callback). To bridge the gap we
18//! [`mem::transmute`] the closure's lifetime to `'static` before boxing it.
19//!
20//! That transmute is, in isolation, unsound: it lets a closure borrowing
21//! `'scope` data be stored where Lua believes it lives forever, and Lua may keep
22//! the resulting [`Function`] handle past the end of `'scope` (e.g. stored in a
23//! global). It is made sound by the **scope-exit invariant**:
24//!
25//! 1. On scope exit, *before* returning to the caller (and therefore before any
26//!    `'scope`-borrowed data can be dropped), every registered destructor runs.
27//! 2. A callback's destructor ([`destruct_callback`]) overwrites the boxed
28//!    closure inside the function's upvalue with a sentinel that returns
29//!    [`Error::CallbackDestructed`], and **drops the original box** — ending the
30//!    borrows right there. The Lua function object itself stays valid; only its
31//!    behavior changes. A post-scope call therefore hits the sentinel and
32//!    surfaces as `CallbackError { cause: CallbackDestructed }`, never a
33//!    use-after-free.
34//! 3. A userdata's destructor `take()`s the wrapped value out of its cell
35//!    (dropping the borrowed data) while leaving the cell memory valid; later
36//!    dispatch finds `None` and returns [`Error::UserDataDestructed`].
37//!
38//! Because `'scope` outlives nothing the closures borrow until *after* the
39//! destructors have run, no borrow can dangle. The destructors are run by a
40//! drop guard (the `destructors` field's `Drop`), so they execute even if the
41//! user closure returns `Err` or panics — preserving the invariant on every
42//! exit path.
43//!
44//! The two-lifetime shape `Scope<'scope, 'env>` mirrors mlua: `'env` is the
45//! lifetime of the data borrowed *into* the scope, `'scope` the (shorter)
46//! lifetime of the scope itself; `'env: 'scope`. Created objects are bounded by
47//! `'scope`, so they cannot escape the `scope` closure.
48
49use std::cell::RefCell;
50use std::marker::PhantomData;
51use std::mem;
52
53use crate::callback::{create_callback_function, destruct_callback, BoxedCallback};
54use crate::error::{Error, Result};
55use crate::function::Function;
56use crate::multi::MultiValue;
57use crate::state::Lua;
58use crate::traits::{FromLuaMulti, IntoLuaMulti};
59use crate::userdata::{create_scoped_userdata, AnyUserData, UserData};
60
61/// A scope for creating lifetime-bounded Lua callbacks and userdata.
62///
63/// Mirrors `mlua::Scope`. See the [module docs](self) and [`Lua::scope`] for the
64/// full picture, including the soundness argument for the lifetime erasure.
65pub struct Scope<'scope, 'env: 'scope> {
66    lua: Lua,
67    /// Destructors run (in reverse registration order) on scope exit, by the
68    /// `Drop` impl on this field. Held in a separate type so its `Drop` fires
69    /// regardless of how the `scope` closure exits.
70    destructors: Destructors,
71    /// Invariance over `'scope` and `'env`, exactly as mlua, so created objects
72    /// cannot outlive the scope and the borrowed data cannot be shortened.
73    _scope_invariant: PhantomData<&'scope mut &'scope ()>,
74    _env_invariant: PhantomData<&'env mut &'env ()>,
75}
76
77/// The registered destructors. Wrapped in its own struct so the `Drop` impl runs
78/// the destructors even if the user's `scope` closure panics or returns `Err`.
79struct Destructors {
80    list: RefCell<Vec<Box<dyn FnOnce()>>>,
81}
82
83impl Drop for Destructors {
84    fn drop(&mut self) {
85        // Run destructors in reverse registration order (LIFO), so objects are
86        // torn down opposite to how they were built — matching mlua. Each
87        // destructor only drops Rust state / neutralises a Lua object and never
88        // panics, so a `drain` loop is fine even mid-unwind.
89        let mut list = mem::take(&mut *self.list.borrow_mut());
90        while let Some(destructor) = list.pop() {
91            destructor();
92        }
93    }
94}
95
96impl<'scope, 'env: 'scope> Scope<'scope, 'env> {
97    pub(crate) fn new(lua: Lua) -> Self {
98        Scope {
99            lua,
100            destructors: Destructors {
101                list: RefCell::new(Vec::new()),
102            },
103            _scope_invariant: PhantomData,
104            _env_invariant: PhantomData,
105        }
106    }
107
108    /// Wrap a non-`'static` Rust closure into a callable Lua [`Function`] that is
109    /// invalidated when the scope ends.
110    ///
111    /// This is the scoped version of [`Lua::create_function`]: the closure may
112    /// borrow data living for `'scope`. See the [module docs](self) for why the
113    /// lifetime erasure is sound.
114    pub fn create_function<F, A, R>(&'scope self, func: F) -> Result<Function>
115    where
116        F: Fn(&Lua, A) -> Result<R> + 'scope,
117        A: FromLuaMulti,
118        R: IntoLuaMulti,
119    {
120        // The boxed callback borrows `'scope` data; erase the lifetime to the
121        // `'static` the callback machinery expects. Sound only because the
122        // destructor below drops the box before `'scope` data can die.
123        let boxed: Box<dyn Fn(&Lua, MultiValue) -> Result<MultiValue> + 'scope> =
124            Box::new(move |lua, args| {
125                let a = A::from_lua_multi(args, lua)?;
126                let r = func(lua, a)?;
127                r.into_lua_multi(lua)
128            });
129        let boxed: BoxedCallback = unsafe {
130            mem::transmute::<
131                Box<dyn Fn(&Lua, MultiValue) -> Result<MultiValue> + 'scope>,
132                BoxedCallback,
133            >(boxed)
134        };
135
136        let f = create_callback_function(&self.lua, boxed)?;
137
138        // Register the neutraliser: on scope exit, swap the upvalue's boxed
139        // closure for a `CallbackDestructed` sentinel and drop the original.
140        let f_for_dtor = f.clone();
141        self.destructors
142            .list
143            .borrow_mut()
144            .push(Box::new(move || destruct_callback(&f_for_dtor)));
145
146        Ok(f)
147    }
148
149    /// Wrap a non-`'static` mutable Rust closure into a callable Lua [`Function`]
150    /// that is invalidated when the scope ends.
151    ///
152    /// This is the scoped version of `create_function_mut`. The closure is
153    /// guarded by a [`RefCell`]; re-entrant calls (the callback triggering Lua
154    /// that calls the same callback) surface as a runtime error rather than a
155    /// borrow panic, matching mlua's `RecursiveMutCallback` intent.
156    pub fn create_function_mut<F, A, R>(&'scope self, func: F) -> Result<Function>
157    where
158        F: FnMut(&Lua, A) -> Result<R> + 'scope,
159        A: FromLuaMulti,
160        R: IntoLuaMulti,
161    {
162        let func = RefCell::new(func);
163        self.create_function(move |lua, args| {
164            let mut borrow = func
165                .try_borrow_mut()
166                .map_err(|_| Error::runtime("mutable callback called recursively"))?;
167            (borrow)(lua, args)
168        })
169    }
170
171    /// Create a Lua userdata wrapping a non-`'static` `T: UserData`, invalidated
172    /// when the scope ends.
173    ///
174    /// This is the scoped version of [`Lua::create_userdata`]: `T` need not be
175    /// `'static`, so it may borrow `'env` data. The trade-off (matching mlua) is
176    /// that the userdata carries no `TypeId`, so the value cannot be read back
177    /// out by concrete type from an [`AnyUserData`] handle — only metatable
178    /// method/field/meta dispatch is supported. After the scope ends, any access
179    /// from Lua errors with [`Error::UserDataDestructed`].
180    pub fn create_userdata<T>(&'scope self, data: T) -> Result<AnyUserData>
181    where
182        T: UserData + 'env,
183    {
184        // Erase `T`'s `'env` lifetime down to what the userdata machinery needs.
185        // Sound because the neutraliser drops `data` on scope exit (see module
186        // docs); the userdata never exposes the value back to Rust by type.
187        let (ud, neutralise) = create_scoped_userdata(&self.lua, data)?;
188        self.destructors.list.borrow_mut().push(neutralise);
189        Ok(ud)
190    }
191
192    /// Register an arbitrary destructor to run when the scope ends.
193    ///
194    /// Mirrors `mlua::Scope::add_destructor`. Useful for cleaning up resources
195    /// tied to the scope. Destructors run in reverse registration order on every
196    /// exit path (normal, `?`, or panic).
197    pub fn add_destructor(&'scope self, destructor: impl FnOnce() + 'env) {
198        // Erase `'env` to `'static`: the destructor runs before scope return, so
199        // any `'env` data it touches is still alive.
200        let destructor: Box<dyn FnOnce() + 'env> = Box::new(destructor);
201        let destructor: Box<dyn FnOnce()> =
202            unsafe { mem::transmute::<Box<dyn FnOnce() + 'env>, Box<dyn FnOnce()>>(destructor) };
203        self.destructors.list.borrow_mut().push(destructor);
204    }
205}
206
207impl Lua {
208    /// Create a [`Scope`] in which non-`'static` callbacks and userdata can be
209    /// created, borrowing data from the enclosing stack frame.
210    ///
211    /// Mirrors `mlua::Lua::scope`. Everything the scope creates is invalidated
212    /// when this method returns (on every exit path), so the borrows it held are
213    /// guaranteed to end before the borrowed data can. See [`Scope`] and the
214    /// [`scope` module docs](crate) for the soundness argument.
215    ///
216    /// ```
217    /// use luaur_rt::prelude::*;
218    /// use std::cell::Cell;
219    ///
220    /// let lua = Lua::new();
221    /// let counter = Cell::new(0);
222    /// lua.scope(|scope| {
223    ///     let f = scope.create_function(|_, ()| {
224    ///         counter.set(counter.get() + 1);
225    ///         Ok(())
226    ///     })?;
227    ///     f.call::<()>(())?;
228    ///     Ok(())
229    /// })
230    /// .unwrap();
231    /// assert_eq!(counter.get(), 1);
232    /// ```
233    pub fn scope<'env, R>(
234        &self,
235        f: impl for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> Result<R>,
236    ) -> Result<R> {
237        let scope = Scope::new(self.clone());
238        // `f` runs; on return (or unwind) `scope` drops, running all destructors
239        // via `Destructors::drop` — the invariant that makes the lifetime
240        // erasure sound. We materialise the result before `scope` is dropped.
241        f(&scope)
242    }
243}