Start work on require overhaul

This commit is contained in:
Filip Tibell 2023-03-20 13:52:18 +01:00
parent 1876a25922
commit 6318176516
No known key found for this signature in database
5 changed files with 249 additions and 148 deletions

View file

@ -8,6 +8,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Builtin modules such as `fs`, `net` and others can now be imported using `require("@lune/fs")`, `require("@lune/net")` ..
### Removed
- Removed option to preserve default Luau require behavior
## `0.5.6` - March 11th, 2023
### Added

View file

@ -1,49 +1,51 @@
use std::{
env::{self, current_dir},
fs,
cell::RefCell,
collections::HashMap,
env::current_dir,
path::{self, PathBuf},
};
use dunce::canonicalize;
use mlua::prelude::*;
use tokio::{fs, sync::oneshot};
use crate::lua::table::TableBuilder;
use crate::lua::{
table::TableBuilder,
task::{TaskScheduler, TaskSchedulerScheduleExt},
};
const REQUIRE_IMPL_LUA: &str = r#"
local source = info(1, "s")
if source == '[string "require"]' then
source = info(2, "s")
end
local absolute, relative = paths(source, ...)
if loaded[absolute] ~= true then
local first, second = load(absolute, relative)
if first == nil or second ~= nil then
error("Module did not return exactly one value")
end
loaded[absolute] = true
cache[absolute] = first
return first
else
return cache[absolute]
end
local absolute, relative = importer:paths(source, ...)
return importer:load(thread(), absolute, relative)
"#;
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
// Preserve original require behavior if we have a special env var set,
// returning an empty table since there are no globals to overwrite
if env::var_os("LUAU_PWD_REQUIRE").is_some() {
return TableBuilder::new(lua)?.build_readonly();
#[derive(Debug, Clone, Default)]
struct Importer<'lua> {
builtins: HashMap<String, LuaMultiValue<'lua>>,
cached: RefCell<HashMap<String, LuaResult<LuaMultiValue<'lua>>>>,
pwd: String,
}
// Store the current pwd, and make the functions for path conversions & loading a file
let mut require_pwd = current_dir()?.to_string_lossy().to_string();
if !require_pwd.ends_with(path::MAIN_SEPARATOR) {
require_pwd = format!("{require_pwd}{}", path::MAIN_SEPARATOR)
impl<'lua> Importer<'lua> {
pub fn new() -> Self {
let mut pwd = current_dir()
.expect("Failed to access current working directory")
.to_string_lossy()
.to_string();
if !pwd.ends_with(path::MAIN_SEPARATOR) {
pwd = format!("{pwd}{}", path::MAIN_SEPARATOR)
}
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
let require_error: LuaFunction = lua.named_registry_value("error")?;
let require_get_abs_rel_paths = lua
.create_function(
|_, (require_pwd, require_source, require_path): (String, String, String)| {
Self {
pwd,
..Default::default()
}
}
fn paths(&self, require_source: String, require_path: String) -> LuaResult<(String, String)> {
if require_path.starts_with('@') {
return Ok((require_path.clone(), require_path));
}
@ -69,69 +71,115 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
}
};
let absolute = file_path.to_string_lossy().to_string();
let relative = absolute.trim_start_matches(&require_pwd).to_string();
let relative = absolute.trim_start_matches(&self.pwd).to_string();
Ok((absolute, relative))
},
)?
.bind(require_pwd)?;
// Note that file loading must be blocking to guarantee the require cache works, if it
// were async then one lua script may require a module during the file reading process
let require_get_loaded_file = lua.create_function(
|lua: &Lua, (path_absolute, path_relative): (String, String)| {
// Check if we got a special require path starting with an @
if path_absolute == path_relative && path_absolute.starts_with('@') {
return match path_absolute {
p if p.starts_with("@lune/") => {
let _module_name = p.strip_prefix("@lune/").unwrap();
// TODO: Return builtin module
Err(LuaError::RuntimeError(
"Builtin require paths prefixed by '@' are not yet supported"
.to_string(),
))
}
_ => Err(LuaError::RuntimeError(
"Custom require paths prefixed by '@' are not yet supported".to_string(),
)),
};
fn load_builtin(&self, module_name: &str) -> LuaResult<LuaMultiValue> {
match self.builtins.get(module_name) {
Some(module) => Ok(module.clone()),
None => Err(LuaError::RuntimeError(format!(
"No builtin module exists with the name '{}'",
module_name
))),
}
// Use a name without extensions for loading the chunk, the
// above code assumes the require path is without extensions
let path_relative_no_extension = path_relative
.trim_end_matches(".lua")
.trim_end_matches(".luau");
}
async fn load_file(
&self,
lua: &'lua Lua,
absolute_path: String,
relative_path: String,
) -> LuaResult<LuaMultiValue> {
let cached = { self.cached.borrow().get(&absolute_path).cloned() };
match cached {
Some(cached) => cached,
None => {
// Try to read the wanted file, note that we use bytes instead of reading
// to a string since lua scripts are not necessarily valid utf-8 strings
match fs::read(path_absolute) {
Ok(contents) => lua
let contents = fs::read(&absolute_path).await.map_err(LuaError::external)?;
// Use a name without extensions for loading the chunk, some
// other code assumes the require path is without extensions
let path_relative_no_extension = relative_path
.trim_end_matches(".lua")
.trim_end_matches(".luau");
// Load the file into a thread
let loaded_func = lua
.load(&contents)
.set_name(path_relative_no_extension)?
.eval::<LuaValue>(),
Err(e) => Err(LuaError::external(e)),
.into_function()?;
let loaded_thread = lua.create_thread(loaded_func)?;
// Run the thread and provide a channel that will
// then get its result received when it finishes
let (tx, rx) = oneshot::channel();
{
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
sched.set_task_result_sender(task, tx);
}
// Wait for the thread to finish running, cache + return our result
let rets = rx.await.expect("Sender was dropped during require");
self.cached.borrow_mut().insert(absolute_path, rets.clone());
rets
}
}
}
},
)?;
/*
We need to get the source file where require was
called to be able to do path-relative requires,
so we make a small wrapper to do that here, this
will then call our actual async require function
This must be done in lua because due to how our
scheduler works mlua can not preserve debug info
*/
async fn load(
&self,
lua: &'lua Lua,
absolute_path: String,
relative_path: String,
) -> LuaResult<LuaMultiValue> {
if absolute_path == relative_path && absolute_path.starts_with('@') {
if let Some(module_name) = absolute_path.strip_prefix("@lune/") {
self.load_builtin(module_name)
} else {
Err(LuaError::RuntimeError(
"Require paths prefixed by '@' are not yet supported".to_string(),
))
}
} else {
self.load_file(lua, absolute_path, relative_path).await
}
}
}
impl<'i> LuaUserData for Importer<'i> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_method(
"paths",
|_, this, (require_source, require_path): (String, String)| {
this.paths(require_source, require_path)
},
);
methods.add_method(
"load",
|lua, this, (thread, absolute_path, relative_path): (LuaThread, String, String)| {
// TODO: Make this work
// this.load(lua, absolute_path, relative_path)
Ok(())
},
);
}
}
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
let require_importer = Importer::new();
let require_thread: LuaFunction = lua.named_registry_value("co.thread")?;
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
let require_env = TableBuilder::new(lua)?
.with_value("loaded", lua.create_table()?)?
.with_value("cache", lua.create_table()?)?
.with_value("importer", require_importer)?
.with_value("thread", require_thread)?
.with_value("info", require_info)?
.with_value("error", require_error)?
.with_value("paths", require_get_abs_rel_paths)?
.with_value("load", require_get_loaded_file)?
.build_readonly()?;
let require_fn_lua = lua
.load(REQUIRE_IMPL_LUA)
.set_name("require")?
.set_environment(require_env)?
.into_function()?;
TableBuilder::new(lua)?
.with_value("require", require_fn_lua)?
.build_readonly()

