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}