From f8a2eb79d4b12c7d227d3dd2f2bdebb36b73e256 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 21 Jan 2023 20:11:17 -0500 Subject: [PATCH] Implement task library & test suite, mostly --- src/lib/globals/console.rs | 2 +- src/lib/globals/fs.rs | 2 +- src/lib/globals/mod.rs | 2 - src/lib/globals/net.rs | 2 +- src/lib/globals/process.rs | 2 +- src/lib/globals/task.rs | 80 +++++++++-------------- src/lib/lib.rs | 13 ++-- src/lib/luau/task.luau | 112 +++++++++++++++++++++++++++++++++ src/lib/utils/table_builder.rs | 15 ++--- src/tests/task/defer.luau | 10 ++- src/tests/task/delay.luau | 14 +++-- src/tests/task/spawn.luau | 8 ++- src/tests/task/wait.luau | 11 ++++ 13 files changed, 188 insertions(+), 85 deletions(-) create mode 100644 src/lib/luau/task.luau diff --git a/src/lib/globals/console.rs b/src/lib/globals/console.rs index 7322f9e..e030a29 100644 --- a/src/lib/globals/console.rs +++ b/src/lib/globals/console.rs @@ -5,7 +5,7 @@ use crate::utils::{ table_builder::ReadonlyTableBuilder, }; -pub fn new(lua: &Lua) -> Result { +pub async fn new(lua: &Lua) -> Result
{ let print = |args: &MultiValue, throw: bool| -> Result<()> { let s = pretty_format_multi_value(args)?; if throw { diff --git a/src/lib/globals/fs.rs b/src/lib/globals/fs.rs index 88580e0..9afd785 100644 --- a/src/lib/globals/fs.rs +++ b/src/lib/globals/fs.rs @@ -5,7 +5,7 @@ use tokio::fs; use crate::utils::table_builder::ReadonlyTableBuilder; -pub fn new(lua: &Lua) -> Result
{ +pub async fn new(lua: &Lua) -> Result
{ ReadonlyTableBuilder::new(lua)? .with_async_function("readFile", fs_read_file)? .with_async_function("readDir", fs_read_dir)? diff --git a/src/lib/globals/mod.rs b/src/lib/globals/mod.rs index 18515f9..4ecc1c5 100644 --- a/src/lib/globals/mod.rs +++ b/src/lib/globals/mod.rs @@ -9,5 +9,3 @@ pub use fs::new as new_fs; pub use net::new as new_net; pub use process::new as new_process; pub use task::new as new_task; - -pub use task::WaitingThread as WaitingTaskThread; diff --git a/src/lib/globals/net.rs b/src/lib/globals/net.rs index 40ea446..2fea6e3 100644 --- a/src/lib/globals/net.rs +++ b/src/lib/globals/net.rs @@ -8,7 +8,7 @@ use reqwest::{ use crate::utils::{net::get_request_user_agent_header, table_builder::ReadonlyTableBuilder}; -pub fn new(lua: &Lua) -> Result
{ +pub async fn new(lua: &Lua) -> Result
{ ReadonlyTableBuilder::new(lua)? .with_function("jsonEncode", net_json_encode)? .with_function("jsonDecode", net_json_decode)? diff --git a/src/lib/globals/process.rs b/src/lib/globals/process.rs index 268d13c..314c190 100644 --- a/src/lib/globals/process.rs +++ b/src/lib/globals/process.rs @@ -9,7 +9,7 @@ use tokio::process::Command; use crate::utils::table_builder::ReadonlyTableBuilder; -pub fn new(lua: &Lua, args_vec: Vec) -> Result
{ +pub async fn new(lua: &Lua, args_vec: Vec) -> Result
{ // Create readonly args array let inner_args = lua.create_table()?; for arg in &args_vec { diff --git a/src/lib/globals/task.rs b/src/lib/globals/task.rs index d0c0e57..8acda69 100644 --- a/src/lib/globals/task.rs +++ b/src/lib/globals/task.rs @@ -1,60 +1,40 @@ -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use std::{thread::sleep, time::Duration}; -use mlua::{Function, Lua, Result, Table, Thread, Value, Variadic}; -use tokio::time; +use mlua::{Function, Lua, Result, Table, Value}; use crate::utils::table_builder::ReadonlyTableBuilder; const DEFAULT_SLEEP_DURATION: f32 = 1.0 / 60.0; -#[allow(dead_code)] -pub struct WaitingThread<'a> { - is_delayed_for: Option, - is_deferred: Option, - thread: Thread<'a>, - args: Variadic>, -} +const TASK_LIB_LUAU: &str = include_str!("../luau/task.luau"); -pub fn new<'a>(lua: &'a Lua, _threads: &Arc>>>) -> Result> { - // TODO: Figure out how to insert into threads vec +pub async fn new(lua: &Lua) -> Result
{ + let task_lib: Table = lua + .load(TASK_LIB_LUAU) + .set_name("task")? + .eval_async() + .await?; + // FUTURE: Properly implementing the task library in async rust is + // very complicated but should be done at some point, for now we will + // fall back to implementing only task.wait and doing the rest in lua + let task_cancel: Function = task_lib.raw_get("cancel")?; + let task_defer: Function = task_lib.raw_get("defer")?; + let task_delay: Function = task_lib.raw_get("delay")?; + let task_spawn: Function = task_lib.raw_get("spawn")?; ReadonlyTableBuilder::new(lua)? - .with_function("cancel", |lua, thread: Thread| { - thread.reset(lua.create_function(|_, _: ()| Ok(()))?)?; - Ok(()) - })? - .with_async_function( - "defer", - |lua, (func, args): (Function, Variadic)| async move { - let thread = lua.create_thread(func)?; - thread.into_async(args).await?; - Ok(()) - }, - )? - .with_async_function( - "delay", - |lua, (duration, func, args): (Option, Function, Variadic)| async move { - let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION); - time::sleep(Duration::from_secs_f32(secs)).await; - let thread = lua.create_thread(func)?; - thread.into_async(args).await?; - Ok(()) - }, - )? - .with_async_function( - "spawn", - |lua, (func, args): (Function, Variadic)| async move { - let thread = lua.create_thread(func)?; - thread.into_async(args).await?; - Ok(()) - }, - )? - .with_async_function("wait", |_, duration: Option| async move { - let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION); - time::sleep(Duration::from_secs_f32(secs)).await; - Ok(secs) - })? + .with_value("cancel", Value::Function(task_cancel))? + .with_value("defer", Value::Function(task_defer))? + .with_value("delay", Value::Function(task_delay))? + .with_value("spawn", Value::Function(task_spawn))? + .with_function("wait", wait)? .build() } + +// FIXME: It does seem possible to properly make an async wait +// function with mlua right now, something breaks when using +// async wait functions inside of coroutines +fn wait(_: &Lua, duration: Option) -> Result { + let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION); + sleep(Duration::from_secs_f32(secs)); + Ok(secs) +} diff --git a/src/lib/lib.rs b/src/lib/lib.rs index 46c4a92..4318a40 100644 --- a/src/lib/lib.rs +++ b/src/lib/lib.rs @@ -1,5 +1,3 @@ -use std::sync::{Arc, Mutex}; - use anyhow::{bail, Result}; use mlua::Lua; @@ -13,16 +11,15 @@ use crate::{ pub async fn run_lune(name: &str, chunk: &str, args: Vec) -> Result<()> { let lua = Lua::new(); - let threads = Arc::new(Mutex::new(Vec::new())); lua.sandbox(true)?; // Add in all globals { let globals = lua.globals(); - globals.raw_set("console", new_console(&lua)?)?; - globals.raw_set("fs", new_fs(&lua)?)?; - globals.raw_set("net", new_net(&lua)?)?; - globals.raw_set("process", new_process(&lua, args.clone())?)?; - globals.raw_set("task", new_task(&lua, &threads)?)?; + globals.raw_set("console", new_console(&lua).await?)?; + globals.raw_set("fs", new_fs(&lua).await?)?; + globals.raw_set("net", new_net(&lua).await?)?; + globals.raw_set("process", new_process(&lua, args.clone()).await?)?; + globals.raw_set("task", new_task(&lua).await?)?; globals.set_readonly(true); } // Run the requested chunk asynchronously diff --git a/src/lib/luau/task.luau b/src/lib/luau/task.luau new file mode 100644 index 0000000..b598086 --- /dev/null +++ b/src/lib/luau/task.luau @@ -0,0 +1,112 @@ +local MINIMUM_DELAY_TIME = 1 / 100 + +type ThreadOrFunction = thread | (A...) -> R... +type AnyThreadOrFunction = ThreadOrFunction<...any, ...any> + +type WaitingThreadKind = "Normal" | "Deferred" | "Delayed" +type WaitingThread = { + idx: number, + kind: WaitingThreadKind, + thread: thread, + args: { [number]: any, n: number }, +} + +local waitingThreadCounter = 0 +local waitingThreads: { WaitingThread } = {} + +local function scheduleWaitingThreads() + -- Grab currently waiting threads and clear the queue but keep capacity + local threadsToResume: { WaitingThread } = table.clone(waitingThreads) + table.clear(waitingThreads) + table.sort(threadsToResume, function(t0, t1) + local k0: WaitingThreadKind = t0.kind + local k1: WaitingThreadKind = t1.kind + if k0 == k1 then + return t0.idx < t1.idx + end + if k0 == "Normal" then + return true + elseif k1 == "Normal" then + return false + elseif k0 == "Deferred" then + return true + else + return false + end + end) + -- Resume threads in order, giving args & waiting if necessary + for _, waitingThread in threadsToResume do + coroutine.resume( + waitingThread.thread, + table.unpack(waitingThread.args, 1, waitingThread.args.n) + ) + end +end + +local function insertWaitingThread(kind: WaitingThreadKind, tof: AnyThreadOrFunction, ...: any) + if typeof(tof) ~= "thread" and typeof(tof) ~= "function" then + if tof == nil then + error("Expected thread or function, got nil", 3) + end + error( + string.format("Expected thread or function, got %s %s", typeof(tof), tostring(tof)), + 3 + ) + end + local thread = if type(tof) == "function" then coroutine.create(tof) else tof + waitingThreadCounter += 1 + local waitingThread: WaitingThread = { + idx = waitingThreadCounter, + kind = kind, + thread = thread, + args = table.pack(...), + } + table.insert(waitingThreads, waitingThread) + return waitingThread +end + +local function cancel(thread: unknown) + if typeof(thread) ~= "thread" then + if thread == nil then + error("Expected thread, got nil", 2) + end + error(string.format("Expected thread, got %s %s", typeof(thread), tostring(thread)), 2) + else + coroutine.close(thread) + end +end + +local function defer(tof: AnyThreadOrFunction, ...: any): thread + local waiting = insertWaitingThread("Deferred", tof, ...) + local original = waiting.thread + waiting.thread = coroutine.create(function(...) + task.wait(1 / 1_000_000) + coroutine.resume(original, ...) + end) + scheduleWaitingThreads() + return waiting.thread +end + +local function delay(delay: number?, tof: AnyThreadOrFunction, ...: any): thread + local waiting = insertWaitingThread("Delayed", tof, ...) + local original = waiting.thread + waiting.thread = coroutine.create(function(...) + task.wait(math.max(MINIMUM_DELAY_TIME, delay or 0)) + coroutine.resume(original, ...) + end) + scheduleWaitingThreads() + return waiting.thread +end + +local function spawn(tof: AnyThreadOrFunction, ...: any): thread + local waiting = insertWaitingThread("Normal", tof, ...) + scheduleWaitingThreads() + return waiting.thread +end + +return { + cancel = cancel, + defer = defer, + delay = delay, + spawn = spawn, +} diff --git a/src/lib/utils/table_builder.rs b/src/lib/utils/table_builder.rs index 8fb3a45..9bbfb8d 100644 --- a/src/lib/utils/table_builder.rs +++ b/src/lib/utils/table_builder.rs @@ -18,9 +18,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> { Ok(self) } - pub fn with_table(self, key: &'static str, value: Table) -> Result { - self.tab.raw_set(key, value)?; - Ok(self) + pub fn with_table(self, key: &'static str, table: Table) -> Result { + self.with_value(key, Value::Table(table)) } pub fn with_function(self, key: &'static str, func: F) -> Result @@ -29,9 +28,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> { R: ToLuaMulti<'lua>, F: 'static + Fn(&'lua Lua, A) -> Result, { - let value = self.lua.create_function(func)?; - self.tab.raw_set(key, value)?; - Ok(self) + let f = self.lua.create_function(func)?; + self.with_value(key, Value::Function(f)) } pub fn with_async_function(self, key: &'static str, func: F) -> Result @@ -41,9 +39,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> { F: 'static + Fn(&'lua Lua, A) -> FR, FR: 'lua + Future>, { - let value = self.lua.create_async_function(func)?; - self.tab.raw_set(key, value)?; - Ok(self) + let f = self.lua.create_async_function(func)?; + self.with_value(key, Value::Function(f)) } pub fn build(self) -> Result> { diff --git a/src/tests/task/defer.luau b/src/tests/task/defer.luau index 92ac060..11e164e 100644 --- a/src/tests/task/defer.luau +++ b/src/tests/task/defer.luau @@ -4,16 +4,20 @@ local flag: boolean = false task.defer(function() flag = true end) -assert(not flag, "Defer should run after other threads, including the main thread") +assert(not flag, "Defer should not run instantly or block") +task.wait(1 / 60) +assert(flag, "Defer should run") -- Deferred functions should work with yielding local flag2: boolean = false task.defer(function() - task.wait() + task.wait(1 / 60) flag2 = true end) -assert(not flag2, "Defer should work with yielding") +assert(not flag2, "Defer should work with yielding (1)") +task.wait(1 / 30) +assert(flag2, "Defer should work with yielding (2)") -- Deferred functions should run after other spawned threads local flag3: boolean = false diff --git a/src/tests/task/delay.luau b/src/tests/task/delay.luau index 4c849f4..b5c9f2c 100644 --- a/src/tests/task/delay.luau +++ b/src/tests/task/delay.luau @@ -4,20 +4,22 @@ local flag: boolean = false task.delay(0, function() flag = true end) -assert(not flag, "Delay should never run instantly") +assert(not flag, "Delay should not run instantly or block") +task.wait(1 / 60) +assert(flag, "Delay should run after the wanted duration") -- Delayed functions should work with yielding local flag2: boolean = false task.delay(0.2, function() flag2 = true - task.wait(0.2) + task.wait(0.4) flag2 = false end) -task.wait(0.25) -assert(flag2, "Delay should work with yielding") -task.wait(0.25) -assert(not flag2, "Delay should work with yielding") +task.wait(0.4) +assert(flag, "Delay should work with yielding (1)") +task.wait(0.4) +assert(not flag2, "Delay should work with yielding (2)") -- Varargs should get passed correctly diff --git a/src/tests/task/spawn.luau b/src/tests/task/spawn.luau index c3fb550..99cb7bd 100644 --- a/src/tests/task/spawn.luau +++ b/src/tests/task/spawn.luau @@ -4,16 +4,18 @@ local flag: boolean = false task.spawn(function() flag = true end) -assert(flag, "Spawn should run instantly until yielded") +assert(flag, "Spawn should run instantly") -- Spawned functions should work with yielding local flag2: boolean = false task.spawn(function() - task.wait() + task.wait(0.1) flag2 = true end) -assert(not flag2, "Spawn should work with yielding") +assert(not flag2, "Spawn should work with yielding (1)") +task.wait(0.2) +assert(flag2, "Spawn should work with yielding (2)") -- Varargs should get passed correctly diff --git a/src/tests/task/wait.luau b/src/tests/task/wait.luau index 333e375..7de1913 100644 --- a/src/tests/task/wait.luau +++ b/src/tests/task/wait.luau @@ -1,3 +1,14 @@ +-- Wait should work everywhere + +local flag: boolean = false +coroutine.wrap(function() + task.wait(0.1) + flag = true +end)() +assert(flag, "Wait failed while in a coroutine") + +-- Wait should be accurate + local DEFAULT = 1 / 60 local EPSILON = 1 / 100