Thread-isolated Lua VM with cancellation and async bridge for mlua.
mlua::Lua is !Send — it cannot cross thread boundaries. This makes it
difficult to use from async runtimes, UI threads, or any multi-threaded context.
mlua-isle solves this by confining the Lua VM to a dedicated thread and communicating via channels.
- Thread isolation — Lua VM runs on a dedicated thread; callers interact
via a
Send + Synchandle - Cancellation — long-running Lua code can be interrupted via
CancelTokenusing a Lua debug hook - Sync API — blocking
Islehandle withTaskfor non-blocking usage - Async API (optional,
tokiofeature) —AsyncIslehandle with Handle/Driver separation, bounded channel backpressure, andAsyncTask<T>which implementsFuture - Coroutine execution (optional,
tokiofeature) — cooperative multitasking viacoroutine_eval/coroutine_call. Multiple Lua coroutines share the same VM and yield when awaiting async Rust functions - Connection pool (optional,
poolfeature) —IslePoolmanages multipleIsleinstances with checkout/return semantics. SupportsCold(fresh VM) andWarm(reuse) strategies - Async connection pool (optional,
pool+tokiofeatures) —AsyncIslePoolis the async counterpart ofIslePool:checkout/try_checkout/checkout_timeoutare async, idle wait usestokio::sync::Notify, and each slot owns both anAsyncIslehandle and itsAsyncIsleDriverso VMs can be joined on shutdown - Zero unsafe in user code — both
IsleandAsyncIsleare safe to share across threads
┌─────────────────┐ std mpsc ┌──────────────────┐
│ caller thread │──────────►│ Lua thread │
│ │ │ (mlua confined) │
│ Isle handle │◄──────────│ │
│ │ oneshot │ Lua VM + hook │
└─────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ tokio tasks │ tokio mpsc │ Lua thread │
│ │──────────────►│ (mlua confined) │
│ AsyncIsle handle │ (bounded, │ │
│ (Clone, no Arc) │ backpressure)│ Lua VM + hook │
│ │◄──────────────│ │
│ │ oneshot │ │
├──────────────────┤ │ │
│ AsyncIsleDriver │───done_tx────►│ │
│ (lifecycle owner)│ └──────────────────┘
└──────────────────┘
- Handle (
AsyncIsle) — lightweight, cloneable. Share across tasks withoutArc. - Driver (
AsyncIsleDriver) — sole lifecycle owner. Callshutdown().awaitfor clean thread join, or drop to let the channel-close mechanism terminate the thread naturally.
Add to your Cargo.toml:
[dependencies]
mlua-isle = "0.4"
# For async support (includes coroutine execution):
# mlua-isle = { version = "0.4", features = ["tokio"] }
# For connection pool:
# mlua-isle = { version = "0.4", features = ["pool"] }
# Both:
# mlua-isle = { version = "0.4", features = ["tokio", "pool"] }use mlua_isle::Isle;
let isle = Isle::spawn(|lua| {
lua.globals().set("greeting", "hello")?;
Ok(())
}).unwrap();
let result: String = isle.eval("return greeting").unwrap();
assert_eq!(result, "hello");
isle.shutdown().unwrap();# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
use mlua_isle::AsyncIsle;
let (isle, driver) = AsyncIsle::spawn(|lua| {
lua.globals().set("greeting", "hello")?;
Ok(())
}).await?;
// Clone freely — no Arc needed.
let isle2 = isle.clone();
let result: String = isle.eval("return greeting").await?;
assert_eq!(result, "hello");
driver.shutdown().await?;
# Ok(())
# }# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
use mlua_isle::AsyncIsle;
let (isle, driver) = AsyncIsle::spawn(|lua| {
// Register an async Rust function — coroutines yield here
lua.globals().set("sleep_ms", lua.create_async_function(|_, ms: u64| async move {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
Ok(())
})?)?;
Ok(())
}).await?;
// Multiple coroutines share the same VM cooperatively
let t1 = isle.spawn_coroutine_eval("sleep_ms(10) return 'a'");
let t2 = isle.spawn_coroutine_eval("sleep_ms(10) return 'b'");
let (r1, r2) = tokio::join!(t1, t2);
assert!(r1.is_ok());
assert!(r2.is_ok());
driver.shutdown().await?;
# Ok(())
# }use mlua_isle::{IslePool, PoolConfig, PoolStrategy};
let pool = IslePool::new(
|lua| {
lua.globals().set("greeting", "hello")?;
Ok(())
},
PoolConfig {
max_size: 4,
strategy: PoolStrategy::Warm,
},
).unwrap();
{
let isle = pool.checkout().unwrap();
let result: String = isle.eval("return greeting").unwrap();
assert_eq!(result, "hello");
} // isle returned to pool automatically
pool.shutdown();use mlua_isle::Isle;
use std::time::Duration;
use std::thread;
let isle = Isle::spawn(|_| Ok(())).unwrap();
let task = isle.spawn_eval("while true do end");
thread::sleep(Duration::from_millis(50));
task.cancel();
let result = task.wait();
assert!(result.is_err()); // IsleError::Cancelled# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
use mlua_isle::AsyncIsle;
use std::time::Duration;
let (isle, driver) = AsyncIsle::spawn(|_lua| Ok(())).await?;
let task = isle.spawn_eval("while true do end");
let token = task.cancel_token().clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
token.cancel();
});
let result = task.await; // Err(Cancelled)
assert!(result.is_err());
driver.shutdown().await?;
# Ok(())
# }| Method | Description |
|---|---|
Isle::spawn(init) |
Create a Lua VM on a dedicated thread |
isle.eval(code) |
Evaluate a Lua chunk (blocking) |
isle.call(func, args) |
Call a global Lua function (blocking) |
isle.exec(closure) |
Run an arbitrary closure on the Lua thread |
isle.spawn_eval(code) |
Non-blocking eval, returns a Task |
isle.spawn_call(func, args) |
Non-blocking call, returns a Task |
isle.spawn_exec(closure) |
Non-blocking exec, returns a Task |
isle.shutdown() |
Graceful shutdown and thread join |
task.wait() |
Block until the task completes |
task.cancel() |
Cancel the running task |
| Method | Description |
|---|---|
AsyncIsle::spawn(init) |
Create a Lua VM, returns (AsyncIsle, AsyncIsleDriver) |
AsyncIsle::builder() |
Configure channel capacity / thread name |
isle.eval(code) |
Evaluate a Lua chunk (async, exclusive) |
isle.call(func, args) |
Call a global Lua function (async, exclusive) |
isle.exec(closure) |
Run a closure on the Lua thread (async, exclusive) |
isle.coroutine_eval(code) |
Evaluate as a cooperative coroutine |
isle.coroutine_call(func, args) |
Call a function as a cooperative coroutine |
isle.spawn_eval(code) |
Returns a cancellable AsyncTask |
isle.spawn_call(func, args) |
Returns a cancellable AsyncTask |
isle.spawn_exec(closure) |
Returns a cancellable AsyncTask |
isle.spawn_coroutine_eval(code) |
Coroutine eval, returns AsyncTask |
isle.spawn_coroutine_call(func, args) |
Coroutine call, returns AsyncTask |
driver.shutdown().await |
Graceful shutdown (drains pending coroutines) |
task.cancel() |
Cancel the running task |
task.cancel_token() |
Access the CancelToken for sharing |
| Method | Description |
|---|---|
IslePool::new(factory, config) |
Create a pool with factory closure |
pool.checkout() |
Checkout an Isle (blocks until available) |
pool.try_checkout() |
Non-blocking checkout, returns None at capacity |
pool.checkout_timeout(dur) |
Checkout with timeout |
pool.active() |
Number of currently checked-out Isles |
pool.idle() |
Number of idle Isles |
pool.shutdown() |
Shut down all idle Isles |
pooled.kill() |
Mark Isle for disposal on drop |
Rust 1.77 or later.
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.