Skip to main content

mlua_isle/
handle.rs

1//! Isle — the public handle for interacting with the Lua thread.
2
3use crate::error::IsleError;
4use crate::hook::CancelToken;
5use crate::task::Task;
6use crate::thread;
7use crate::Request;
8use std::sync::mpsc;
9use std::sync::Mutex;
10use std::thread::JoinHandle;
11
12/// Handle to a thread-isolated Lua VM.
13///
14/// `Isle` owns the communication channel and the join handle for the
15/// Lua thread.  All operations are thread-safe (`Isle: Send + Sync`).
16///
17/// # Lifecycle
18///
19/// 1. [`Isle::spawn`] creates the Lua VM on a dedicated thread.
20/// 2. Use [`eval`](Isle::eval), [`call`](Isle::call), or [`exec`](Isle::exec)
21///    to run code.
22/// 3. [`shutdown`](Isle::shutdown) sends a graceful stop signal and
23///    joins the thread.
24///
25/// If the `Isle` is dropped without calling `shutdown`, the channel
26/// disconnects and the Lua thread exits on its next receive attempt.
27#[must_use = "use .shutdown() for clean thread join; dropping without shutdown leaks the thread"]
28pub struct Isle {
29    tx: mpsc::Sender<Request>,
30    join: Mutex<Option<JoinHandle<()>>>,
31}
32
33impl Isle {
34    /// Spawn a new Lua VM on a dedicated thread.
35    ///
36    /// The `init` closure runs on the Lua thread before any requests
37    /// are processed.  Use it to register globals, install mlua-pkg
38    /// resolvers, load mlua-batteries, etc.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`IsleError::Init`] if the init closure fails.
43    pub fn spawn<F>(init: F) -> Result<Self, IsleError>
44    where
45        F: FnOnce(&mlua::Lua) -> Result<(), mlua::Error> + Send + 'static,
46    {
47        let (tx, rx) = mpsc::channel::<Request>();
48        let (init_tx, init_rx) = mpsc::channel::<Result<(), IsleError>>();
49
50        let join = std::thread::Builder::new()
51            .name("mlua-isle".into())
52            .spawn(move || {
53                let lua = mlua::Lua::new();
54                match init(&lua) {
55                    Ok(()) => {
56                        let _ = init_tx.send(Ok(()));
57                        thread::run_loop(lua, rx);
58                    }
59                    Err(e) => {
60                        let _ = init_tx.send(Err(IsleError::Init(e.to_string())));
61                    }
62                }
63            })
64            .map_err(|e| IsleError::Init(format!("thread spawn failed: {e}")))?;
65
66        // Wait for init to complete
67        init_rx
68            .recv()
69            .map_err(|e| IsleError::Init(format!("init channel closed: {e}")))??;
70
71        Ok(Self {
72            tx,
73            join: Mutex::new(Some(join)),
74        })
75    }
76
77    /// Evaluate a Lua chunk (blocking).
78    ///
79    /// Returns the result as a string.  Equivalent to
80    /// `spawn_eval(code).wait()`.
81    pub fn eval(&self, code: &str) -> Result<String, IsleError> {
82        self.spawn_eval(code).wait()
83    }
84
85    /// Evaluate a Lua chunk, returning a cancellable [`Task`].
86    pub fn spawn_eval(&self, code: &str) -> Task {
87        let cancel = CancelToken::new();
88        let (resp_tx, resp_rx) = mpsc::channel();
89
90        let req = Request::Eval {
91            code: code.to_string(),
92            cancel: cancel.clone(),
93            tx: resp_tx,
94        };
95
96        if self.tx.send(req).is_err() {
97            // Channel closed — return a task that immediately errors
98            let (err_tx, err_rx) = mpsc::channel();
99            let _ = err_tx.send(Err(IsleError::Shutdown));
100            return Task::new(err_rx, cancel);
101        }
102
103        Task::new(resp_rx, cancel)
104    }
105
106    /// Call a named global Lua function with string arguments (blocking).
107    pub fn call(&self, func: &str, args: &[&str]) -> Result<String, IsleError> {
108        self.spawn_call(func, args).wait()
109    }
110
111    /// Call a named global Lua function, returning a cancellable [`Task`].
112    pub fn spawn_call(&self, func: &str, args: &[&str]) -> Task {
113        let cancel = CancelToken::new();
114        let (resp_tx, resp_rx) = mpsc::channel();
115
116        let req = Request::Call {
117            func: func.to_string(),
118            args: args.iter().map(|s| s.to_string()).collect(),
119            cancel: cancel.clone(),
120            tx: resp_tx,
121        };
122
123        if self.tx.send(req).is_err() {
124            let (err_tx, err_rx) = mpsc::channel();
125            let _ = err_tx.send(Err(IsleError::Shutdown));
126            return Task::new(err_rx, cancel);
127        }
128
129        Task::new(resp_rx, cancel)
130    }
131
132    /// Execute an arbitrary closure on the Lua thread (blocking).
133    ///
134    /// The closure receives `&Lua` and can perform any operation.
135    /// This is the escape hatch for complex interactions that don't
136    /// fit into `eval` or `call`.
137    ///
138    /// **Note:** The cancel hook only fires during Lua instruction
139    /// execution.  If the closure blocks in Rust code (e.g. HTTP
140    /// calls, file I/O), cancellation will not take effect until
141    /// control returns to the Lua VM.
142    pub fn exec<F>(&self, f: F) -> Result<String, IsleError>
143    where
144        F: FnOnce(&mlua::Lua) -> Result<String, IsleError> + Send + 'static,
145    {
146        self.spawn_exec(f).wait()
147    }
148
149    /// Execute a closure on the Lua thread, returning a cancellable [`Task`].
150    pub fn spawn_exec<F>(&self, f: F) -> Task
151    where
152        F: FnOnce(&mlua::Lua) -> Result<String, IsleError> + Send + 'static,
153    {
154        let cancel = CancelToken::new();
155        let (resp_tx, resp_rx) = mpsc::channel();
156
157        let req = Request::Exec {
158            f: Box::new(f),
159            cancel: cancel.clone(),
160            tx: resp_tx,
161        };
162
163        if self.tx.send(req).is_err() {
164            let (err_tx, err_rx) = mpsc::channel();
165            let _ = err_tx.send(Err(IsleError::Shutdown));
166            return Task::new(err_rx, cancel);
167        }
168
169        Task::new(resp_rx, cancel)
170    }
171
172    /// Graceful shutdown: signal the Lua thread to exit and join it.
173    ///
174    /// After shutdown, all subsequent requests will return
175    /// [`IsleError::Shutdown`].
176    pub fn shutdown(self) -> Result<(), IsleError> {
177        let _ = self.tx.send(Request::Shutdown);
178        let handle = self.join.lock().map_err(|_| IsleError::ThreadPanic)?.take();
179        if let Some(join) = handle {
180            join.join().map_err(|_| IsleError::ThreadPanic)?;
181        }
182        Ok(())
183    }
184
185    /// Check if the Lua thread is still alive.
186    pub fn is_alive(&self) -> bool {
187        self.join
188            .lock()
189            .ok()
190            .and_then(|guard| guard.as_ref().map(|j| !j.is_finished()))
191            .unwrap_or(false)
192    }
193}
194
195impl Drop for Isle {
196    fn drop(&mut self) {
197        // Send shutdown signal; ignore errors (channel may already be closed)
198        let _ = self.tx.send(Request::Shutdown);
199        // Don't join on drop — let the thread exit on its own.
200        // Use explicit shutdown() for a clean join.
201    }
202}