#![no_std]
#![no_main]
extern crate alloc;
mod globals;
pub mod host;
mod host_fn;
mod libc;
mod modules;
pub(crate) mod utils;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use anyhow::{anyhow, Context as _};
use hashbrown::HashMap;
use rquickjs::loader::{Loader, Resolver};
use rquickjs::promise::MaybePromise;
use rquickjs::{Context, Ctx, Function, Module, Persistent, Result, Runtime, Value};
use serde::de::DeserializeOwned;
use serde::Serialize;
use tracing::instrument;
use crate::host::Host;
use crate::host_fn::{HostFunction, HostModuleLoader};
use crate::modules::NativeModuleLoader;
#[derive(Clone)]
struct Handler<'a> {
func: Persistent<Function<'a>>,
}
pub struct JsRuntime {
context: Context,
handlers: HashMap<String, Handler<'static>>,
}
unsafe impl Send for JsRuntime {}
impl JsRuntime {
#[instrument(skip_all, level = "info")]
pub fn new<H: Host + 'static>(host: H) -> anyhow::Result<Self> {
let runtime = Runtime::new().context("Unable to initialize JS_RUNTIME")?;
let context = Context::full(&runtime).context("Unable to create JS context")?;
let host_loader = HostModuleLoader::default();
let native_loader = NativeModuleLoader;
let module_loader = ModuleLoader::new(host);
let loader = (host_loader.clone(), native_loader, module_loader);
runtime.set_loader(loader.clone(), loader);
context.with(|ctx| -> anyhow::Result<()> {
host_loader.install(&ctx)?;
globals::setup(&ctx).catch(&ctx)
})?;
Ok(Self {
context,
handlers: HashMap::new(),
})
}
pub fn register_json_host_function(
&mut self,
module_name: impl Into<String>,
function_name: impl Into<String>,
function: impl Fn(String) -> anyhow::Result<String> + 'static,
) -> anyhow::Result<()> {
self.context.with(|ctx| {
ctx.userdata::<HostModuleLoader>()
.context("HostModuleLoader not found in context")?
.borrow_mut()
.entry(module_name.into())
.or_default()
.add_function(function_name.into(), HostFunction::new_json(function));
Ok(())
})
}
pub fn register_host_function<Args, Output>(
&mut self,
module_name: impl Into<String>,
function_name: impl Into<String>,
function: impl fn_traits::Fn<Args, Output = anyhow::Result<Output>> + 'static,
) -> anyhow::Result<()>
where
Args: DeserializeOwned,
Output: Serialize,
{
self.context.with(|ctx| {
ctx.userdata::<HostModuleLoader>()
.context("HostModuleLoader not found in context")?
.borrow_mut()
.entry(module_name.into())
.or_default()
.add_function(function_name.into(), HostFunction::new_serde(function));
Ok(())
})
}
pub fn register_handler(
&mut self,
function_name: impl Into<String>,
handler_script: impl Into<String>,
handler_pwd: impl Into<String>,
) -> anyhow::Result<()> {
let function_name = function_name.into();
let handler_script = handler_script.into();
let handler_pwd = handler_pwd.into();
let handler_script = if !has_export_statement(&handler_script) {
format!("{}\nexport {{ handler }};", handler_script)
} else {
handler_script
};
let handler_path = make_handler_path(&function_name, &handler_pwd);
let func = self.context.with(|ctx| -> anyhow::Result<_> {
let module =
Module::declare(ctx.clone(), handler_path.as_str(), handler_script.clone())
.catch(&ctx)?;
let (module, promise) = module.eval().catch(&ctx)?;
promise.finish::<()>().catch(&ctx)?;
let handler_func: Function = module.get("handler").catch(&ctx)?;
Ok(Persistent::save(&ctx, handler_func))
})?;
self.handlers.insert(function_name, Handler { func });
Ok(())
}
pub fn run_handler(
&mut self,
function_name: String,
event: String,
run_gc: bool,
) -> anyhow::Result<String> {
let handler = self
.handlers
.get(&function_name)
.with_context(|| format!("No handler registered for function {function_name}"))?
.clone();
let _guard = FlushGuard;
self.context.with(|ctx| {
let _gc_guard = MaybeRunGcGuard::new(run_gc, &ctx);
let func = handler.func.clone().restore(&ctx).catch(&ctx)?;
let arg = ctx.json_parse(event).catch(&ctx)?;
let promise: MaybePromise = func.call((arg,)).catch(&ctx)?;
let obj: Value = promise.finish().catch(&ctx)?;
ctx.json_stringify(obj)
.catch(&ctx)?
.context("The handler function did not return a value")?
.to_string()
.catch(&ctx)
})
}
}
impl Drop for JsRuntime {
fn drop(&mut self) {
modules::io::io::flush();
self.handlers.clear();
}
}
#[derive(Clone)]
struct ModuleLoader {
host: Rc<dyn Host>,
}
impl ModuleLoader {
fn new(host: impl Host + 'static) -> Self {
Self {
host: Rc::new(host),
}
}
}
impl Resolver for ModuleLoader {
fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> Result<String> {
let (dir, _) = base.rsplit_once('/').unwrap_or((".", ""));
let path = self
.host
.resolve_module(dir.to_string(), name.to_string())
.map_err(|_err| rquickjs::Error::new_resolving(base, name))?;
let path = path.replace('\\', "/");
Ok(path)
}
}
impl Loader for ModuleLoader {
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> Result<Module<'js>> {
let source = self
.host
.load_module(name.to_string())
.map_err(|_err| rquickjs::Error::new_loading(name))?;
Module::declare(ctx.clone(), name, source)
}
}
fn make_handler_path(function_name: &str, handler_dir: &str) -> String {
let handler_dir = if handler_dir.is_empty() {
"."
} else {
handler_dir
};
let function_name = if function_name.is_empty() {
"handler"
} else {
function_name
};
let function_name = function_name.replace('\\', "/");
let mut handler_path = handler_dir.replace('\\', "/");
if !handler_path.ends_with('/') {
handler_path.push('/');
}
handler_path.push_str(&function_name);
if !handler_path.ends_with(".js") && !handler_path.ends_with(".mjs") {
handler_path.push_str(".js");
}
handler_path
}
fn has_export_statement(script: &str) -> bool {
script.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("export ") || trimmed.starts_with("export{")
})
}
struct FlushGuard;
impl Drop for FlushGuard {
fn drop(&mut self) {
modules::io::io::flush();
}
}
trait CatchJsErrorExt {
type Ok;
fn catch(self, ctx: &Ctx<'_>) -> anyhow::Result<Self::Ok>;
}
impl<T> CatchJsErrorExt for rquickjs::Result<T> {
type Ok = T;
fn catch(self, ctx: &Ctx<'_>) -> anyhow::Result<T> {
match rquickjs::CatchResultExt::catch(self, ctx) {
Ok(s) => Ok(s),
Err(e) => Err(anyhow!("Runtime error: {e:#?}")),
}
}
}
struct MaybeRunGcGuard<'a> {
run_gc: bool,
ctx: Ctx<'a>,
}
impl<'a> MaybeRunGcGuard<'a> {
fn new(run_gc: bool, ctx: &Ctx<'a>) -> Self {
Self {
run_gc,
ctx: ctx.clone(),
}
}
}
impl Drop for MaybeRunGcGuard<'_> {
fn drop(&mut self) {
if self.run_gc {
self.ctx.run_gc();
}
}
}