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