use core::sync::atomic::AtomicU64;
use core::sync::atomic::Ordering;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::thread::ThreadId;
use rquickjs::Array;
use rquickjs::Context;
use rquickjs::Ctx;
use rquickjs::Function;
use rquickjs::Object;
use rquickjs::Persistent;
use rquickjs::Runtime;
use rquickjs::Value;
use polyplug::Runtime as PolyplugRuntime;
use polyplug::error::LoaderError;
use polyplug::loader::BundleLoader;
use polyplug::loader::BundleSource;
use polyplug::loader::ManifestData;
use polyplug::logger::LoggerHandle;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::BundleInitContext;
use polyplug_abi::CallArena;
use polyplug_abi::DispatchType;
use polyplug_abi::GuestContractHandle;
use polyplug_abi::GuestContractInstance;
use polyplug_abi::GuestContractInterface;
use polyplug_abi::HostApi;
use polyplug_abi::HostContractInstance;
use polyplug_abi::HostContractInterface;
use polyplug_abi::PluginDescriptor;
use polyplug_abi::StringView;
use polyplug_abi::SupportedLanguage;
use polyplug_abi::VmLoaderData;
use polyplug_abi::dispatch::dispatch_mechanisms::DispatchMechanisms;
use polyplug_abi::dispatch::vm_dispatch::VmDispatch;
use polyplug_abi::types::LogLevel;
use polyplug_abi::types::Version;
use polyplug_utils::BundleId;
use polyplug_utils::GuestContractId;
use crate::config::JsConfig;
struct JsRegistrationData {
contract_id: u64,
contract_version: u32,
contract_name: String,
functions: Vec<Persistent<Function<'static>>>,
factory: Persistent<Function<'static>>,
}
pub struct JsLoaderData {
pub functions: Vec<Persistent<Function<'static>>>,
pub factory: Persistent<Function<'static>>,
pub bridge: Persistent<Object<'static>>,
pub host_lo: f64,
pub host_hi: f64,
pub default_impl: Persistent<Value<'static>>,
pub instances: Mutex<HashMap<u64, Persistent<Value<'static>>>>,
pub next_id: AtomicU64,
pub contract_id: u64,
pub ctx: Context,
pub _runtime: Runtime,
pub in_dispatch_threads: Mutex<Vec<ThreadId>>,
pub logger: LoggerHandle,
}
struct SendVm(Box<JsLoaderData>);
unsafe impl Send for SendVm {}
unsafe impl Sync for SendVm {}
impl SendVm {
fn as_ptr(&self) -> *const JsLoaderData {
&*self.0 as *const JsLoaderData
}
#[cfg(test)]
fn data(&self) -> &JsLoaderData {
&self.0
}
}
struct JsDispatchGuard<'a> {
threads: &'a Mutex<Vec<ThreadId>>,
}
impl Drop for JsDispatchGuard<'_> {
fn drop(&mut self) {
let this: ThreadId = std::thread::current().id();
let mut guard: std::sync::MutexGuard<'_, Vec<ThreadId>> =
self.threads.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(pos) = guard.iter().position(|&id| id == this) {
guard.swap_remove(pos);
}
}
}
unsafe extern "C" fn js_create_instance(
loader_data: VmLoaderData,
_host: *const HostApi,
_args: *const (),
out_instance: *mut GuestContractInstance,
) {
if out_instance.is_null() {
return;
}
if loader_data.data.is_null() {
unsafe { out_instance.write(GuestContractInstance::null()) };
return;
}
let data: &JsLoaderData = unsafe { &*(loader_data.data as *const JsLoaderData) };
let this_thread: ThreadId = std::thread::current().id();
{
let threads: std::sync::MutexGuard<'_, Vec<ThreadId>> = data
.in_dispatch_threads
.lock()
.unwrap_or_else(PoisonError::into_inner);
if threads.contains(&this_thread) {
drop(threads);
unsafe { out_instance.write(GuestContractInstance::null()) };
return;
}
}
let built: Result<Persistent<Value<'static>>, rquickjs::Error> = data.ctx.with(|ctx| {
let factory: Function<'_> = data.factory.clone().restore(&ctx)?;
let bridge: Object<'_> = data.bridge.clone().restore(&ctx)?;
let impl_val: Value<'_> = factory.call::<(Object<'_>, f64, f64), Value<'_>>((
bridge,
data.host_lo,
data.host_hi,
))?;
Ok(Persistent::save(&ctx, impl_val))
});
let instance: GuestContractInstance = match built {
Ok(impl_persistent) => {
let id: u64 = data.next_id.fetch_add(1, Ordering::Relaxed);
let mut map: std::sync::MutexGuard<'_, HashMap<u64, Persistent<Value<'static>>>> = data
.instances
.lock()
.unwrap_or_else(PoisonError::into_inner);
map.insert(id, impl_persistent);
GuestContractInstance {
data: id as usize as *mut core::ffi::c_void,
contract_id: GuestContractId::from_u64(data.contract_id),
}
}
Err(e) => {
data.logger.log(LogLevel::Error, "loader.js", || {
format!("JS create_instance: factory call failed: {e}")
});
GuestContractInstance::null()
}
};
unsafe { out_instance.write(instance) };
}
unsafe extern "C" fn js_destroy_instance(
loader_data: VmLoaderData,
_host: *const HostApi,
instance: GuestContractInstance,
) {
let id: u64 = instance.data as usize as u64;
if id == 0 {
return;
}
if loader_data.data.is_null() {
return;
}
let data: &JsLoaderData = unsafe { &*(loader_data.data as *const JsLoaderData) };
let mut map: std::sync::MutexGuard<'_, HashMap<u64, Persistent<Value<'static>>>> = data
.instances
.lock()
.unwrap_or_else(PoisonError::into_inner);
map.remove(&id);
}
unsafe extern "C" fn js_dispatch(
loader_data: VmLoaderData,
instance: GuestContractInstance,
fn_id: u32,
args: *const (),
out: *mut (),
arena: *mut CallArena,
out_err: *mut AbiError,
) {
let result: AbiError =
unsafe { js_dispatch_impl(loader_data, instance, fn_id, args, out, arena) };
if !out_err.is_null() {
unsafe { out_err.write(result) };
}
}
unsafe fn js_dispatch_impl(
loader_data: VmLoaderData,
instance: GuestContractInstance,
fn_id: u32,
args: *const (),
out: *mut (),
arena: *mut CallArena,
) -> AbiError {
let data: &JsLoaderData = unsafe { &*(loader_data.data as *const JsLoaderData) };
let this_thread: ThreadId = std::thread::current().id();
{
let mut threads: std::sync::MutexGuard<'_, Vec<ThreadId>> = data
.in_dispatch_threads
.lock()
.unwrap_or_else(PoisonError::into_inner);
if threads.contains(&this_thread) {
drop(threads);
return AbiError {
code: AbiErrorCode::ReentrantCall as u32,
message: StringView::null(),
};
}
threads.push(this_thread);
}
let _dispatch_guard: JsDispatchGuard<'_> = JsDispatchGuard {
threads: &data.in_dispatch_threads,
};
let func_persistent: &Persistent<Function<'static>> = match data.functions.get(fn_id as usize) {
Some(f) => f,
None => {
return AbiError {
code: AbiErrorCode::FunctionNotAvailable as u32,
message: StringView::null(),
};
}
};
let instance_id: u64 = instance.data as usize as u64;
let impl_persistent: Persistent<Value<'static>> = if instance_id == 0 {
data.default_impl.clone()
} else {
let map: std::sync::MutexGuard<'_, HashMap<u64, Persistent<Value<'static>>>> = data
.instances
.lock()
.unwrap_or_else(PoisonError::into_inner);
match map.get(&instance_id) {
Some(p) => p.clone(),
None => {
return AbiError {
code: AbiErrorCode::FunctionNotAvailable as u32,
message: StringView::null(),
};
}
}
};
let args_usize: usize = args as usize;
let out_usize: usize = out as usize;
let args_f64: f64 = args_usize as f64;
let out_f64: f64 = out_usize as f64;
let arena_usize: usize = arena as usize;
let arena_f64: f64 = arena_usize as f64;
let call_result: Result<i32, rquickjs::Error> = data.ctx.with(|ctx| {
let js_fn: Function<'_> = func_persistent.clone().restore(&ctx)?;
let impl_val: Value<'_> = impl_persistent.clone().restore(&ctx)?;
let bridge: Object<'_> = data.bridge.clone().restore(&ctx)?;
js_fn.call::<(Value<'_>, f64, f64, f64, Object<'_>), i32>((
impl_val, args_f64, out_f64, arena_f64, bridge,
))
});
match call_result {
Ok(0) => AbiError::ok(),
Ok(code) => AbiError {
code: code as u32,
message: StringView::null(),
},
Err(e) => {
data.logger.log(LogLevel::Error, "loader.js", || {
format!("JS function call failed: {e}")
});
AbiError {
code: AbiErrorCode::Generic as u32,
message: StringView::null(),
}
}
}
}
fn pack_handle(h: GuestContractHandle) -> Option<u64> {
if h.is_null() {
None
} else {
Some(h.pack())
}
}
fn host_from_usize(addr: usize) -> Option<*const HostApi> {
let ptr: *const HostApi = addr as *const HostApi;
if ptr.is_null() { None } else { Some(ptr) }
}
unsafe fn destroy_host_instance_if_needed(
iface: *const HostContractInterface,
singleton: bool,
instance: HostContractInstance,
) {
if singleton {
return;
}
unsafe { ((*iface).destroy_instance)(iface, instance) };
}
fn register_host_functions<'js>(
ctx: &Ctx<'js>,
polyplug_obj: &Object<'js>,
host_interface: *const HostApi,
bundle_name: &str,
logger: LoggerHandle,
) -> Result<(), LoaderError> {
let host_interface_usize: usize = host_interface as usize;
let find_by_contract_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>, lo: u32, hi: u32, min_ver: u32| -> Option<u64> {
let contract_id: u64 = (hi as u64) << 32 | lo as u64;
let hvt: *const HostApi = host_from_usize(host_interface_usize)?;
let handle: GuestContractHandle =
unsafe { ((*hvt).find_guest_contract)(hvt, contract_id, min_ver) };
pack_handle(handle)
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: findByContract function creation failed: {e}"),
})?;
polyplug_obj
.set("findByContract", find_by_contract_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: findByContract set failed: {e}"),
})?;
let find_by_bundle_fn: Function<'js> = Function::new(
ctx.clone(),
|_ctx: Ctx<'js>,
_blo: u32,
_bhi: u32,
_clo: u32,
_chi: u32,
_min_ver: u32|
-> Option<u64> {
None
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: findByBundle function creation failed: {e}"),
})?;
polyplug_obj
.set("findByBundle", find_by_bundle_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: findByBundle set failed: {e}"),
})?;
let find_all_by_contract_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>, lo: u32, hi: u32, min_ver: u32| -> u32 {
let contract_id: u64 = (hi as u64) << 32 | lo as u64;
let hvt: *const HostApi = match host_from_usize(host_interface_usize) {
Some(ptr) => ptr,
None => return 0_u32,
};
let handles: polyplug_abi::types::Array<GuestContractHandle> =
unsafe { ((*hvt).find_all_guest_contracts)(hvt, contract_id, min_ver) };
handles.len as u32
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"JS runtime js-quickjs error: findAllByContract function creation failed: {e}"
),
})?;
polyplug_obj
.set("findAllByContract", find_all_by_contract_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: findAllByContract set failed: {e}"),
})?;
let resolve_guest_contract_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>, packed: u64| -> Option<u64> {
let index: u32 = packed as u32;
let generation: u32 = (packed >> 32) as u32;
let handle: GuestContractHandle = GuestContractHandle { index, generation };
let hvt: *const HostApi = host_from_usize(host_interface_usize)?;
let vtable_ptr: *const GuestContractInterface =
unsafe { ((*hvt).resolve_guest_contract)(hvt, handle) };
if vtable_ptr.is_null() {
None
} else {
Some(vtable_ptr as usize as u64)
}
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"JS runtime js-quickjs error: resolveGuestContract function creation failed: {e}"
),
})?;
polyplug_obj
.set("resolveGuestContract", resolve_guest_contract_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: resolveGuestContract set failed: {e}"),
})?;
let revision_fn: Function<'js> = Function::new(
ctx.clone(),
move |ctx: Ctx<'js>| -> Result<Array<'js>, rquickjs::Error> {
let value: u64 = match host_from_usize(host_interface_usize) {
Some(hvt) => {
let ptr: *const u64 = unsafe { ((*hvt).revision_counter)(hvt) };
if ptr.is_null() {
0_u64
} else {
unsafe { (*(ptr as *const AtomicU64)).load(Ordering::Acquire) }
}
}
None => 0_u64,
};
let arr: Array<'js> = Array::new(ctx.clone())
.map_err(|_| rquickjs::Exception::throw_message(&ctx, "array creation failed"))?;
let _ = arr.set(0, (value as u32) as f64);
let _ = arr.set(1, ((value >> 32) as u32) as f64);
Ok(arr)
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: revision function creation failed: {e}"),
})?;
polyplug_obj
.set("revision", revision_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: revision set failed: {e}"),
})?;
let dispatch_peer_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>,
contract_id_lo: u32,
contract_id_hi: u32,
min_version: u32,
fn_id: u32,
args_ptr: u64,
out_ptr: u64|
-> u32 {
let contract_id: u64 = (contract_id_hi as u64) << 32 | contract_id_lo as u64;
let hvt: *const HostApi = match host_from_usize(host_interface_usize) {
Some(p) => p,
None => return AbiErrorCode::Generic as u32,
};
let handle: GuestContractHandle =
unsafe { ((*hvt).find_guest_contract)(hvt, contract_id, min_version) };
let iface: *const GuestContractInterface =
unsafe { ((*hvt).resolve_guest_contract)(hvt, handle) };
if iface.is_null() {
return AbiErrorCode::NotFound as u32;
}
let peer_dt: DispatchType = unsafe { (*iface).dispatch_type };
let peer_loader_data: VmLoaderData = match peer_dt {
DispatchType::VirtualMachine => unsafe { (*iface).dispatch.vm.loader_data },
DispatchType::Native => VmLoaderData::null(),
};
let mut instance: GuestContractInstance = GuestContractInstance::null();
unsafe {
((*iface).create_instance)(peer_loader_data, hvt, core::ptr::null(), &mut instance)
};
instance.contract_id = GuestContractId::from_u64(contract_id);
let args: *const core::ffi::c_void = args_ptr as usize as *const core::ffi::c_void;
let out: *mut core::ffi::c_void = out_ptr as usize as *mut core::ffi::c_void;
let mut err: AbiError = AbiError::ok();
let code: u32 = match peer_dt {
DispatchType::Native => {
let native: polyplug_abi::NativeDispatch = unsafe { (*iface).dispatch.native };
if fn_id >= native.function_count || native.functions.is_null() {
unsafe { ((*iface).destroy_instance)(peer_loader_data, hvt, instance) };
return AbiErrorCode::FunctionNotAvailable as u32;
}
let slot: *const () = unsafe { *native.functions.add(fn_id as usize) };
if slot.is_null() {
unsafe { ((*iface).destroy_instance)(peer_loader_data, hvt, instance) };
return AbiErrorCode::FunctionNotAvailable as u32;
}
let dispatch_fn: unsafe extern "C" fn(
GuestContractInstance,
*const core::ffi::c_void,
*mut core::ffi::c_void,
*mut AbiError,
) = unsafe { core::mem::transmute(slot) };
unsafe { dispatch_fn(instance, args, out, &mut err) };
err.code
}
DispatchType::VirtualMachine => {
unsafe {
((*iface).dispatch.vm.call)(
(*iface).dispatch.vm.loader_data,
instance,
fn_id,
args as *const (),
out as *mut (),
core::ptr::null_mut(),
&mut err,
)
};
err.code
}
};
unsafe { ((*iface).destroy_instance)(peer_loader_data, hvt, instance) };
code
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: dispatchPeer function creation failed: {e}"),
})?;
polyplug_obj
.set("dispatchPeer", dispatch_peer_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: dispatchPeer set failed: {e}"),
})?;
let alloc_fn: Function<'js> = Function::new(
ctx.clone(),
move |ctx: Ctx<'js>, size: u32| -> Result<Array<'js>, rquickjs::Error> {
let hvt: *const HostApi = match host_from_usize(host_interface_usize) {
Some(ptr) => ptr,
None => {
let arr: Array<'js> = Array::new(ctx.clone()).map_err(|_| {
rquickjs::Exception::throw_message(&ctx, "array creation failed")
})?;
let _ = arr.set(0, 0.0_f64);
let _ = arr.set(1, 0.0_f64);
return Ok(arr);
}
};
let ptr: *mut u8 = unsafe { ((*hvt).alloc)(hvt, size as usize, 1) };
let ptr_usize: usize = ptr as usize;
let arr: Array<'js> = Array::new(ctx.clone())
.map_err(|_| rquickjs::Exception::throw_message(&ctx, "array creation failed"))?;
let _ = arr.set(0, (ptr_usize as u32) as f64);
let _ = arr.set(1, ((ptr_usize >> 32) as u32) as f64);
Ok(arr)
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: alloc function creation failed: {e}"),
})?;
polyplug_obj
.set("alloc", alloc_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: alloc set failed: {e}"),
})?;
let arena_alloc_fn: Function<'js> = Function::new(
ctx.clone(),
move |ctx: Ctx<'js>, size: u32, arena_ptr: f64| -> Result<Array<'js>, rquickjs::Error> {
let arena: *mut CallArena = arena_ptr as u64 as usize as *mut CallArena;
let ptr: *mut u8 = if arena.is_null() {
match host_from_usize(host_interface_usize) {
Some(hvt) => unsafe { ((*hvt).alloc)(hvt, size as usize, 1) },
None => core::ptr::null_mut(),
}
} else {
unsafe { (*arena).alloc(size as usize, 1) }
};
let ptr_usize: usize = ptr as usize;
let arr: Array<'js> = Array::new(ctx.clone())
.map_err(|_| rquickjs::Exception::throw_message(&ctx, "array creation failed"))?;
let _ = arr.set(0, (ptr_usize as u32) as f64);
let _ = arr.set(1, ((ptr_usize >> 32) as u32) as f64);
Ok(arr)
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: arenaAlloc function creation failed: {e}"),
})?;
polyplug_obj
.set("arenaAlloc", arena_alloc_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: arenaAlloc set failed: {e}"),
})?;
let log_fn: Function<'js> = Function::new(
ctx.clone(),
move |level: f64, scope: String, message: String| {
let log_level: LogLevel = if level.fract() == 0.0 {
LogLevel::from_u32(level as u32).unwrap_or(LogLevel::Error)
} else {
LogLevel::Error
};
logger.log(log_level, &scope, || message);
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: log function creation failed: {e}"),
})?;
polyplug_obj
.set("log", log_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: log set failed: {e}"),
})?;
let free_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>, lo: f64, hi: f64, size: u32, align: u32| {
let hvt: *const HostApi = match host_from_usize(host_interface_usize) {
Some(ptr) => ptr,
None => return,
};
let ptr: *mut u8 = ((hi as u64) << 32 | lo as u64) as usize as *mut u8;
if ptr.is_null() {
return;
}
unsafe { ((*hvt).free)(hvt, ptr, size as usize, align as usize) };
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: free function creation failed: {e}"),
})?;
polyplug_obj
.set("free", free_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: free set failed: {e}"),
})?;
let read_i32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64| -> i32 {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *const i32 = ptr_u64 as usize as *const i32;
if ptr.is_null() {
return 0;
}
unsafe { *ptr }
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readI32 function creation failed: {e}"),
})?;
polyplug_obj
.set("readI32", read_i32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readI32 set failed: {e}"),
})?;
let write_i32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64, value: i32| {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *mut i32 = ptr_u64 as usize as *mut i32;
if ptr.is_null() {
return;
}
unsafe {
*ptr = value;
}
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeI32 function creation failed: {e}"),
})?;
polyplug_obj
.set("writeI32", write_i32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeI32 set failed: {e}"),
})?;
let read_byte_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64| -> u32 {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *const u8 = ptr_u64 as usize as *const u8;
if ptr.is_null() {
return 0;
}
unsafe { *ptr as u32 }
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readByte function creation failed: {e}"),
})?;
polyplug_obj
.set("readByte", read_byte_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readByte set failed: {e}"),
})?;
let write_byte_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64, value: u32| {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *mut u8 = ptr_u64 as usize as *mut u8;
if ptr.is_null() {
return;
}
unsafe {
*ptr = value as u8;
}
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeByte function creation failed: {e}"),
})?;
polyplug_obj
.set("writeByte", write_byte_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeByte set failed: {e}"),
})?;
let read_memory_fn: Function<'js> = Function::new(
ctx.clone(),
|ctx: Ctx<'js>, ptr_num: f64, len: u32| -> Result<Array<'js>, rquickjs::Error> {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *const u8 = ptr_u64 as usize as *const u8;
let len_usize: usize = len as usize;
let arr: Array<'js> = Array::new(ctx.clone())
.map_err(|_| rquickjs::Exception::throw_message(&ctx, "Array creation failed"))?;
if ptr.is_null() || len_usize == 0 {
return Ok(arr);
}
let bytes: &[u8] = unsafe { core::slice::from_raw_parts(ptr, len_usize) };
for (i, &byte) in bytes.iter().enumerate() {
let _ = arr.set(i, u32::from(byte) as f64);
}
Ok(arr)
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readMemory function creation failed: {e}"),
})?;
polyplug_obj
.set("readMemory", read_memory_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readMemory set failed: {e}"),
})?;
let read_u32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64| -> f64 {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *const u32 = ptr_u64 as usize as *const u32;
if ptr.is_null() {
return 0.0;
}
unsafe { *ptr as f64 }
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readU32 function creation failed: {e}"),
})?;
polyplug_obj
.set("readU32", read_u32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readU32 set failed: {e}"),
})?;
let write_u32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64, value: f64| {
let ptr_u64: u64 = ptr_num as u64;
let ptr: *mut u32 = ptr_u64 as usize as *mut u32;
if ptr.is_null() {
return;
}
unsafe {
*ptr = value as u64 as u32;
}
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeU32 function creation failed: {e}"),
})?;
polyplug_obj
.set("writeU32", write_u32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeU32 set failed: {e}"),
})?;
let call_host_contract_fn: Function<'js> = Function::new(
ctx.clone(),
move |_ctx: Ctx<'js>,
contract_id_lo: u32,
contract_id_hi: u32,
min_version: u32,
fn_id: u32,
args_ptr: u64,
out_ptr: u64|
-> u32 {
let contract_id: u64 = (contract_id_hi as u64) << 32 | contract_id_lo as u64;
let hvt: *const HostApi = match host_from_usize(host_interface_usize) {
Some(p) => p,
None => return AbiErrorCode::Generic as u32,
};
let iface: *const HostContractInterface =
unsafe { ((*hvt).resolve_host_contract_interface)(hvt, contract_id, min_version) };
if iface.is_null() {
return AbiErrorCode::NotFound as u32;
}
let instance: HostContractInstance =
unsafe { ((*hvt).get_host_contract)(hvt, contract_id, min_version) };
let args: *const core::ffi::c_void = args_ptr as usize as *const core::ffi::c_void;
let out: *mut core::ffi::c_void = out_ptr as usize as *mut core::ffi::c_void;
let singleton: bool = unsafe { (*iface).singleton };
let dt: DispatchType = unsafe { (*iface).dispatch_type };
let code: u32 = match dt {
DispatchType::Native => {
let native: polyplug_abi::NativeDispatch = unsafe { (*iface).dispatch.native };
if native.functions.is_null() || fn_id >= native.function_count {
unsafe { destroy_host_instance_if_needed(iface, singleton, instance) };
return AbiErrorCode::FunctionNotAvailable as u32;
}
let fn_ptr: *const () = unsafe { *native.functions.add(fn_id as usize) };
if fn_ptr.is_null() {
unsafe { destroy_host_instance_if_needed(iface, singleton, instance) };
return AbiErrorCode::FunctionNotAvailable as u32;
}
let dispatch_fn: unsafe extern "C" fn(
*const core::ffi::c_void,
*const core::ffi::c_void,
*mut core::ffi::c_void,
*mut AbiError,
) = unsafe { core::mem::transmute(fn_ptr) };
let mut err: AbiError = AbiError::ok();
unsafe {
dispatch_fn(
instance.data as *const core::ffi::c_void,
args,
out,
&mut err,
)
};
err.code
}
DispatchType::VirtualMachine => {
let mut err: AbiError = AbiError::ok();
unsafe {
((*iface).dispatch.vm.call)(
(*iface).dispatch.vm.loader_data,
GuestContractInstance::null(),
fn_id,
args as *const (),
out as *mut (),
core::ptr::null_mut(),
&mut err,
)
};
err.code
}
};
unsafe { destroy_host_instance_if_needed(iface, singleton, instance) };
code
},
)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!(
"JS runtime js-quickjs error: callHostContract function creation failed: {e}"
),
})?;
polyplug_obj
.set("callHostContract", call_host_contract_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: callHostContract set failed: {e}"),
})?;
let read_f64_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64| -> f64 {
let ptr: *const f64 = (ptr_num as u64) as usize as *const f64;
if ptr.is_null() {
return 0.0;
}
unsafe { *ptr }
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readF64 function creation failed: {e}"),
})?;
polyplug_obj
.set("readF64", read_f64_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readF64 set failed: {e}"),
})?;
let read_f32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64| -> f64 {
let ptr: *const f32 = (ptr_num as u64) as usize as *const f32;
if ptr.is_null() {
return 0.0;
}
unsafe { *ptr as f64 }
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readF32 function creation failed: {e}"),
})?;
polyplug_obj
.set("readF32", read_f32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: readF32 set failed: {e}"),
})?;
let write_f64_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64, value: f64| {
let ptr: *mut f64 = (ptr_num as u64) as usize as *mut f64;
if ptr.is_null() {
return;
}
unsafe {
*ptr = value;
}
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeF64 function creation failed: {e}"),
})?;
polyplug_obj
.set("writeF64", write_f64_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeF64 set failed: {e}"),
})?;
let write_f32_fn: Function<'js> = Function::new(ctx.clone(), |ptr_num: f64, value: f64| {
let ptr: *mut f32 = (ptr_num as u64) as usize as *mut f32;
if ptr.is_null() {
return;
}
unsafe {
*ptr = value as f32;
}
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeF32 function creation failed: {e}"),
})?;
polyplug_obj
.set("writeF32", write_f32_fn)
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: bundle_name.to_owned(),
error: format!("JS runtime js-quickjs error: writeF32 set failed: {e}"),
})?;
Ok(())
}
struct InitBundleGuard<'r> {
runtime: &'r PolyplugRuntime,
}
impl<'r> InitBundleGuard<'r> {
fn enter(runtime: &'r PolyplugRuntime, bundle_id: u64) -> Self {
runtime.push_init_bundle_id(bundle_id);
Self { runtime }
}
}
impl Drop for InitBundleGuard<'_> {
fn drop(&mut self) {
self.runtime.pop_init_bundle_id();
}
}
pub struct JsLoader {
_config: JsConfig,
live: Mutex<HashMap<BundleId, Vec<SendVm>>>,
scheduled_reclaims: AtomicU64,
}
impl JsLoader {
pub fn new(config: JsConfig) -> JsLoader {
JsLoader {
_config: config,
live: Mutex::new(HashMap::new()),
scheduled_reclaims: AtomicU64::new(0),
}
}
fn schedule_reclaim(&self, state: Vec<SendVm>) {
for vm in state {
self.scheduled_reclaims.fetch_add(1, Ordering::Relaxed);
crossbeam_epoch::pin().defer(move || drop(vm));
}
}
fn read_path_source(manifest: &ManifestData) -> Result<String, LoaderError> {
let bundle_path: PathBuf = if !manifest.file.is_empty() {
manifest.path.join(&manifest.file)
} else {
manifest.path.join("bundle.js")
};
std::fs::read_to_string(&bundle_path).map_err(|e: std::io::Error| {
LoaderError::ManifestParse {
path: bundle_path.display().to_string(),
reason: e.to_string(),
}
})
}
fn load_inner(
&self,
manifest: &ManifestData,
bundle_js: &str,
bundle_dir: Option<&Path>,
runtime: &PolyplugRuntime,
) -> Result<(), LoaderError> {
let bundle_id: u64 = manifest.id;
let qjs_runtime: Runtime =
Runtime::new().map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("JS runtime init failed: QuickJS runtime init failed: {e}"),
})?;
let ctx: Context =
Context::full(&qjs_runtime).map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("JS runtime js-quickjs error: context creation failed: {e}"),
})?;
let host_interface: *const HostApi = runtime.as_context_ptr();
let _init_window: InitBundleGuard<'_> = InitBundleGuard::enter(runtime, bundle_id);
let bundle_dir_str: String = match bundle_dir {
Some(dir) => dir.to_string_lossy().into_owned(),
None => String::new(),
};
let host_usize: usize = host_interface as usize;
let host_lo: f64 = (host_usize as u32) as f64;
let host_hi: f64 = ((host_usize >> 32) as u32) as f64;
type InitExtract = (
JsRegistrationData,
Persistent<Object<'static>>,
Persistent<Value<'static>>,
);
let init_extract: Result<InitExtract, LoaderError> = ctx.with(|ctx_ref: Ctx<'_>| {
let polyplug_obj: Object<'_> =
Object::new(ctx_ref.clone()).map_err(|e: rquickjs::Error| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("JS runtime js-quickjs error: object creation failed: {e}"),
}
})?;
register_host_functions(
&ctx_ref,
&polyplug_obj,
host_interface,
&manifest.name,
runtime.logger(),
)?;
let set_bundle: String = format!("globalThis.bundlePath = {:?};", bundle_dir_str);
ctx_ref
.eval::<Value<'_>, _>(set_bundle.as_str())
.map_err(|e: rquickjs::Error| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("JS runtime js-quickjs error: bundlePath injection failed: {e}"),
}
})?;
ctx_ref
.eval::<Value<'_>, _>(bundle_js)
.map_err(|e: rquickjs::Error| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!("JS runtime js-quickjs error: bundle eval failed: {e}"),
}
})?;
let init_fn: Function<'_> = ctx_ref
.globals()
.get::<&str, Function<'_>>("polyplug_init")
.map_err(|_| {
LoaderError::InitSymbolMissing {
bundle: bundle_dir_str.clone(),
}
})?;
let bundle_path_static: &'static str =
Box::leak(bundle_dir_str.clone().into_boxed_str());
let plugin_ctx: BundleInitContext = BundleInitContext {
bundle_path: StringView {
ptr: bundle_path_static.as_ptr(),
len: bundle_path_static.len(),
},
bundle_id,
};
let ctx_usize: usize = &plugin_ctx as *const BundleInitContext as usize;
let ctx_lo: f64 = (ctx_usize as u32) as f64;
let ctx_hi: f64 = ((ctx_usize >> 32) as u32) as f64;
let init_value: Array<'_> = init_fn
.call::<(f64, f64, f64, f64, Object<'_>), Array<'_>>((
host_lo,
host_hi,
ctx_lo,
ctx_hi,
polyplug_obj.clone(),
))
.map_err(|e: rquickjs::Error| {
let thrown: Value<'_> = ctx_ref.catch();
let detail: String = match thrown.as_exception() {
Some(exc) => exc.message().unwrap_or_else(|| e.to_string()),
None => e.to_string(),
};
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: polyplug_init call failed: {detail}"
),
}
})?;
let abi_error: Object<'_> = init_value.get::<Object<'_>>(1).map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: "JS runtime js-quickjs error: polyplug_init did not return an AbiError"
.to_owned(),
}
})?;
let init_code: u32 = abi_error.get::<&str, f64>("code").unwrap_or(0.0_f64) as u32;
if init_code != AbiErrorCode::Ok as u32 {
let message: Option<String> =
abi_error.get::<&str, Option<String>>("message").unwrap_or(None);
let detail: String = match message {
Some(msg) if !msg.is_empty() => format!(" ({msg})"),
_ => String::new(),
};
return Err(LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: polyplug_init returned error code {init_code}{detail}"
),
});
}
let registrations: Array<'_> = init_value.get::<Array<'_>>(0).map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: "JS runtime js-quickjs error: polyplug_init did not return a registrations array"
.to_owned(),
}
})?;
let entry: Object<'_> = registrations.get::<Object<'_>>(0).map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: "JS runtime js-quickjs error: polyplug_init returned an empty registrations array"
.to_owned(),
}
})?;
let contract_lo: u32 = entry.get::<&str, f64>("contractLo").unwrap_or(0.0_f64) as u32;
let contract_hi: u32 = entry.get::<&str, f64>("contractHi").unwrap_or(0.0_f64) as u32;
let contract_id: u64 = (contract_hi as u64) << 32 | contract_lo as u64;
let fn_count: u32 = entry.get::<&str, f64>("fnCount").unwrap_or(0.0_f64) as u32;
let fn_count_usize: usize = fn_count as usize;
let contract_name: String =
entry.get::<&str, String>("contractName").map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: "JS runtime js-quickjs error: registration entry missing contractName"
.to_owned(),
}
})?;
let contract_version: u32 =
entry.get::<&str, f64>("version").unwrap_or(0.0_f64) as u32;
let interface: Object<'_> = entry.get::<&str, Object<'_>>("interface").map_err(|_| {
LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: "JS runtime js-quickjs error: registration entry missing interface"
.to_owned(),
}
})?;
let functions_array: Object<'_> = interface
.get::<&str, Object<'_>>("functions")
.map_err(|_| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: interface for contract '{contract_name}' has no 'functions' array"
),
})?;
let mut functions: Vec<Persistent<Function<'static>>> =
Vec::with_capacity(fn_count_usize);
for i in 0..fn_count_usize {
let func: Function<'_> = functions_array
.get::<u32, Function<'_>>(i as u32)
.map_err(|_| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: interface for contract '{contract_name}' declares fnCount={fn_count} but functions[{i}] is missing or not a function"
),
})?;
functions.push(Persistent::save(&ctx_ref, func));
}
let factory_fn: Function<'_> = interface
.get::<&str, Function<'_>>("factory")
.map_err(|_| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: interface for contract '{contract_name}' has no 'factory' function (call setXFactory before registering)"
),
})?;
let factory: Persistent<Function<'static>> = Persistent::save(&ctx_ref, factory_fn);
let bridge: Persistent<Object<'static>> = Persistent::save(&ctx_ref, polyplug_obj.clone());
let impl_val: Value<'_> = factory
.clone()
.restore(&ctx_ref)
.and_then(|f: Function<'_>| {
f.call::<(Object<'_>, f64, f64), Value<'_>>((polyplug_obj, host_lo, host_hi))
})
.map_err(|e: rquickjs::Error| LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: default impl factory call failed: {e}"
),
})?;
let default_impl: Persistent<Value<'static>> = Persistent::save(&ctx_ref, impl_val);
let registration_data: JsRegistrationData = JsRegistrationData {
contract_id,
contract_version,
contract_name,
functions,
factory,
};
Ok((registration_data, bridge, default_impl))
});
let (registration_data, bridge, default_impl): InitExtract = init_extract?;
let loader_data: SendVm = SendVm(Box::new(JsLoaderData {
functions: registration_data.functions,
factory: registration_data.factory,
bridge,
host_lo,
host_hi,
default_impl,
instances: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
contract_id: registration_data.contract_id,
ctx,
_runtime: qjs_runtime,
in_dispatch_threads: Mutex::new(Vec::new()),
logger: runtime.logger(),
}));
let loader_data_ptr: *const JsLoaderData = loader_data.as_ptr();
let contract_id: GuestContractId = GuestContractId::from_u64(registration_data.contract_id);
let major_version: u32 = registration_data.contract_version >> 16;
let plugin_interface: GuestContractInterface = GuestContractInterface {
contract_id,
contract_version: Version {
major: major_version,
minor: 0,
patch: 0,
},
dispatch_type: DispatchType::VirtualMachine,
create_instance: js_create_instance,
destroy_instance: js_destroy_instance,
dispatch: DispatchMechanisms {
vm: VmDispatch {
call: js_dispatch,
loader_data: VmLoaderData {
data: loader_data_ptr as *mut JsLoaderData as *mut core::ffi::c_void,
},
},
},
};
let interface_for_reg: GuestContractInterface = plugin_interface;
let static_interface: *const GuestContractInterface =
&interface_for_reg as *const GuestContractInterface;
let contract_name_owned: String = registration_data.contract_name;
let descriptor: PluginDescriptor = PluginDescriptor {
name: StringView::from_static(b"js-quickjs-plugin"),
contract_name: StringView {
ptr: contract_name_owned.as_ptr(),
len: contract_name_owned.len(),
},
version: Version {
major: major_version,
minor: 0,
patch: 0,
},
};
let mut abi_result: AbiError = AbiError::ok();
unsafe {
((*host_interface).register_guest_contract)(
host_interface,
&descriptor,
static_interface,
&mut abi_result,
)
};
if !abi_result.is_ok() {
self.schedule_reclaim(vec![loader_data]);
return Err(LoaderError::InitFailed {
bundle: manifest.name.clone(),
error: format!(
"JS runtime js-quickjs error: register_guest_contract returned error code {:?}",
abi_result.code
),
});
}
let superseded: Option<Vec<SendVm>> = {
let mut live: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<SendVm>>> =
self.live.lock().unwrap_or_else(PoisonError::into_inner);
live.insert(BundleId::from_u64(bundle_id), vec![loader_data])
};
if let Some(old_state) = superseded {
self.schedule_reclaim(old_state);
}
Ok(())
}
#[cfg(test)]
fn live_vm_count(&self, bundle_id: BundleId) -> usize {
let live: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<SendVm>>> =
self.live.lock().unwrap_or_else(PoisonError::into_inner);
live.get(&bundle_id).map(Vec::len).unwrap_or(0)
}
#[cfg(test)]
fn scheduled_reclaim_count(&self) -> u64 {
self.scheduled_reclaims.load(Ordering::Relaxed)
}
}
impl BundleLoader for JsLoader {
fn loader_name(&self) -> &'static str {
"js-quickjs"
}
fn loader_language(&self) -> SupportedLanguage {
SupportedLanguage::JavaScript
}
fn supports_hot_reload(&self) -> bool {
true
}
fn load(
&self,
manifest: &ManifestData,
source: &BundleSource,
runtime: &PolyplugRuntime,
) -> Result<(), LoaderError> {
match source {
BundleSource::Path(_) => {
let bundle_js: String = JsLoader::read_path_source(manifest)?;
self.load_inner(manifest, &bundle_js, Some(&manifest.path), runtime)
}
BundleSource::Code(code) => self.load_inner(manifest, code, None, runtime),
BundleSource::Bytes(bytes) => {
let code: &str = core::str::from_utf8(bytes).map_err(|_| {
LoaderError::InvalidSourceEncoding {
loader: "js-quickjs",
source_kind: source.kind(),
bundle: manifest.name.clone(),
}
})?;
self.load_inner(manifest, code, None, runtime)
}
}
}
fn reload(
&self,
manifest: &ManifestData,
runtime: &PolyplugRuntime,
) -> Result<(), LoaderError> {
let bundle_js: String = JsLoader::read_path_source(manifest)?;
self.load_inner(manifest, &bundle_js, Some(&manifest.path), runtime)
}
fn unload(&self, bundle_id: BundleId, _runtime: &PolyplugRuntime) -> Result<(), LoaderError> {
let state: Vec<SendVm> = {
let mut live: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<SendVm>>> =
self.live.lock().unwrap_or_else(PoisonError::into_inner);
match live.remove(&bundle_id) {
Some(v) => v,
None => return Ok(()),
}
};
self.schedule_reclaim(state);
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use core::sync::atomic::AtomicUsize;
use core::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Barrier;
use super::*;
#[test]
fn js_quickjs_loader_name() {
let loader: JsLoader = JsLoader::new(JsConfig {});
assert_eq!(loader.loader_name(), "js-quickjs");
}
fn unload_bundle_js(contract_id: u64, contract_name: &str) -> String {
let contract_lo: u32 = contract_id as u32;
let contract_hi: u32 = (contract_id >> 32) as u32;
format!(
r#"
function polyplug_init(host_lo, host_hi, ctx_lo, ctx_hi, bridge) {{
var iface = {{
factory: function(bridge, hostLo, hostHi) {{ return {{}}; }},
functions: [ function(impl, args, out, arena, bridge) {{ return 0; }} ]
}};
var registrations = [{{
contractLo: {contract_lo}, contractHi: {contract_hi}, interface: iface,
fnCount: 1, contractName: "{contract_name}", version: 0x00010000
}}];
return [registrations, {{ code: 0, message: "" }}];
}}
"#
)
}
fn write_unload_bundle(name: &str) -> (tempfile::TempDir, ManifestData) {
let contract_id: u64 = polyplug_utils::guest_contract_id("test.unload", 1);
let dir: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
std::fs::write(
dir.path().join("bundle.js"),
unload_bundle_js(contract_id, "test.unload@1"),
)
.expect("write bundle.js");
let manifest: ManifestData = ManifestData {
id: polyplug_utils::bundle_id(name),
name: name.to_owned(),
loader: "js-quickjs".to_owned(),
file: "bundle.js".to_owned(),
path: dir.path().to_path_buf(),
version: String::new(),
provides: Vec::new(),
function_count: HashMap::new(),
dependencies: Vec::new(),
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
};
(dir, manifest)
}
#[test]
fn unload_removes_live_and_schedules_reclaim() {
let loader: JsLoader = JsLoader::new(JsConfig {});
let runtime: Arc<PolyplugRuntime> = polyplug::runtime::RuntimeBuilder::new()
.loader(JsLoader::new(JsConfig {}))
.build()
.expect("runtime build must succeed");
let (_dir, manifest): (tempfile::TempDir, ManifestData) =
write_unload_bundle("js_unload_quiescent");
let bundle_id: BundleId = BundleId::from_u64(manifest.id);
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("load must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
1,
"the bundle's VM state must be owned after load"
);
loader
.unload(bundle_id, &runtime)
.expect("unload must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
0,
"unload must remove the bundle's VM state from the live map"
);
assert_eq!(
loader.scheduled_reclaim_count(),
1,
"unload must schedule the VM state for epoch-deferred reclaim"
);
}
#[test]
fn unload_load_loop_is_bounded() {
let loader: JsLoader = JsLoader::new(JsConfig {});
let runtime: Arc<PolyplugRuntime> = polyplug::runtime::RuntimeBuilder::new()
.loader(JsLoader::new(JsConfig {}))
.build()
.expect("runtime build must succeed");
let (_dir, manifest): (tempfile::TempDir, ManifestData) =
write_unload_bundle("js_unload_loop");
let bundle_id: BundleId = BundleId::from_u64(manifest.id);
for _ in 0..5 {
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("load must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
1,
"live map must hold exactly one entry per load"
);
loader
.unload(bundle_id, &runtime)
.expect("unload must succeed");
runtime
.registry()
.invalidate_bundle(bundle_id)
.expect("invalidate must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
0,
"unload must reclaim the entry each iteration"
);
}
assert_eq!(
loader.scheduled_reclaim_count(),
5,
"each of the 5 unloads must schedule its VM state for epoch-deferred reclaim"
);
}
#[test]
fn reload_replaces_live_and_reclaims_superseded_vm() {
let loader: JsLoader = JsLoader::new(JsConfig {});
let runtime: Arc<PolyplugRuntime> = polyplug::runtime::RuntimeBuilder::new()
.loader(JsLoader::new(JsConfig {}))
.build()
.expect("runtime build must succeed");
let (_dir, manifest): (tempfile::TempDir, ManifestData) =
write_unload_bundle("js_reload_reclaim");
let bundle_id: BundleId = BundleId::from_u64(manifest.id);
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("first load must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
1,
"first load installs exactly one live VM"
);
assert_eq!(
loader.scheduled_reclaim_count(),
0,
"nothing is superseded by the first load"
);
runtime
.registry()
.invalidate_bundle(bundle_id)
.expect("invalidate must succeed");
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("second load (reload) must succeed");
assert_eq!(
loader.live_vm_count(bundle_id),
1,
"reload replaces the live VM — the live map must not grow"
);
assert_eq!(
loader.scheduled_reclaim_count(),
1,
"reload must schedule the superseded VM for epoch-deferred reclaim"
);
}
#[test]
fn unload_schedules_reclaim_even_when_in_flight() {
let loader: JsLoader = JsLoader::new(JsConfig {});
let runtime: Arc<PolyplugRuntime> = polyplug::runtime::RuntimeBuilder::new()
.loader(JsLoader::new(JsConfig {}))
.build()
.expect("runtime build must succeed");
let (_dir, manifest): (tempfile::TempDir, ManifestData) =
write_unload_bundle("js_unload_deferred");
let bundle_id: BundleId = BundleId::from_u64(manifest.id);
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("load must succeed");
{
let live: std::sync::MutexGuard<'_, HashMap<BundleId, Vec<SendVm>>> =
loader.live.lock().unwrap_or_else(PoisonError::into_inner);
let state: &Vec<SendVm> = live.get(&bundle_id).expect("bundle must be live");
let mut threads: std::sync::MutexGuard<'_, Vec<ThreadId>> = state[0]
.data()
.in_dispatch_threads
.lock()
.unwrap_or_else(PoisonError::into_inner);
threads.push(std::thread::current().id());
}
loader
.unload(bundle_id, &runtime)
.expect("unload must succeed even when marked in-flight");
assert_eq!(
loader.live_vm_count(bundle_id),
0,
"unload must remove the bundle from the live map"
);
assert_eq!(
loader.scheduled_reclaim_count(),
1,
"unload must schedule epoch-deferred reclaim even when marked in-flight"
);
}
#[test]
fn js_guest_f64_param_and_return_round_trip() {
let loader: JsLoader = JsLoader::new(JsConfig {});
let runtime: Arc<PolyplugRuntime> = polyplug::runtime::RuntimeBuilder::new()
.loader(JsLoader::new(JsConfig {}))
.build()
.expect("runtime build must succeed");
let contract_id: u64 = polyplug_utils::guest_contract_id("test.float", 1);
let contract_lo: u32 = contract_id as u32;
let contract_hi: u32 = (contract_id >> 32) as u32;
let bundle_js: String = format!(
r#"
function polyplug_init(host_lo, host_hi, ctx_lo, ctx_hi, bridge) {{
var iface = {{
factory: function(bridge, hostLo, hostHi) {{ return {{}}; }},
functions: [ function(impl, args_ptr, out_ptr, arena, bridge) {{
var v = bridge.readF64(args_ptr);
bridge.writeF64(out_ptr, v * 2.0 + 0.25);
return 0;
}} ]
}};
var registrations = [{{
contractLo: {contract_lo}, contractHi: {contract_hi}, interface: iface,
fnCount: 1, contractName: "test.float@1", version: 0x00010000
}}];
return [registrations, {{ code: 0, message: "" }}];
}}
"#
);
let dir: tempfile::TempDir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("bundle.js"), bundle_js).expect("write bundle.js");
let manifest: ManifestData = ManifestData {
id: polyplug_utils::bundle_id("js_f64_round_trip"),
name: "js_f64_round_trip".to_owned(),
loader: "js-quickjs".to_owned(),
file: "bundle.js".to_owned(),
path: dir.path().to_path_buf(),
version: String::new(),
provides: Vec::new(),
function_count: HashMap::new(),
dependencies: Vec::new(),
needs_reinit_on_dep_reload: false,
bundle_dependencies: Vec::new(),
};
loader
.load(
&manifest,
&BundleSource::Path(manifest.path.clone()),
&runtime,
)
.expect("load must succeed");
let handle: GuestContractHandle = runtime
.find_guest_contract(contract_id, 0)
.expect("contract must be registered");
let iface: *const GuestContractInterface = runtime
.resolve_guest_contract(handle)
.expect("interface must resolve");
assert!(!iface.is_null(), "resolved interface must be non-null");
let arg: f64 = 1234.5625;
let mut out_val: f64 = 0.0;
let mut err: AbiError = AbiError::ok();
unsafe {
assert_eq!((*iface).dispatch_type, DispatchType::VirtualMachine);
((*iface).dispatch.vm.call)(
(*iface).dispatch.vm.loader_data,
GuestContractInstance::null(),
0,
&arg as *const f64 as *const (),
&mut out_val as *mut f64 as *mut (),
core::ptr::null_mut(),
&mut err,
)
};
assert_eq!(
err.code,
AbiErrorCode::Ok as u32,
"f64 dispatch must succeed"
);
assert_eq!(out_val, 2469.375, "f64 must round-trip exactly");
}
fn make_loader_data(
runtime: Runtime,
ctx: Context,
functions: Vec<Persistent<Function<'static>>>,
) -> (VmLoaderData, &'static JsLoaderData) {
type LoaderParts = (
Persistent<Function<'static>>,
Persistent<Value<'static>>,
Persistent<Object<'static>>,
);
let (factory, default_impl, bridge): LoaderParts = ctx.with(|ctx_ref: Ctx<'_>| {
let factory_fn: Function<'_> = ctx_ref
.eval::<Function<'_>, _>("(function(bridge, hostLo, hostHi) { return {}; })")
.expect("factory eval should produce a function");
let default_obj: Object<'_> =
Object::new(ctx_ref.clone()).expect("default impl object creation");
let bridge_obj: Object<'_> =
Object::new(ctx_ref.clone()).expect("bridge object creation");
(
Persistent::save(&ctx_ref, factory_fn),
Persistent::save(&ctx_ref, default_obj.into_value()),
Persistent::save(&ctx_ref, bridge_obj),
)
});
let boxed: Box<JsLoaderData> = Box::new(JsLoaderData {
functions,
factory,
bridge,
host_lo: 0.0,
host_hi: 0.0,
default_impl,
instances: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
contract_id: 0,
ctx,
_runtime: runtime,
in_dispatch_threads: Mutex::new(Vec::new()),
logger: LoggerHandle::default_stderr(),
});
let ptr: *mut JsLoaderData = Box::into_raw(boxed);
let data_ref: &'static JsLoaderData = unsafe { &*ptr };
let vm_loader_data: VmLoaderData = VmLoaderData {
data: ptr as *mut core::ffi::c_void,
};
(vm_loader_data, data_ref)
}
#[test]
fn js_dispatch_normal_call_succeeds() {
let runtime: Runtime = Runtime::new().expect("runtime creation should succeed");
let ctx: Context = Context::full(&runtime).expect("context creation should succeed");
let func: Persistent<Function<'static>> = ctx.with(|ctx_ref: Ctx<'_>| {
let f: Function<'_> = ctx_ref
.eval::<Function<'_>, _>("(function(a, o) { return 0; })")
.expect("eval should produce a function");
Persistent::save(&ctx_ref, f)
});
let (vm_loader_data, data_ref): (VmLoaderData, &'static JsLoaderData) =
make_loader_data(runtime, ctx, vec![func]);
let mut out_buf: i32 = 0;
let err: AbiError = unsafe {
js_dispatch_impl(
vm_loader_data,
GuestContractInstance::null(),
0,
core::ptr::null(),
&mut out_buf as *mut i32 as *mut (),
core::ptr::null_mut(),
)
};
assert!(err.is_ok(), "normal dispatch should return Ok");
assert!(
data_ref
.in_dispatch_threads
.lock()
.expect("tracking mutex must not be poisoned")
.is_empty(),
"thread tracking must be empty after a normal dispatch"
);
}
#[test]
fn js_dispatch_reentrant_call_is_rejected_and_vm_recovers() {
let runtime: Runtime = Runtime::new().expect("runtime creation should succeed");
let ctx: Context = Context::full(&runtime).expect("context creation should succeed");
let loader_data_cell: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
let cell_for_fn: Arc<AtomicUsize> = Arc::clone(&loader_data_cell);
let func: Persistent<Function<'static>> = ctx.with(|ctx_ref: Ctx<'_>| {
let reenter_fn: Function<'_> = Function::new(ctx_ref.clone(), move || -> f64 {
let ptr_usize: usize = cell_for_fn.load(Ordering::Acquire);
let vm_loader_data: VmLoaderData = VmLoaderData {
data: ptr_usize as *mut core::ffi::c_void,
};
let nested: AbiError = unsafe {
js_dispatch_impl(
vm_loader_data,
GuestContractInstance::null(),
0,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
nested.code as f64
})
.expect("reenter function creation should succeed");
ctx_ref
.globals()
.set("reenter", reenter_fn)
.expect("reenter global set should succeed");
let f: Function<'_> = ctx_ref
.eval::<Function<'_>, _>(
"(function(a, o) { globalThis._nestedCode = reenter(); return 0; })",
)
.expect("eval should produce a function");
Persistent::save(&ctx_ref, f)
});
let (vm_loader_data, data_ref): (VmLoaderData, &'static JsLoaderData) =
make_loader_data(runtime, ctx, vec![func]);
loader_data_cell.store(vm_loader_data.data as usize, Ordering::Release);
let outer: AbiError = unsafe {
js_dispatch_impl(
vm_loader_data,
GuestContractInstance::null(),
0,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
assert!(outer.is_ok(), "outer dispatch should complete Ok");
let nested_code: f64 = data_ref.ctx.with(|ctx_ref: Ctx<'_>| {
ctx_ref
.globals()
.get::<&str, f64>("_nestedCode")
.expect("nested code global must be set by the guest fn")
});
assert_eq!(
nested_code as u32,
AbiErrorCode::ReentrantCall as u32,
"nested same-VM dispatch must return ReentrantCall"
);
assert!(
data_ref
.in_dispatch_threads
.lock()
.expect("tracking mutex must not be poisoned")
.is_empty(),
"thread tracking must be empty after the outer dispatch returns"
);
let recovered: AbiError = unsafe {
js_dispatch_impl(
vm_loader_data,
GuestContractInstance::null(),
0,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
assert!(
recovered.is_ok(),
"VM must remain usable after a rejected reentrant call"
);
}
#[test]
fn js_dispatch_cross_thread_concurrent_call_succeeds() {
let runtime: Runtime = Runtime::new().expect("runtime creation should succeed");
let ctx: Context = Context::full(&runtime).expect("context creation should succeed");
let entered: Arc<Barrier> = Arc::new(Barrier::new(2));
let release: Arc<Barrier> = Arc::new(Barrier::new(2));
let entered_for_fn: Arc<Barrier> = Arc::clone(&entered);
let release_for_fn: Arc<Barrier> = Arc::clone(&release);
let func: Persistent<Function<'static>> = ctx.with(|ctx_ref: Ctx<'_>| {
let block_fn: Function<'_> = Function::new(ctx_ref.clone(), move || {
entered_for_fn.wait();
release_for_fn.wait();
})
.expect("block function creation should succeed");
ctx_ref
.globals()
.set("block", block_fn)
.expect("block global set should succeed");
let f: Function<'_> = ctx_ref
.eval::<Function<'_>, _>("(function(a, o) { block(); return 0; })")
.expect("eval should produce a function");
Persistent::save(&ctx_ref, f)
});
let noop_func: Persistent<Function<'static>> = ctx.with(|ctx_ref: Ctx<'_>| {
let f: Function<'_> = ctx_ref
.eval::<Function<'_>, _>("(function(a, o) { return 0; })")
.expect("eval should produce a function");
Persistent::save(&ctx_ref, f)
});
let (vm_loader_data, data_ref): (VmLoaderData, &'static JsLoaderData) =
make_loader_data(runtime, ctx, vec![func, noop_func]);
let data_addr: usize = vm_loader_data.data as usize;
let handle: std::thread::JoinHandle<AbiError> = std::thread::spawn(move || {
let vm_loader_data_a: VmLoaderData = VmLoaderData {
data: data_addr as *mut core::ffi::c_void,
};
unsafe {
js_dispatch_impl(
vm_loader_data_a,
GuestContractInstance::null(),
0,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
}
});
entered.wait();
let main_handle: std::thread::JoinHandle<AbiError> = std::thread::spawn(move || {
let vm_loader_data_b: VmLoaderData = VmLoaderData {
data: data_addr as *mut core::ffi::c_void,
};
unsafe {
js_dispatch_impl(
vm_loader_data_b,
GuestContractInstance::null(),
1,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
}
});
release.wait();
let a_result: AbiError = handle.join().expect("thread A must not panic");
let b_result: AbiError = main_handle
.join()
.expect("concurrent thread must not panic");
assert!(a_result.is_ok(), "the initial dispatch must succeed");
assert!(
b_result.is_ok(),
"a concurrent cross-thread dispatch must succeed, not return ReentrantCall (got code {})",
b_result.code
);
assert!(
data_ref
.in_dispatch_threads
.lock()
.expect("tracking mutex must not be poisoned")
.is_empty(),
"thread tracking must be empty after both dispatches return"
);
}
}