Skip to main content

kevy_lua/
pool.rs

1//! The per-dialect VM pool behind [`Bridge`]: which slot a dialect
2//! lives in, how a sandbox VM is built on first use, and how the
3//! bridge reports itself.
4
5use crate::{Bridge, DISPATCH_KEY, DispatchSlot, cjson, cmsgpack, host};
6use luna_core::version::LuaVersion;
7use luna_core::vm::exec::Vm;
8use std::rc::Rc;
9
10pub(crate) fn dialect_slot(v: LuaVersion) -> usize {
11    match v {
12        LuaVersion::Lua51 => 0,
13        LuaVersion::Lua52 => 1,
14        LuaVersion::Lua53 => 2,
15        LuaVersion::Lua54 => 3,
16        LuaVersion::MacroLua => 4,
17        LuaVersion::Lua55 => 5,
18    }
19}
20
21impl Bridge {
22    /// Number of dialect VMs currently spawned. Test-only helper —
23    /// production code doesn't need to inspect the pool.
24    #[cfg(test)]
25    fn vm_count(&self) -> usize {
26        self.vms.iter().filter(|s| s.is_some()).count()
27    }
28
29    /// Lazily build the sandbox Vm for `version`. Conservative
30    /// default: base + math + string + table libraries, no JIT,
31    /// no bytecode loading, 200M instruction budget (~5 s on modern
32    /// hardware — Redis's default `lua-time-limit`). The `redis`
33    /// host table is installed once at Vm-construction time; KEYS /
34    /// ARGV are re-bound per `eval` call (see [`Bridge::eval`]).
35    pub(crate) fn vm_for(&mut self, version: LuaVersion) -> &mut Vm {
36        let slot = &mut self.vms[dialect_slot(version)];
37        if slot.is_none() {
38            let mut builder =
39                Vm::sandbox(version).open_base().open_math().open_string().open_table();
40            if self.instr_budget > 0 {
41                builder = builder.with_instr_budget(self.instr_budget);
42            }
43            let mut vm = builder.build();
44            host::install_redis_table(&mut vm);
45            // BullMQ + Sidekiq Pro require the `cmsgpack` global.
46            cmsgpack::install_cmsgpack(&mut vm);
47            cjson::install_cjson(&mut vm);
48            // Install the dispatch handle as a userdata global.
49            // `redis.call` retrieves it via
50            // `vm.userdata_borrow::<DispatchSlot>(DISPATCH_KEY)`. We
51            // clone the Rc so each Vm holds an independent handle
52            // pointing at the shared closure.
53            let _ = vm.set_userdata(
54                DISPATCH_KEY,
55                DispatchSlot {
56                    f: Rc::clone(&self.dispatch),
57                    read_only: Rc::clone(&self.read_only),
58                },
59            );
60            *slot = Some(vm);
61        }
62        slot.as_mut().expect("just-inserted Vm")
63    }
64}
65
66pub(crate) fn format_lua_error(e: &luna_core::vm::error::LuaError) -> String {
67    // luna-core impls `Display for LuaError` — embedders don't
68    // need to case-split on the inner Value type.
69    format!("{e}")
70}
71
72pub(crate) fn version_tag(v: LuaVersion) -> &'static str {
73    match v {
74        LuaVersion::Lua51 => "5.1",
75        LuaVersion::Lua52 => "5.2",
76        LuaVersion::Lua53 => "5.3",
77        LuaVersion::Lua54 => "5.4",
78        LuaVersion::MacroLua => "macro",
79        LuaVersion::Lua55 => "5.5",
80    }
81}
82
83impl core::fmt::Debug for Bridge {
84    /// Reports the bridge's configuration and how much of it is live,
85    /// without touching the VMs or the dispatch closure.
86    ///
87    /// Neither can be printed: luna-core's `Vm` has no `Debug`, and
88    /// `dispatch` is an `Rc<dyn Fn>` with no identity worth showing. The
89    /// VMs are reported as a count of spawned slots, which is the thing
90    /// worth knowing about them from outside — whether a dialect has been
91    /// used yet.
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        f.debug_struct("Bridge")
94            .field("vms_spawned", &self.vms.iter().filter(|v| v.is_some()).count())
95            .field("read_only", &self.read_only.get())
96            .field("instr_budget", &self.instr_budget)
97            .field("allow", &self.allow)
98            .field("cached_scripts", &self.script_cache.len())
99            .finish_non_exhaustive()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use crate::{Bridge, FlushMode};
106
107    #[test]
108    fn eval_reuses_vm_across_calls() {
109        let mut b = Bridge::with_no_dispatch();
110        assert_eq!(b.eval(b"return 1", &[], &[]), b":1\r\n");
111        assert_eq!(b.eval(b"return 2", &[], &[]), b":2\r\n");
112        // One VM should be cached for the 5.1 default dialect.
113        assert_eq!(b.vm_count(), 1);
114    }
115
116    #[test]
117    fn script_flush_drops_vm_pool() {
118        let mut b = Bridge::with_no_dispatch();
119        let _ = b.eval(b"return 1", &[], &[]);
120        assert_eq!(b.vm_count(), 1);
121        b.script_flush(FlushMode::Sync);
122        assert_eq!(b.vm_count(), 0);
123    }
124}