View file

@ -158,9 +158,9 @@ fn coroutine_resume<'lua>(
let result = match value {
LuaThreadOrTaskReference::Thread(t) => {
let task = sched.create_task(TaskKind::Instant, t, None, true)?;
sched.resume_task(task, None)
sched.resume_task(task)
}
LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t, None),
LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t),
};
sched.force_set_current_task(Some(current));
match result {
@ -187,7 +187,7 @@ fn coroutine_wrap<'lua>(lua: &'lua Lua, func: LuaFunction) -> LuaResult<LuaFunct
let result = lua
.app_data_ref::<&TaskScheduler>()
.unwrap()
.resume_task(task, Some(Ok(args)));
.resume_task_override(task, Ok(args));
sched.force_set_current_task(Some(current));
result
})

View file

@ -87,10 +87,16 @@ fn resume_next_blocking_task(
task
} {
None => TaskSchedulerState::new(scheduler),
Some(task) => match scheduler.resume_task(task, override_args) {
Some(task) => match override_args {
Some(args) => match scheduler.resume_task_override(task, args) {
Ok(_) => TaskSchedulerState::new(scheduler),
Err(task_err) => TaskSchedulerState::err(scheduler, task_err),
},
None => match scheduler.resume_task(task) {
Ok(_) => TaskSchedulerState::new(scheduler),
Err(task_err) => TaskSchedulerState::err(scheduler, task_err),
},
},
}
}

View file

