#![allow(dead_code, unused_imports)]
use alloc::{boxed::Box, ffi::CString};
use core::{mem, panic::AssertUnwindSafe, ptr::NonNull, result::Result as StdResult};
use rquickjs_sys::JSPromiseHookType;
use crate::allocator::{Allocator, AllocatorHolder};
#[cfg(feature = "loader")]
use crate::loader::{Loader, LoaderHolder, Resolver};
use crate::{
qjs::{self, size_t},
Ctx, Error, Result, Value,
};
use super::{opaque::Opaque, InterruptHandler, PromiseHook, PromiseHookType, RejectionTracker};
const DUMP_BYTECODE_FINAL: u64 = 0x01;
const DUMP_BYTECODE_PASS2: u64 = 0x02;
const DUMP_BYTECODE_PASS1: u64 = 0x04;
const DUMP_BYTECODE_HEX: u64 = 0x10;
const DUMP_BYTECODE_PC2LINE: u64 = 0x20;
const DUMP_BYTECODE_STACK: u64 = 0x40;
const DUMP_BYTECODE_STEP: u64 = 0x80;
const DUMP_READ_OBJECT: u64 = 0x100;
const DUMP_FREE: u64 = 0x200;
const DUMP_GC: u64 = 0x400;
const DUMP_GC_FREE: u64 = 0x800;
const DUMP_MODULE_RESOLVE: u64 = 0x1000;
const DUMP_PROMISE: u64 = 0x2000;
const DUMP_LEAKS: u64 = 0x4000;
const DUMP_ATOM_LEAKS: u64 = 0x8000;
const DUMP_MEM: u64 = 0x10000;
const DUMP_OBJECTS: u64 = 0x20000;
const DUMP_ATOMS: u64 = 0x40000;
const DUMP_SHAPES: u64 = 0x80000;
const fn build_dump_flags() -> u64 {
#[allow(unused_mut)]
let mut flags: u64 = 0;
#[cfg(feature = "dump-bytecode")]
{
flags |= DUMP_BYTECODE_FINAL | DUMP_BYTECODE_PASS2 | DUMP_BYTECODE_PASS1;
}
#[cfg(feature = "dump-gc")]
{
flags |= DUMP_GC;
}
#[cfg(feature = "dump-gc-free")]
{
flags |= DUMP_GC_FREE;
}
#[cfg(feature = "dump-free")]
{
flags |= DUMP_FREE;
}
#[cfg(feature = "dump-leaks")]
{
flags |= DUMP_LEAKS;
}
#[cfg(feature = "dump-mem")]
{
flags |= DUMP_MEM;
}
#[cfg(feature = "dump-objects")]
{
flags |= DUMP_OBJECTS;
}
#[cfg(feature = "dump-atoms")]
{
flags |= DUMP_ATOMS;
}
#[cfg(feature = "dump-shapes")]
{
flags |= DUMP_SHAPES;
}
#[cfg(feature = "dump-module-resolve")]
{
flags |= DUMP_MODULE_RESOLVE;
}
#[cfg(feature = "dump-promise")]
{
flags |= DUMP_PROMISE;
}
#[cfg(feature = "dump-read-object")]
{
flags |= DUMP_READ_OBJECT;
}
flags
}
#[derive(Debug)]
pub(crate) struct RawRuntime {
pub(crate) rt: NonNull<qjs::JSRuntime>,
#[allow(dead_code)]
pub info: Option<CString>,
#[allow(dead_code)]
pub allocator: Option<AllocatorHolder>,
#[cfg(feature = "loader")]
#[allow(dead_code)]
pub loader: Option<LoaderHolder>,
}
#[cfg(feature = "parallel")]
unsafe impl Send for RawRuntime {}
impl Drop for RawRuntime {
fn drop(&mut self) {
unsafe {
let ptr = qjs::JS_GetRuntimeOpaque(self.rt.as_ptr());
let mut opaque: Box<Opaque> = Box::from_raw(ptr as *mut _);
opaque.clear();
qjs::JS_FreeRuntime(self.rt.as_ptr());
mem::drop(opaque);
}
}
}
impl RawRuntime {
pub unsafe fn new(opaque: Opaque<'static>) -> Result<Self> {
#[cfg(not(feature = "rust-alloc"))]
return Self::new_base(opaque);
#[cfg(feature = "rust-alloc")]
Self::new_with_allocator(opaque, crate::allocator::RustAllocator)
}
#[allow(dead_code)]
pub unsafe fn new_base(mut opaque: Opaque<'static>) -> Result<Self> {
let rt = qjs::JS_NewRuntime();
Self::add_dump_flags(rt);
let rt = NonNull::new(rt).ok_or(Error::Allocation)?;
opaque.initialize(rt.as_ptr())?;
let opaque = Box::into_raw(Box::new(opaque));
unsafe { qjs::JS_SetRuntimeOpaque(rt.as_ptr(), opaque as *mut _) };
Ok(RawRuntime {
rt,
info: None,
allocator: None,
#[cfg(feature = "loader")]
loader: None,
})
}
pub unsafe fn new_with_allocator<A>(mut opaque: Opaque<'static>, allocator: A) -> Result<Self>
where
A: Allocator + 'static,
{
let allocator = AllocatorHolder::new(allocator);
let functions = AllocatorHolder::functions::<A>();
let opaque_ptr = allocator.opaque_ptr();
let rt = qjs::JS_NewRuntime2(&functions, opaque_ptr as _);
Self::add_dump_flags(rt);
let rt = NonNull::new(rt).ok_or(Error::Allocation)?;
opaque.initialize(rt.as_ptr())?;
let opaque = Box::into_raw(Box::new(opaque));
unsafe { qjs::JS_SetRuntimeOpaque(rt.as_ptr(), opaque as *mut _) };
Ok(RawRuntime {
rt,
info: None,
allocator: Some(allocator),
#[cfg(feature = "loader")]
loader: None,
})
}
pub fn update_stack_top(&self) {
#[cfg(feature = "parallel")]
unsafe {
qjs::JS_UpdateStackTop(self.rt.as_ptr());
}
}
pub fn get_opaque<'js>(&self) -> &Opaque<'js> {
unsafe { &*(qjs::JS_GetRuntimeOpaque(self.rt.as_ptr()) as *mut _) }
}
pub fn is_job_pending(&self) -> bool {
(unsafe { qjs::JS_IsJobPending(self.rt.as_ptr()) } as i32) != 0
}
pub fn execute_pending_job(&mut self) -> StdResult<bool, *mut qjs::JSContext> {
let mut ctx_ptr = mem::MaybeUninit::<*mut qjs::JSContext>::uninit();
let result = unsafe { qjs::JS_ExecutePendingJob(self.rt.as_ptr(), ctx_ptr.as_mut_ptr()) };
if result == 0 {
return Ok(false);
}
if result == 1 {
return Ok(true);
}
Err(unsafe { ctx_ptr.assume_init() })
}
#[cfg(feature = "loader")]
pub unsafe fn set_loader<R, L>(&mut self, resolver: R, loader: L)
where
R: Resolver + 'static,
L: Loader + 'static,
{
let loader = LoaderHolder::new(resolver, loader);
loader.set_to_runtime(self.rt.as_ptr());
self.loader = Some(loader);
}
pub unsafe fn set_info(&mut self, info: CString) {
unsafe { qjs::JS_SetRuntimeInfo(self.rt.as_ptr(), info.as_ptr()) };
self.info = Some(info);
}
pub unsafe fn set_memory_limit(&mut self, limit: usize) {
let limit: size_t = limit.try_into().unwrap_or(size_t::MAX);
qjs::JS_SetMemoryLimit(self.rt.as_ptr(), limit)
}
pub unsafe fn set_max_stack_size(&mut self, limit: usize) {
let limit: size_t = limit.try_into().unwrap_or(size_t::MAX);
qjs::JS_SetMaxStackSize(self.rt.as_ptr(), limit);
}
pub unsafe fn set_gc_threshold(&self, threshold: usize) {
qjs::JS_SetGCThreshold(self.rt.as_ptr(), threshold as _);
}
pub unsafe fn set_dump_flags(&self, flags: u64) {
qjs::JS_SetDumpFlags(self.rt.as_ptr(), flags);
}
pub unsafe fn run_gc(&mut self) {
qjs::JS_RunGC(self.rt.as_ptr());
}
pub unsafe fn memory_usage(&mut self) -> qjs::JSMemoryUsage {
let mut stats = mem::MaybeUninit::uninit();
qjs::JS_ComputeMemoryUsage(self.rt.as_ptr(), stats.as_mut_ptr());
stats.assume_init()
}
#[allow(clippy::unnecessary_cast)]
pub unsafe fn set_promise_hook(&mut self, hook: Option<PromiseHook>) {
unsafe extern "C" fn promise_hook_wrapper(
ctx: *mut rquickjs_sys::JSContext,
type_: JSPromiseHookType,
promise: rquickjs_sys::JSValue,
parent: rquickjs_sys::JSValue,
opaque: *mut ::core::ffi::c_void,
) {
let opaque = NonNull::new_unchecked(opaque).cast::<Opaque>();
let catch_unwind = crate::util::catch_unwind(AssertUnwindSafe(move || {
let ctx = Ctx::from_ptr(ctx);
const INIT: u32 = qjs::JSPromiseHookType_JS_PROMISE_HOOK_INIT as u32;
const BEFORE: u32 = qjs::JSPromiseHookType_JS_PROMISE_HOOK_BEFORE as u32;
const AFTER: u32 = qjs::JSPromiseHookType_JS_PROMISE_HOOK_AFTER as u32;
const RESOLVE: u32 = qjs::JSPromiseHookType_JS_PROMISE_HOOK_RESOLVE as u32;
let rtype = match type_ as u32 {
INIT => PromiseHookType::Init,
BEFORE => PromiseHookType::Before,
AFTER => PromiseHookType::After,
RESOLVE => PromiseHookType::Resolve,
_ => unreachable!(),
};
opaque.as_ref().run_promise_hook(
ctx.clone(),
rtype,
Value::from_js_value_const(ctx.clone(), promise),
Value::from_js_value_const(ctx, parent),
);
}));
match catch_unwind {
Ok(_) => {}
Err(panic) => {
opaque.as_ref().set_panic(panic);
}
}
}
qjs::JS_SetPromiseHook(
self.rt.as_ptr(),
hook.as_ref().map(|_| {
promise_hook_wrapper
as unsafe extern "C" fn(
*mut rquickjs_sys::JSContext,
JSPromiseHookType,
rquickjs_sys::JSValue,
rquickjs_sys::JSValue,
*mut core::ffi::c_void,
)
}),
qjs::JS_GetRuntimeOpaque(self.rt.as_ptr()),
);
self.get_opaque().set_promise_hook(hook);
}
pub unsafe fn set_host_promise_rejection_tracker(&mut self, tracker: Option<RejectionTracker>) {
unsafe extern "C" fn rejection_tracker_wrapper(
ctx: *mut rquickjs_sys::JSContext,
promise: rquickjs_sys::JSValue,
reason: rquickjs_sys::JSValue,
is_handled: bool,
opaque: *mut ::core::ffi::c_void,
) {
let opaque = NonNull::new_unchecked(opaque).cast::<Opaque>();
let catch_unwind = crate::util::catch_unwind(AssertUnwindSafe(move || {
let ctx = Ctx::from_ptr(ctx);
opaque.as_ref().run_rejection_tracker(
ctx.clone(),
Value::from_js_value_const(ctx.clone(), promise),
Value::from_js_value_const(ctx, reason),
is_handled,
);
}));
match catch_unwind {
Ok(_) => {}
Err(panic) => {
opaque.as_ref().set_panic(panic);
}
}
}
qjs::JS_SetHostPromiseRejectionTracker(
self.rt.as_ptr(),
tracker.as_ref().map(|_| rejection_tracker_wrapper as _),
qjs::JS_GetRuntimeOpaque(self.rt.as_ptr()),
);
self.get_opaque().set_rejection_tracker(tracker);
}
pub unsafe fn set_interrupt_handler(&mut self, handler: Option<InterruptHandler>) {
unsafe extern "C" fn interrupt_handler_trampoline(
_rt: *mut qjs::JSRuntime,
opaque: *mut ::core::ffi::c_void,
) -> ::core::ffi::c_int {
let opaque = NonNull::new_unchecked(opaque).cast::<Opaque>();
let should_interrupt = {
let catch_unwind = crate::util::catch_unwind(AssertUnwindSafe(move || {
opaque.as_ref().run_interrupt_handler()
}));
match catch_unwind {
Ok(should_interrupt) => should_interrupt,
Err(panic) => {
opaque.as_ref().set_panic(panic);
true
}
}
};
should_interrupt as _
}
qjs::JS_SetInterruptHandler(
self.rt.as_ptr(),
handler.as_ref().map(|_| interrupt_handler_trampoline as _),
qjs::JS_GetRuntimeOpaque(self.rt.as_ptr()),
);
self.get_opaque().set_interrupt_handler(handler);
}
fn add_dump_flags(rt: *mut rquickjs_sys::JSRuntime) {
unsafe {
qjs::JS_SetDumpFlags(rt, build_dump_flags());
}
}
}
#[cfg(test)]
mod test {
use std::sync::{Arc, Mutex};
use crate::{Context, Runtime};
#[test]
fn promise_rejection_handler() {
let counter = Arc::new(Mutex::new(0));
let rt = Runtime::new().unwrap();
{
let counter = counter.clone();
rt.set_host_promise_rejection_tracker(Some(Box::new(move |_, _, _, is_handled| {
if !is_handled {
let mut c = counter.lock().unwrap();
*c += 1;
}
})));
}
let context = Context::full(&rt).unwrap();
context.with(|ctx| {
let _: Result<(), _> = ctx.eval(
r#"
const x = async () => {
throw new Error("Uncaught")
}
x()
throw new Error("Caught")
"#,
);
});
assert_eq!(*counter.lock().unwrap(), 1);
}
}