Skip to main content

luna_jit/
lua_facade.rs

1//! mlua-style `Lua` facade (B12, Phase 2 P2-D).
2//!
3//! A thin wrapper around [`luna_core::vm::Vm`] that exposes the same
4//! API in a shape familiar to embedders coming from `rlua` / `mlua`:
5//!
6//! ```
7//! use luna_jit::Lua;
8//!
9//! let mut lua = Lua::new();
10//! lua.open_base();
11//! lua.open_math();
12//! let r: i64 = lua.eval("return 1 + 2").unwrap();
13//! assert_eq!(r, 3);
14//!
15//! let add = lua.create_function(|a: i64, b: i64| -> i64 { a + b });
16//! lua.set_global("add", add).unwrap();
17//! let r: i64 = lua.eval("return add(40, 2)").unwrap();
18//! assert_eq!(r, 42);
19//! ```
20//!
21//! ## Handles
22//!
23//! [`LuaFunction`] / [`LuaTable`] / [`LuaRoot`] are `Copy` wrappers
24//! around a [`HostRootTicket`] returned by [`Vm::pin_host`]. They
25//! keep their referenced `Gc<T>` alive across calls (so a `LuaTable`
26//! survives a GC cycle even when no Lua-side reference exists).
27//!
28//! v1.3 Phase SR added slot recycling — a single handle can be
29//! released via [`Lua::unpin`]; the whole batch via
30//! [`Lua::unpin_all`]. Both operations bump the slot's generation,
31//! invalidating any further use of `LuaFunction` / `LuaTable` /
32//! `LuaRoot` `Copy` values that referenced the released slot
33//! (subsequent reads / calls panic on the stale ticket).
34//!
35//! ## Threading
36//!
37//! `Lua` inherits `Vm`'s `!Send + !Sync` contract. See
38//! [`docs/threading.md`](../../../../docs/threading.md) for canonical
39//! embedding patterns.
40
41use luna_core::runtime::Value;
42use luna_core::version::LuaVersion;
43use luna_core::vm::{
44    FromLuaValue, HostRootStale, HostRootTicket, IntoValue, LuaError, NativeTypedSig,
45    SandboxBuilder, Vm,
46};
47
48/// `mlua`-style front door for embedders. Wraps a [`Vm`] with JIT
49/// installed by default (`Vm::new_minimal_with_jit`).
50pub struct Lua(Vm);
51
52impl Lua {
53    /// Create a Lua VM with JIT installed + Lua 5.5 dialect.
54    pub fn new() -> Lua {
55        Lua(crate::new_minimal_with_jit(LuaVersion::Lua55))
56    }
57
58    /// Pick a specific dialect (5.1-5.5).
59    pub fn with_version(v: LuaVersion) -> Lua {
60        Lua(crate::new_minimal_with_jit(v))
61    }
62
63    /// Sandbox-mode builder — same as [`Vm::sandbox`] but doesn't
64    /// install JIT by default. `.build_lua()` finalizes to a `Lua`
65    /// wrapping the sandboxed `Vm`.
66    pub fn sandbox(v: LuaVersion) -> LuaSandboxBuilder {
67        LuaSandboxBuilder {
68            inner: Vm::sandbox(v),
69        }
70    }
71
72    /// Borrow the underlying `Vm` for direct access (escape hatch
73    /// for cases the facade doesn't cover).
74    pub fn vm(&mut self) -> &mut Vm {
75        &mut self.0
76    }
77
78    /// Open the base library (`print`, `type`, `pcall`, etc.).
79    pub fn open_base(&mut self) {
80        self.0.open_base();
81    }
82
83    /// Open the math library.
84    pub fn open_math(&mut self) {
85        self.0.open_math();
86    }
87
88    /// Open the string library.
89    pub fn open_string(&mut self) {
90        self.0.open_string();
91    }
92
93    /// Open the table library.
94    pub fn open_table(&mut self) {
95        self.0.open_table();
96    }
97
98    /// Open the coroutine library.
99    pub fn open_coroutine(&mut self) {
100        self.0.open_coroutine();
101    }
102
103    /// Compile and run `src`; extract the first return value as `T`.
104    /// Use [`Lua::eval_multi`] to retrieve all returns.
105    pub fn eval<T: FromLuaValue>(&mut self, src: &str) -> Result<T, LuaError> {
106        let mut r = self.0.eval(src)?;
107        if r.is_empty() {
108            T::from_lua_value(Value::Nil)
109        } else {
110            T::from_lua_value(r.remove(0))
111        }
112    }
113
114    /// Compile and run `src`; return all results.
115    pub fn eval_multi(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
116        self.0.eval(src)
117    }
118
119    /// Async variant of [`Lua::eval`]. Returns an `!Send` future that
120    /// drives the dispatcher with cooperative yields on instruction
121    /// budget exhaustion. Pin this to a `current_thread` Tokio
122    /// runtime (or a `LocalSet` inside multi-thread Tokio) — see
123    /// `docs/threading.md` and `examples/async_host.rs`.
124    pub async fn eval_async<T: FromLuaValue>(&mut self, src: &str) -> Result<T, LuaError> {
125        let mut r = self.0.eval_async(src).await?;
126        if r.is_empty() {
127            T::from_lua_value(Value::Nil)
128        } else {
129            T::from_lua_value(r.remove(0))
130        }
131    }
132
133    /// Async variant of [`Lua::eval_multi`].
134    pub async fn eval_async_multi(&mut self, src: &str) -> Result<Vec<Value>, LuaError> {
135        self.0.eval_async(src).await
136    }
137
138    /// Register an async native function callable from Lua. The
139    /// raw fn pointer ABI takes `(*mut Vm, func_slot, nargs)` and
140    /// returns a boxed future — see [`luna_core::vm::AsyncNativeFn`]
141    /// for the safety contract.
142    ///
143    /// Calling an async native from inside `vm.eval()` (sync mode)
144    /// errors with a typed `LuaError`; embedders must drive the call
145    /// through `eval_async`.
146    pub fn set_async_native(
147        &mut self,
148        name: &str,
149        f: luna_core::vm::AsyncNativeFn,
150    ) -> Result<(), LuaError> {
151        self.0.set_async_native(name, f)
152    }
153
154    /// Set a global by name. Accepts any [`IntoValue`] including
155    /// `LuaFunction` / `LuaTable` / `LuaRoot` (the handle types impl
156    /// `IntoValue` so they fan in alongside primitives + `Value`).
157    pub fn set_global<V: IntoValue>(&mut self, name: &str, v: V) -> Result<(), LuaError> {
158        self.0.set_global(name, v)
159    }
160
161    /// Borrow the globals table as a [`LuaTable`] handle.
162    pub fn globals(&mut self) -> LuaTable {
163        let g = self.0.globals();
164        let ticket = self.0.pin_host(Value::Table(g));
165        LuaTable { ticket }
166    }
167
168    /// Allocate a fresh empty table; return a handle that keeps it alive.
169    pub fn create_table(&mut self) -> LuaTable {
170        let t = self.0.new_table().build();
171        let ticket = self.0.pin_host(Value::Table(t));
172        LuaTable { ticket }
173    }
174
175    /// Wrap a typed Rust function as a Lua callable. See
176    /// [`Vm::native_typed`] for the supported callable shapes.
177    pub fn create_function<F, Marker>(&mut self, f: F) -> LuaFunction
178    where
179        F: NativeTypedSig<Marker>,
180    {
181        let v = self.0.native_typed(f);
182        let ticket = self.0.pin_host(v);
183        LuaFunction { ticket }
184    }
185
186    /// Pin an arbitrary value as a host root; the returned [`LuaRoot`]
187    /// keeps it alive until [`Lua::unpin`] or [`Lua::unpin_all`].
188    pub fn pin<V: IntoValue>(&mut self, v: V) -> LuaRoot {
189        let v = v.into_value(&mut self.0);
190        let ticket = self.0.pin_host(v);
191        LuaRoot { ticket }
192    }
193
194    /// Release a single pinned handle (v1.3 Phase SR). The handle's
195    /// slot is recycled; the supplied `LuaFunction` / `LuaTable` /
196    /// `LuaRoot` value (and any `Copy`-cloned aliases) becomes stale
197    /// and will panic on subsequent reads / calls.
198    ///
199    /// Returns `Err(HostRootStale)` if the handle was already
200    /// released — pool is unchanged in that case, so embedders can
201    /// safely ignore the error if double-unpin is acceptable.
202    pub fn unpin<H: PinnedHandle>(&mut self, h: H) -> Result<(), HostRootStale> {
203        self.0.unpin(h.ticket())
204    }
205
206    /// Drop every pinned handle. `LuaFunction` / `LuaTable` /
207    /// `LuaRoot` created before this call become invalid (panic on
208    /// use). Bumps every slot's generation; underlying `Vec` capacity
209    /// is retained for amortized future allocations.
210    pub fn unpin_all(&mut self) {
211        self.0.unpin_all();
212    }
213
214    /// Number of currently-pinned handles (diagnostic). v1.3 Phase
215    /// SR: counts live (non-free) slots, so a steady `pin → unpin`
216    /// loop holds at 1 instead of growing monotonically.
217    pub fn pinned_count(&self) -> usize {
218        self.0.host_root_count()
219    }
220}
221
222/// v1.3 Phase SR — common trait for handle types that wrap a
223/// [`HostRootTicket`]. Lets [`Lua::unpin`] accept `LuaFunction` /
224/// `LuaTable` / `LuaRoot` uniformly.
225pub trait PinnedHandle {
226    /// The ticket this handle wraps.
227    fn ticket(&self) -> HostRootTicket;
228}
229
230impl Default for Lua {
231    fn default() -> Self {
232        Lua::new()
233    }
234}
235
236/// Sandbox builder that finalizes to a `Lua` (instead of a bare `Vm`).
237pub struct LuaSandboxBuilder {
238    inner: SandboxBuilder,
239}
240
241impl LuaSandboxBuilder {
242    /// Whitelist the `base` standard library.
243    pub fn open_base(mut self) -> Self {
244        self.inner = self.inner.open_base();
245        self
246    }
247    /// Whitelist the `math` standard library.
248    pub fn open_math(mut self) -> Self {
249        self.inner = self.inner.open_math();
250        self
251    }
252    /// Whitelist the `string` standard library.
253    pub fn open_string(mut self) -> Self {
254        self.inner = self.inner.open_string();
255        self
256    }
257    /// Whitelist the `table` standard library.
258    pub fn open_table(mut self) -> Self {
259        self.inner = self.inner.open_table();
260        self
261    }
262    /// Whitelist the `coroutine` standard library.
263    pub fn open_coroutine(mut self) -> Self {
264        self.inner = self.inner.open_coroutine();
265        self
266    }
267    /// Cap interpreter instruction count per call (fires once, then trips).
268    pub fn with_instr_budget(mut self, n: i64) -> Self {
269        self.inner = self.inner.with_instr_budget(n);
270        self
271    }
272    /// Cap heap memory (approximate; see [`crate::vm::Vm::set_memory_cap`]).
273    pub fn with_memory_cap(mut self, n: usize) -> Self {
274        self.inner = self.inner.with_memory_cap(n);
275        self
276    }
277    /// Re-enable precompiled-bytecode loading (off by default in sandbox
278    /// mode for safety).
279    pub fn allow_bytecode_loading(mut self) -> Self {
280        self.inner = self.inner.allow_bytecode_loading();
281        self
282    }
283    /// Finalize the builder and return a configured [`Lua`].
284    pub fn build(self) -> Lua {
285        Lua(self.inner.build())
286    }
287}
288
289// ─────────────────────────────────────────────────────────────────────
290// Handle types
291// ─────────────────────────────────────────────────────────────────────
292
293/// Handle to a Lua-callable value (`Value::Closure` or
294/// `Value::Native`) pinned in the host root pool. `Copy`-able —
295/// clones share the same [`HostRootTicket`].
296///
297/// Becomes stale (panics on call) after [`Lua::unpin`] /
298/// [`Lua::unpin_all`].
299#[derive(Copy, Clone, Debug)]
300pub struct LuaFunction {
301    ticket: HostRootTicket,
302}
303
304impl LuaFunction {
305    /// The underlying [`HostRootTicket`]. Facade-author use only.
306    pub fn ticket(self) -> HostRootTicket {
307        self.ticket
308    }
309
310    /// Call this function with the given typed args; decode the
311    /// (first) return as `R`. Use [`LuaFunction::call_multi`] for
312    /// the full result vector.
313    pub fn call<A, R>(self, lua: &mut Lua, args: A) -> Result<R, LuaError>
314    where
315        A: IntoLuaArgs,
316        R: FromLuaValue,
317    {
318        let f = lua
319            .0
320            .read_host(self.ticket)
321            .expect("LuaFunction used after unpin / unpin_all");
322        let args = args.into_lua_args(&mut lua.0);
323        let mut r = lua.0.call_value(f, &args)?;
324        if r.is_empty() {
325            R::from_lua_value(Value::Nil)
326        } else {
327            R::from_lua_value(r.remove(0))
328        }
329    }
330
331    /// Call this function; return all results.
332    pub fn call_multi<A>(self, lua: &mut Lua, args: A) -> Result<Vec<Value>, LuaError>
333    where
334        A: IntoLuaArgs,
335    {
336        let f = lua
337            .0
338            .read_host(self.ticket)
339            .expect("LuaFunction used after unpin / unpin_all");
340        let args = args.into_lua_args(&mut lua.0);
341        lua.0.call_value(f, &args)
342    }
343}
344
345impl IntoValue for LuaFunction {
346    fn into_value(self, vm: &mut Vm) -> Value {
347        vm.read_host(self.ticket)
348            .expect("LuaFunction used after unpin / unpin_all")
349    }
350}
351
352impl PinnedHandle for LuaFunction {
353    fn ticket(&self) -> HostRootTicket {
354        self.ticket
355    }
356}
357
358/// Handle to a `Value::Table` pinned in the host root pool.
359#[derive(Copy, Clone, Debug)]
360pub struct LuaTable {
361    ticket: HostRootTicket,
362}
363
364impl LuaTable {
365    /// The underlying [`HostRootTicket`]. Facade-author use only.
366    pub fn ticket(self) -> HostRootTicket {
367        self.ticket
368    }
369
370    /// Set `t[k] = v`. Both `k` and `v` may be any [`IntoValue`].
371    pub fn set<K: IntoValue, V: IntoValue>(
372        self,
373        lua: &mut Lua,
374        k: K,
375        v: V,
376    ) -> Result<(), LuaError> {
377        let t = match lua
378            .0
379            .read_host(self.ticket)
380            .expect("LuaTable used after unpin / unpin_all")
381        {
382            Value::Table(t) => t,
383            _ => return Err(LuaError(Value::Nil)),
384        };
385        let k = k.into_value(&mut lua.0);
386        let v = v.into_value(&mut lua.0);
387        // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is
388        // single-threaded (see heap.rs:5-7).
389        unsafe { t.as_mut() }.set(&mut lua.0.heap, k, v)?;
390        lua.0
391            .heap
392            .barrier_back(t.as_ptr() as *mut luna_core::runtime::heap::GcHeader);
393        Ok(())
394    }
395
396    /// Read `t[k]`; decode as `V`. Returns `Err` if the key is
397    /// missing OR the value's type doesn't match `V`. Use
398    /// `t.raw_get(k)` (returning `Value`) for runtime branching.
399    pub fn get<K: IntoValue, V: FromLuaValue>(self, lua: &mut Lua, k: K) -> Result<V, LuaError> {
400        let v = self.raw_get(lua, k)?;
401        V::from_lua_value(v)
402    }
403
404    /// Read `t[k]` as a raw [`Value`] (no type coercion).
405    pub fn raw_get<K: IntoValue>(self, lua: &mut Lua, k: K) -> Result<Value, LuaError> {
406        let t = match lua
407            .0
408            .read_host(self.ticket)
409            .expect("LuaTable used after unpin / unpin_all")
410        {
411            Value::Table(t) => t,
412            _ => return Err(LuaError(Value::Nil)),
413        };
414        let k = k.into_value(&mut lua.0);
415        // SAFETY: see set() — same single-threaded GC contract.
416        Ok(unsafe { t.as_mut() }.get(k))
417    }
418}
419
420impl IntoValue for LuaTable {
421    fn into_value(self, vm: &mut Vm) -> Value {
422        vm.read_host(self.ticket)
423            .expect("LuaTable used after unpin / unpin_all")
424    }
425}
426
427impl PinnedHandle for LuaTable {
428    fn ticket(&self) -> HostRootTicket {
429        self.ticket
430    }
431}
432
433/// Generic pinned root. Use for arbitrary `Value`s the embedder
434/// wants to keep alive without wrapping in `LuaFunction` / `LuaTable`.
435#[derive(Copy, Clone, Debug)]
436pub struct LuaRoot {
437    ticket: HostRootTicket,
438}
439
440impl LuaRoot {
441    /// The underlying [`HostRootTicket`]. Facade-author use only.
442    pub fn ticket(self) -> HostRootTicket {
443        self.ticket
444    }
445
446    /// Read the pinned value. Panics if the handle was released.
447    pub fn get(self, lua: &Lua) -> Value {
448        lua.0
449            .read_host(self.ticket)
450            .expect("LuaRoot used after unpin / unpin_all")
451    }
452}
453
454impl IntoValue for LuaRoot {
455    fn into_value(self, vm: &mut Vm) -> Value {
456        vm.read_host(self.ticket)
457            .expect("LuaRoot used after unpin / unpin_all")
458    }
459}
460
461impl PinnedHandle for LuaRoot {
462    fn ticket(&self) -> HostRootTicket {
463        self.ticket
464    }
465}
466
467// ─────────────────────────────────────────────────────────────────────
468// IntoLuaArgs — tuple to &[Value] conversion for LuaFunction::call
469// ─────────────────────────────────────────────────────────────────────
470
471/// Convert a tuple of typed values into the `&[Value]` shape
472/// [`Vm::call_value`] expects. Implemented for `()` + tuples of
473/// [`IntoValue`] up to arity 6.
474pub trait IntoLuaArgs {
475    /// Encode `self` (a tuple of [`IntoValue`] implementors) as a flat
476    /// argument list ready for [`LuaFunction::call`].
477    fn into_lua_args(self, vm: &mut Vm) -> Vec<Value>;
478}
479
480impl IntoLuaArgs for () {
481    fn into_lua_args(self, _vm: &mut Vm) -> Vec<Value> {
482        Vec::new()
483    }
484}
485
486macro_rules! impl_into_lua_args_tuple {
487    ( $( ($($name:ident: $idx:tt),+) ),+ $(,)? ) => {
488        $(
489            impl<$($name: IntoValue),+> IntoLuaArgs for ($($name,)+) {
490                fn into_lua_args(self, vm: &mut Vm) -> Vec<Value> {
491                    vec![ $( self.$idx.into_value(vm), )+ ]
492                }
493            }
494        )+
495    };
496}
497impl_into_lua_args_tuple! {
498    (T0: 0),
499    (T0: 0, T1: 1),
500    (T0: 0, T1: 1, T2: 2),
501    (T0: 0, T1: 1, T2: 2, T3: 3),
502    (T0: 0, T1: 1, T2: 2, T3: 3, T4: 4),
503    (T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5),
504}