@ -9,11 +9,12 @@ use std::{
use futures_util::{future::LocalBoxFuture, stream::FuturesUnordered, Future};
use mlua::prelude::*;
use tokio::sync::{mpsc, Mutex as AsyncMutex};
use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex};
use super::scheduler_message::TaskSchedulerMessage;
pub use super::{task_kind::TaskKind, task_reference::TaskReference};
type TaskResultSender = oneshot::Sender<LuaResult<LuaMultiValue<'static>>>;
type TaskFutureRets<'fut> = LuaResult<Option<LuaMultiValue<'fut>>>;
type TaskFuture<'fut> = LocalBoxFuture<'fut, (Option<TaskReference>, TaskFutureRets<'fut>)>;
@ -48,6 +49,7 @@ pub struct TaskScheduler<'fut> {
pub(super) tasks_count: Cell<usize>,
pub(super) tasks_current: Cell<Option<TaskReference>>,
pub(super) tasks_queue_blocking: RefCell<VecDeque<TaskReference>>,
pub(super) tasks_result_senders: RefCell<HashMap<TaskReference, TaskResultSender>>,
pub(super) tasks_current_lua_error: Arc<RefCell<Option<LuaError>>>,
// Future tasks & objects for waking
pub(super) futures: AsyncMutex<FuturesUnordered<TaskFuture<'fut>>>,
@ -77,6 +79,7 @@ impl<'fut> TaskScheduler<'fut> {
tasks_count: Cell::new(0),
tasks_current: Cell::new(None),
tasks_queue_blocking: RefCell::new(VecDeque::new()),
tasks_result_senders: RefCell::new(HashMap::new()),
tasks_current_lua_error,
futures: AsyncMutex::new(FuturesUnordered::new()),
futures_tx: tx,
@ -270,6 +273,8 @@ impl<'fut> TaskScheduler<'fut> {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// Remove any sender
self.tasks_result_senders.borrow_mut().remove(task_ref);
// NOTE: We need to close the thread here to
// make 100% sure that nothing can resume it
let close: LuaFunction = self.lua.named_registry_value("co.close")?;
@ -291,10 +296,60 @@ impl<'fut> TaskScheduler<'fut> {
This will be a no-op if the task no longer exists.
*/
pub fn resume_task<'a>(
pub fn resume_task(&self, reference: TaskReference) -> LuaResult<LuaMultiValue> {
// Fetch and check if the task was removed, if it got
// removed it means it was intentionally cancelled
let task = {
let mut tasks = self.tasks.borrow_mut();
match tasks.remove(&reference) {
Some(task) => task,
None => return Ok(LuaMultiValue::new()),
}
};
// Decrement the corresponding task counter
match task.kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// Fetch and remove the thread to resume + its arguments
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
let thread_args: Option<LuaMultiValue> = {
self.lua
.registry_value::<Option<Vec<LuaValue>>>(&task.args)
.expect("Failed to get stored args for task")
.map(LuaMultiValue::from_vec)
};
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
// We got everything we need and our references
// were cleaned up properly, resume the thread
self.tasks_current.set(Some(reference));
let rets = match thread_args {
Some(args) => thread.resume(args),
None => thread.resume(()),
};
self.tasks_current.set(None);
// If we have a result sender for this task, we should run it if the thread finished
if thread.status() != LuaThreadStatus::Resumable {
if let Some(sender) = self.tasks_result_senders.borrow_mut().remove(&reference) {
let _ = sender.send(rets.clone());
}
}
rets
}
/**
Resumes a task, if the task still exists in the scheduler, using the given arguments.
A task may no longer exist in the scheduler if it has been manually
cancelled and removed by calling [`TaskScheduler::cancel_task()`].
This will be a no-op if the task no longer exists.
*/
pub fn resume_task_override<'a>(
&self,
reference: TaskReference,
override_args: Option<LuaResult<LuaMultiValue<'a>>>,
override_args: LuaResult<LuaMultiValue<'a>>,
) -> LuaResult<LuaMultiValue<'a>> {
// Fetch and check if the task was removed, if it got
// removed it means it was intentionally cancelled
@ -312,21 +367,12 @@ impl<'fut> TaskScheduler<'fut> {
}
// Fetch and remove the thread to resume + its arguments
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
let args_opt_res = override_args.or_else(|| {
Ok(self
.lua
.registry_value::<Option<Vec<LuaValue>>>(&task.args)
.expect("Failed to get stored args for task")
.map(LuaMultiValue::from_vec))
.transpose()
});
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
// We got everything we need and our references
// were cleaned up properly, resume the thread
self.tasks_current.set(Some(reference));
let rets = match args_opt_res {
Some(args_res) => match args_res {
let rets = match override_args {
Err(e) => {
// NOTE: Setting this error here means that when the thread
// is resumed it will error instantly, so we don't need
@ -335,28 +381,13 @@ impl<'fut> TaskScheduler<'fut> {
thread.resume(())
}
Ok(args) => thread.resume(args),
},
None => thread.resume(()),
};
self.tasks_current.set(None);
rets
}
/**
Queues a new task to run on the task scheduler.
When we want to schedule a task to resume instantly after the
currently running task we should pass `after_current_resume = true`.
This is useful in cases such as our task.spawn implementation:
```lua
task.spawn(function()
-- This will be a new task, but it should
-- also run right away, until the first yield
end)
-- Here we have either yielded or finished the above task
```
Queues a new blocking task to run on the task scheduler.
*/
pub(crate) fn queue_blocking_task(
&self,
@ -414,4 +445,10 @@ impl<'fut> TaskScheduler<'fut> {
}));
Ok(task_ref)
}
pub(crate) fn set_task_result_sender(&self, task_ref: TaskReference, sender: TaskResultSender) {
self.tasks_result_senders
.borrow_mut()
.insert(task_ref, sender);
}
}