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}