diff --git a/Cargo.lock b/Cargo.lock index 303c4d6..906cd06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 4.0.3", + "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -21,7 +21,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" dependencies = [ - "async-lock 3.3.0", + "async-lock", "async-task", "concurrent-queue", "fastrand", @@ -35,7 +35,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd1f344136bad34df1f83a47f3fd7f2ab85d75cb8a940af4ccf6d482a84ea01b" dependencies = [ - "async-lock 3.3.0", + "async-lock", "blocking", "futures-lite", ] @@ -46,7 +46,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744" dependencies = [ - "async-lock 3.3.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", @@ -59,73 +59,17 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener 4.0.3", + "event-listener", "event-listener-strategy", "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c1cd5d253ecac3d3cf15e390fd96bd92a13b1d14497d81abf077304794fb04" -dependencies = [ - "async-channel", - "async-io", - "async-lock 3.3.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 4.0.3", - "futures-lite", - "rustix", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-signal" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" -dependencies = [ - "async-io", - "async-lock 2.8.0", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.48.0", -] - [[package]] name = "async-task" version = "4.7.0" @@ -157,7 +101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", - "async-lock 3.3.0", + "async-lock", "async-task", "fastrand", "futures-io", @@ -206,6 +150,15 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "erased-serde" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d05712b2d8d88102bc9868020c9e5c7a1f5527c452b9b97450a1d006140ba7" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.8" @@ -216,12 +169,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "4.0.3" @@ -239,7 +186,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.3", + "event-listener", "pin-project-lite", ] @@ -337,12 +284,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d3561f79659ff3afad7b25e2bf2ec21507fe601ebecb7f81088669ec4bfd51e" dependencies = [ "bstr", + "erased-serde", "futures-util", "libloading", "mlua-sys", "num-traits", "once_cell", "rustc-hash", + "serde", + "serde-value", +] + +[[package]] +name = "mlua-luau-runtime" +version = "0.0.0" +dependencies = [ + "async-executor", + "async-fs", + "async-io", + "concurrent-queue", + "event-listener", + "futures-lite", + "mlua", ] [[package]] @@ -372,6 +335,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "parking" version = "2.2.0" @@ -467,6 +439,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.195" @@ -478,15 +460,6 @@ dependencies = [ "syn", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.9" @@ -496,33 +469,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "smol" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e635339259e51ef85ac7aa29a1cd991b957047507288697a690e80ab97d07cad" -dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock 3.3.0", - "async-net", - "async-process", - "blocking", - "futures-lite", -] - -[[package]] -name = "smol-mlua" -version = "0.0.0" -dependencies = [ - "concurrent-queue", - "event-listener 4.0.3", - "mlua", - "smol", -] - [[package]] name = "syn" version = "2.0.48" diff --git a/Cargo.toml b/Cargo.toml index 8e283f3..e75bc90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,28 @@ [package] -name = "smol-mlua" +name = "mlua-luau-runtime" version = "0.0.0" edition = "2021" -[dependencies] -concurrent-queue = "2.4" -event-listener = "4.0" -smol = "2.0" -mlua = { version = "0.9", features = ["luau", "luau-jit", "async"] } - [lib] path = "lib/lib.rs" +[dependencies] +async-executor = "1.8" +concurrent-queue = "2.4" +event-listener = "4.0" +futures-lite = "2.2" + +mlua = { version = "0.9.5", features = [ + "luau", + "luau-jit", + "async", + "serialize", +] } + +[dev-dependencies] +async-fs = "2.1" +async-io = "2.3" + [[example]] name = "basic_sleep" test = true diff --git a/README.md b/README.md index c0d4fd0..6f7e1a2 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,22 @@ -

smol-mlua

+

mlua-luau-runtime

- - CI status + + CI status - - Crate license + + Crate license

-Integration between [smol] and [mlua] that provides a fully functional and asynchronous Luau runtime using smol executor(s). - -[smol]: https://crates.io/crates/smol -[mlua]: https://crates.io/crates/mlua +Luau-based async runtime for [`mlua`](https://crates.io/crates/mlua), built on top of [`async-executor`](https://crates.io/crates/async-executor). ## Example Usage @@ -27,10 +24,13 @@ Integration between [smol] and [mlua] that provides a fully functional and async ```rs use std::time::{Duration, Instant}; +use std::io::ErrorKind; + +use async_io::{block_on, Timer}; +use async_fs::read_to_string; use mlua::prelude::*; -use smol::{Timer, io, fs::read_to_string} -use smol_mlua::Runtime; +use mlua_luau_runtime::*; ``` ### 2. Set up lua environment @@ -55,7 +55,7 @@ lua.globals().set( let task = lua.spawn(async move { match read_to_string(path).await { Ok(s) => Ok(Some(s)), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(None), Err(e) => Err(e), } }); @@ -64,7 +64,7 @@ lua.globals().set( )?; ``` -### 3. Run +### 3. Set up runtime, run threads ```rs let rt = Runtime::new(&lua)?; @@ -77,7 +77,6 @@ let fileThread = lua.load("readFile(\"Cargo.toml\")"); rt.spawn_thread(sleepThread, ()); rt.spawn_thread(fileThread, ()); -// ... and run either async or blocking, until they finish -rt.run_async().await; -rt.run_blocking(); +// ... and run until they finish +block_on(rt.run()); ``` diff --git a/examples/basic_sleep.rs b/examples/basic_sleep.rs index 52f376d..95d3e6b 100644 --- a/examples/basic_sleep.rs +++ b/examples/basic_sleep.rs @@ -1,8 +1,9 @@ use std::time::{Duration, Instant}; +use async_io::{block_on, Timer}; + use mlua::prelude::*; -use smol::Timer; -use smol_mlua::Runtime; +use mlua_luau_runtime::*; const MAIN_SCRIPT: &str = include_str!("./lua/basic_sleep.luau"); @@ -18,11 +19,13 @@ pub fn main() -> LuaResult<()> { })?, )?; - // Load the main script into a runtime and run it until completion + // Load the main script into a runtime let rt = Runtime::new(&lua)?; let main = lua.load(MAIN_SCRIPT); rt.spawn_thread(main, ())?; - rt.run_blocking(); + + // Run until completion + block_on(rt.run()); Ok(()) } diff --git a/examples/basic_spawn.rs b/examples/basic_spawn.rs index a8b1ad9..bc7ec9e 100644 --- a/examples/basic_spawn.rs +++ b/examples/basic_spawn.rs @@ -1,6 +1,10 @@ +use std::io::ErrorKind; + +use async_fs::read_to_string; +use async_io::block_on; + use mlua::prelude::*; -use smol::{fs::read_to_string, io}; -use smol_mlua::{LuaSpawnExt, Runtime}; +use mlua_luau_runtime::*; const MAIN_SCRIPT: &str = include_str!("./lua/basic_spawn.luau"); @@ -14,7 +18,7 @@ pub fn main() -> LuaResult<()> { let task = lua.spawn(async move { match read_to_string(path).await { Ok(s) => Ok(Some(s)), - Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(None), Err(e) => Err(e), } }); @@ -22,11 +26,13 @@ pub fn main() -> LuaResult<()> { })?, )?; - // Load the main script into a runtime and run it until completion + // Load the main script into a runtime let rt = Runtime::new(&lua)?; let main = lua.load(MAIN_SCRIPT); rt.spawn_thread(main, ())?; - rt.run_blocking(); + + // Run until completion + block_on(rt.run()); Ok(()) } diff --git a/examples/callbacks.rs b/examples/callbacks.rs index 0de42a0..2dfae07 100644 --- a/examples/callbacks.rs +++ b/examples/callbacks.rs @@ -1,5 +1,7 @@ use mlua::prelude::*; -use smol_mlua::Runtime; +use mlua_luau_runtime::*; + +use async_io::block_on; const MAIN_SCRIPT: &str = include_str!("./lua/callbacks.luau"); @@ -17,10 +19,12 @@ pub fn main() -> LuaResult<()> { ); }); - // Load and run the main script until completion + // Load the main script into a runtime let main = lua.load(MAIN_SCRIPT); rt.spawn_thread(main, ())?; - rt.run_blocking(); + + // Run until completion + block_on(rt.run()); Ok(()) } diff --git a/examples/lots_of_threads.rs b/examples/lots_of_threads.rs index d4d17bd..120e97b 100644 --- a/examples/lots_of_threads.rs +++ b/examples/lots_of_threads.rs @@ -1,8 +1,9 @@ use std::time::Duration; +use async_io::{block_on, Timer}; + use mlua::prelude::*; -use smol::Timer; -use smol_mlua::Runtime; +use mlua_luau_runtime::*; const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau"); @@ -30,10 +31,12 @@ pub fn main() -> LuaResult<()> { })?, )?; - // Load the main script into the runtime and run it until completion + // Load the main script into the runtime let main = lua.load(MAIN_SCRIPT); rt.spawn_thread(main, ())?; - rt.run_blocking(); + + // Run until completion + block_on(rt.run()); Ok(()) } diff --git a/examples/scheduler_ordering.rs b/examples/scheduler_ordering.rs index 6aa5b61..2b96617 100644 --- a/examples/scheduler_ordering.rs +++ b/examples/scheduler_ordering.rs @@ -1,8 +1,9 @@ use std::time::{Duration, Instant}; +use async_io::{block_on, Timer}; + use mlua::prelude::*; -use smol::Timer; -use smol_mlua::Runtime; +use mlua_luau_runtime::*; const MAIN_SCRIPT: &str = include_str!("./lua/scheduler_ordering.luau"); @@ -23,10 +24,12 @@ pub fn main() -> LuaResult<()> { })?, )?; - // Load the main script into a runtime and run it until completion + // Load the main script into a runtime let main = lua.load(MAIN_SCRIPT); rt.spawn_thread(main, ())?; - rt.run_blocking(); + + // Run until completion + block_on(rt.run()); Ok(()) } diff --git a/lib/error_callback.rs b/lib/error_callback.rs index 6a16b86..0e60908 100644 --- a/lib/error_callback.rs +++ b/lib/error_callback.rs @@ -1,42 +1,32 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; +use std::{cell::RefCell, rc::Rc}; use mlua::prelude::*; -use smol::lock::Mutex; type ErrorCallback = Box; #[derive(Clone)] pub(crate) struct ThreadErrorCallback { - exists: Arc, - inner: Arc>>, + inner: Rc>>, } impl ThreadErrorCallback { pub fn new() -> Self { Self { - exists: Arc::new(AtomicBool::new(false)), - inner: Arc::new(Mutex::new(None)), + inner: Rc::new(RefCell::new(None)), } } pub fn replace(&self, callback: impl Fn(LuaError) + Send + 'static) { - self.exists.store(true, Ordering::Relaxed); - self.inner.lock_blocking().replace(Box::new(callback)); + self.inner.borrow_mut().replace(Box::new(callback)); } pub fn clear(&self) { - self.exists.store(false, Ordering::Relaxed); - self.inner.lock_blocking().take(); + self.inner.borrow_mut().take(); } pub fn call(&self, error: &LuaError) { - if self.exists.load(Ordering::Relaxed) { - if let Some(cb) = &*self.inner.lock_blocking() { - cb(error.clone()); - } + if let Some(cb) = &*self.inner.borrow() { + cb(error.clone()); } } } diff --git a/lib/runtime.rs b/lib/runtime.rs index b049481..94e3379 100644 --- a/lib/runtime.rs +++ b/lib/runtime.rs @@ -1,10 +1,9 @@ use std::sync::{Arc, Weak}; -use std::time::Duration; +use futures_lite::prelude::*; use mlua::prelude::*; -use smol::{prelude::*, Timer}; -use smol::{block_on, Executor, LocalExecutor}; +use async_executor::{Executor, LocalExecutor}; use super::{ error_callback::ThreadErrorCallback, queue::ThreadQueue, traits::IntoLuaThread, @@ -142,54 +141,57 @@ impl<'lua> Runtime<'lua> { Runs the runtime until all Lua threads have completed. Note that the given Lua state must be the same one that was - used to create this runtime, otherwise this method may panic. + used to create this runtime, otherwise this method will panic. */ - pub async fn run_async(&self) { - // Make sure we do not already have an executor - this is a definite user error - // and may happen if the user tries to run multiple runtimes on the same lua state - if self.lua.app_data_ref::>().is_some() { - panic!( - "Lua state already has an executor attached!\ - \nOnly one runtime can be used per lua state." - ); - } + pub async fn run(&self) { + /* + Create new executors to use - note that we do not need create multiple executors + for work stealing, the user may do that themselves if they want to and it will work + just fine, as long as anything async is .await-ed from within a lua async function. - // Create new executors to use - note that we do not need to create multiple executors - // for work stealing, using the `spawn` global function that smol provides will work - // just fine, as long as anything spawned by it is awaited from lua async functions + The main purpose of the two executors here is just to have one with + the Send bound, and another (local) one without it, for lua scheduling. + + We also use the main executor to drive the main loop below forward, + saving a tiny bit of processing from going on the lua executor itself. + */ let lua_exec = LocalExecutor::new(); let main_exec = Arc::new(Executor::new()); - // Store the main executor in lua for spawner trait + /* + Store the main executor in lua, so that it may be used with LuaSpawnExt. + + Also ensure we do not already have an executor - this is a definite user error + and may happen if the user tries to run multiple runtimes on the same lua state. + */ + if self.lua.app_data_ref::>().is_some() { + panic!( + "Lua state already has an executor attached!\ + \nThis may be caused by running multiple runtimes on the same lua state, or a call to Runtime::run being cancelled.\ + \nOnly one runtime can be used per lua state at once, and runtimes must always run until completion." + ); + } self.lua.set_app_data(Arc::downgrade(&main_exec)); - // Create a timer for a resumption cycle / throttling mechanism, waiting on this - // will allow us to batch more work together when the runtime is under high load, - // and adds an acceptable amount of latency for new async tasks (we run at 250hz) - let mut cycle = Timer::interval(Duration::from_millis(4)); + /* + Manually tick the lua executor, while running under the main executor. + Each tick we wait for the next action to perform, in prioritized order: - // Tick local lua executor while also driving main - // executor forward, until all lua threads finish + 1. A lua thread is available to run on the spawned queue + 2. A lua thread is available to run on the deferred queue + 3. Task(s) scheduled on the lua executor have made progress and should be polled again + + This ordering is vital to ensure that we don't accidentally exit the main loop + when there are new lua threads to enqueue and potentially more work to be done. + */ let fut = async { loop { - // Wait for a new thread to arrive __or__ next futures step, prioritizing - // new threads, so we don't accidentally exit when there is more work to do - let fut_spawn = self.queue_spawn.wait_for_item(); - let fut_defer = self.queue_defer.wait_for_item(); - let fut_tick = async { - lua_exec.tick().await; - // Do as much work as possible - loop { - if !lua_exec.try_tick() { - break; - } - } - }; + let fut_spawn = self.queue_spawn.wait_for_item(); // 1 + let fut_defer = self.queue_defer.wait_for_item(); // 2 + let fut_tick = lua_exec.tick(); // 3 fut_spawn.or(fut_defer).or(fut_tick).await; - // If a new thread was spawned onto any queue, - // we must drain them and schedule on the executor let process_thread = |thread: LuaThread<'lua>, args| { // NOTE: Thread may have been cancelled from lua // before we got here, so we need to check it again @@ -199,12 +201,11 @@ impl<'lua> Runtime<'lua> { .spawn(async move { // Only run stream until first coroutine.yield or completion. We will // drop it right away to clear stack space since detached tasks dont drop - // until the executor drops https://github.com/smol-rs/smol/issues/294 + // until the executor drops (https://github.com/smol-rs/smol/issues/294) let res = stream.next().await.unwrap(); if let Err(e) = &res { self.error_callback.call(e); } - // TODO: Figure out how to give this result to caller of spawn_thread/defer_thread }) .detach(); } @@ -218,28 +219,17 @@ impl<'lua> Runtime<'lua> { process_thread(thread, args); } - // Empty executor = no remaining threads + // Empty executor = we didn't spawn any new lua tasks + // above, and there are no remaining tasks to run later if lua_exec.is_empty() { break; } - - // Wait for next resumption cycle - cycle.next().await; } }; main_exec.run(fut).await; - // Make sure we don't leave any references behind + // Clean up self.lua.remove_app_data::>(); } - - /** - Runs the runtime until all Lua threads have completed, blocking the thread. - - See [`ThreadRuntime::run_async`] for more info. - */ - pub fn run_blocking(&self) { - block_on(self.run_async()) - } } diff --git a/lib/traits.rs b/lib/traits.rs index 95e7f52..6cfc2dc 100644 --- a/lib/traits.rs +++ b/lib/traits.rs @@ -1,7 +1,8 @@ use std::{future::Future, sync::Weak}; use mlua::prelude::*; -use smol::{Executor, Task}; + +use async_executor::{Executor, Task}; /** Trait for any struct that can be turned into an [`LuaThread`] @@ -62,8 +63,10 @@ pub trait LuaSpawnExt<'lua> { ### Example usage ```rust + use async_io::block_on; + use mlua::prelude::*; - use smol_mlua::{Runtime, LuaSpawnExt}; + use mlua_luau_runtime::*; fn main() -> LuaResult<()> { let lua = Lua::new(); @@ -80,7 +83,7 @@ pub trait LuaSpawnExt<'lua> { let rt = Runtime::new(&lua)?; rt.spawn_thread(lua.load("spawnBackgroundTask()"), ()); - rt.run_blocking(); + block_on(rt.run()); Ok(()) }