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}