use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString as _};
use alloc::sync::Arc;
use core::cell::{Ref, RefCell, RefMut};
use core::ptr::NonNull;
use anyhow::{bail, ensure, Context as _};
use hashbrown::HashMap;
use rquickjs::loader::{Loader, Resolver};
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::Rest;
use rquickjs::{Ctx, Exception, Function, JsLifetime, Module, Value};
use serde::de::DeserializeOwned;
use serde::Serialize;
struct NakedModule<'js> {
_ptr: NonNull<rquickjs::qjs::JSModuleDef>,
ctx: Ctx<'js>,
}
const _: () = {
assert!(
core::mem::size_of::<rquickjs::Module>() == core::mem::size_of::<Declarations>(),
"Size of Module and Declarations must be the same"
);
assert!(
core::mem::align_of::<rquickjs::Module>() == core::mem::align_of::<Declarations>(),
"Alignment of Module and Declarations must be the same"
);
assert!(
core::mem::size_of::<rquickjs::Module>() == core::mem::size_of::<Exports>(),
"Size of Module and Exports must be the same"
);
assert!(
core::mem::align_of::<rquickjs::Module>() == core::mem::align_of::<Exports>(),
"Alignment of Module and Exports must be the same"
);
assert!(
core::mem::size_of::<rquickjs::Module>() == core::mem::size_of::<NakedModule>(),
"Size of Module and NakedModule must be the same"
);
assert!(
core::mem::align_of::<rquickjs::Module>() == core::mem::align_of::<NakedModule>(),
"Alignment of Module and NakedModule must be the same"
);
};
struct HostModuleDef;
fn coerce_fn_signature<F, E>(f: F) -> F
where
F: for<'js> Fn(Ctx<'js>, Rest<Value<'js>>) -> Result<Value<'js>, E>,
{
f
}
impl ModuleDef for HostModuleDef {
fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
let module: &Module = unsafe { core::mem::transmute(decl) };
let naked_module: &NakedModule = unsafe { core::mem::transmute(module) };
let module_name: String = module.name()?;
let ctx = &naked_module.ctx;
let Some(loader) = ctx.userdata::<HostModuleLoader>() else {
return Err(Exception::throw_internal(ctx, "HostModuleLoader not found"));
};
let modules = loader.modules.borrow();
let Some(module) = modules.get(&module_name) else {
return Ok(());
};
for (name, _) in module.functions.iter() {
decl.declare(name.as_str())?;
}
Ok(())
}
fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
let module: &Module = unsafe { core::mem::transmute(exports) };
let module_name: String = module.name()?;
let Some(loader) = ctx.userdata::<HostModuleLoader>() else {
return Err(Exception::throw_internal(ctx, "HostModuleLoader not found"));
};
let modules = loader.modules.borrow();
let Some(module) = modules.get(&module_name) else {
return Ok(());
};
for (name, func) in module.functions.iter() {
let func = func.clone();
let func = coerce_fn_signature(move |ctx, args| func.call(&ctx, args));
let func = Function::new(ctx.clone(), func)?.with_name(name)?;
exports.export(name.as_str(), func)?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct HostFunction {
#[allow(clippy::type_complexity)]
func: Arc<dyn for<'js> Fn(&Ctx<'js>, Rest<Value<'js>>) -> rquickjs::Result<Value<'js>>>,
}
impl HostFunction {
pub fn new(
func: impl for<'js> Fn(&Ctx<'js>, Rest<Value<'js>>) -> anyhow::Result<Value<'js>> + 'static,
) -> Self {
Self {
func: Arc::new(
move |ctx: &Ctx, args: Rest<Value>| -> rquickjs::Result<Value> {
func(ctx, args).map_err(|e| match e.downcast::<rquickjs::Error>() {
Ok(e) => e,
Err(e) => {
Exception::throw_internal(ctx, &format!("Host function error: {e:#?}"))
}
})
},
),
}
}
pub fn new_json(func: impl Fn(String) -> anyhow::Result<String> + 'static) -> Self {
Self::new(
move |ctx: &Ctx, args: Rest<Value>| -> anyhow::Result<Value> {
let args = ctx
.json_stringify(args.into_inner())?
.map(|s| s.to_string())
.transpose()?
.context("Serializing host function arguments")?;
let res = func(args).context("Calling host function")?;
ctx.json_parse(res).context("Parsing host function result")
},
)
}
pub fn new_serde<Args: DeserializeOwned, Output: Serialize>(
func: impl fn_traits::Fn<Args, Output = anyhow::Result<Output>> + 'static,
) -> Self {
Self::new_json(move |args: String| -> anyhow::Result<String> {
let args: Args =
serde_json::from_str(&args).context("Deserializing arguments for host function")?;
let output: Output = func.call(args)?;
let output =
serde_json::to_string(&output).context("Serializing output of host function")?;
Ok(output)
})
}
pub fn call<'js>(
&self,
ctx: &Ctx<'js>,
args: Rest<Value<'js>>,
) -> rquickjs::Result<Value<'js>> {
(self.func)(ctx, args)
}
}
#[derive(Default, JsLifetime)]
pub struct HostModule {
functions: HashMap<String, HostFunction>,
}
impl HostModule {
pub fn add_function(&mut self, name: impl Into<String>, func: HostFunction) -> &mut Self {
self.functions.insert(name.into(), func);
self
}
}
#[derive(Clone, Default, JsLifetime)]
pub struct HostModuleLoader {
modules: Rc<RefCell<HashMap<String, HostModule>>>,
}
impl Resolver for HostModuleLoader {
fn resolve(&mut self, _ctx: &Ctx<'_>, base: &str, name: &str) -> rquickjs::Result<String> {
if !self.borrow().contains_key(name) {
return Err(rquickjs::Error::new_resolving(base, name));
}
Ok(name.to_string())
}
}
impl Loader for HostModuleLoader {
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Module<'js>> {
if !self.borrow().contains_key(name) {
return Err(rquickjs::Error::new_loading(name));
}
Module::declare_def::<HostModuleDef, _>(ctx.clone(), name)
}
}
impl HostModuleLoader {
pub(crate) fn install(&self, ctx: &Ctx) -> anyhow::Result<()> {
ensure!(
ctx.userdata::<Self>().is_none(),
"HostModuleLoader is already installed"
);
let Ok(None) = ctx.store_userdata(self.clone()) else {
bail!("Failed to install HostModuleLoader");
};
Ok(())
}
pub(crate) fn borrow(&self) -> Ref<'_, HashMap<String, HostModule>> {
self.modules.borrow()
}
pub(crate) fn borrow_mut(&self) -> RefMut<'_, HashMap<String, HostModule>> {
self.modules.borrow_mut()
}
}