use std::cell::RefCell;
use std::mem::ManuallyDrop;
use std::ptr::{self, NonNull};
use std::sync::{Mutex, OnceLock};
use mozjs::jsapi::{JS_ShutDown, JSContext as RawJSContext, OnNewGlobalHookOption};
use mozjs::jsval::UndefinedValue;
use mozjs::realm::AutoRealm;
use mozjs::rooted;
use mozjs::rust::wrappers2::JS_NewGlobalObject;
use mozjs::rust::{RealmOptions, SIMPLE_GLOBAL_CLASS};
use crate::error::JsError;
use crate::host_fn;
use crate::job_queue::JobQueue;
use crate::module_loader::ModuleLoader;
use crate::value::{JsValue, jsval_to_jsvalue};
pub use crate::module_loader::{GlobalSetupFn, PostEvalHook};
pub struct JsContext {
cx: NonNull<RawJSContext>,
global_setup: Option<GlobalSetupFn>,
post_eval_hook: Option<PostEvalHook>,
realm_global: Option<Box<PersistentGlobal>>,
}
struct PersistentGlobal {
cx: *mut RawJSContext,
global_val: mozjs::jsval::JSVal,
}
impl Drop for PersistentGlobal {
fn drop(&mut self) {
if Self::cx_alive(self.cx) {
unsafe {
mozjs::jsapi::RemoveRawValueRoot(self.cx, &mut self.global_val);
}
}
}
}
fn raw_cx_alive(cx: *mut RawJSContext) -> bool {
!cx.is_null() && mozjs::rust::Runtime::get().map(|c| c.as_ptr()) == Some(cx)
}
impl PersistentGlobal {
fn cx_alive(cx: *mut RawJSContext) -> bool {
raw_cx_alive(cx)
}
fn global_ptr(&self) -> *mut mozjs::jsapi::JSObject {
if self.global_val.is_object() {
self.global_val.to_object()
} else {
::std::ptr::null_mut()
}
}
}
pub struct RawValueRootGuard {
cx: *mut RawJSContext,
vals: Box<[mozjs::jsval::JSVal]>,
}
impl RawValueRootGuard {
pub unsafe fn new(
cx: *mut RawJSContext,
vals: &[mozjs::jsval::JSVal],
name: &'static ::std::ffi::CStr,
) -> Option<Self> {
let mut slots: Box<[mozjs::jsval::JSVal]> = vals.to_vec().into_boxed_slice();
let mut rooted = 0usize;
for slot in slots.iter_mut() {
let ok =
unsafe { mozjs::jsapi::AddRawValueRoot(cx, slot, name.as_ptr()) };
if !ok {
for s in slots[..rooted].iter_mut() {
unsafe { mozjs::jsapi::RemoveRawValueRoot(cx, s) };
}
return None;
}
rooted += 1;
}
Some(RawValueRootGuard { cx, vals: slots })
}
pub fn get(&self, i: usize) -> mozjs::jsval::JSVal {
self.vals[i]
}
pub fn len(&self) -> usize {
self.vals.len()
}
pub fn into_inner(mut self) -> Option<Box<[mozjs::jsval::JSVal]>> {
if !raw_cx_alive(self.cx) {
let leaked = ::std::mem::take(&mut self.vals);
::std::mem::forget(leaked);
return None;
}
let mut vals = ::std::mem::take(&mut self.vals);
for slot in vals.iter_mut() {
unsafe { mozjs::jsapi::RemoveRawValueRoot(self.cx, slot) };
}
Some(vals)
}
}
impl Drop for RawValueRootGuard {
fn drop(&mut self) {
if raw_cx_alive(self.cx) {
for slot in self.vals.iter_mut() {
unsafe { mozjs::jsapi::RemoveRawValueRoot(self.cx, slot) };
}
} else {
let leaked = ::std::mem::take(&mut self.vals);
::std::mem::forget(leaked);
}
}
}
thread_local! {
static THREAD_REALM_GLOBAL: ::std::cell::Cell<*mut mozjs::jsapi::JSObject> =
const { ::std::cell::Cell::new(::std::ptr::null_mut()) };
}
pub fn thread_realm_global() -> Option<*mut mozjs::jsapi::JSObject> {
THREAD_REALM_GLOBAL.with(|c| {
let p = c.get();
if p.is_null() {
None
} else {
Some(p)
}
})
}
pub struct SmRuntimeGuard {
#[allow(dead_code)]
runtime: mozjs::rust::Runtime,
}
struct NeverDrop<T>(RefCell<ManuallyDrop<Option<T>>>);
impl<T> NeverDrop<T> {
const fn new() -> Self {
NeverDrop(RefCell::new(ManuallyDrop::new(None)))
}
fn is_some(&self) -> bool {
self.0.borrow().is_some()
}
fn set(&self, val: Option<T>) {
let mut borrow = self.0.borrow_mut();
if borrow.is_some() {
unsafe {
ManuallyDrop::drop(&mut *borrow);
}
}
*borrow = ManuallyDrop::new(val);
}
#[allow(dead_code)]
fn take(&self) -> Option<T> {
let mut borrow = self.0.borrow_mut();
if borrow.is_some() {
let val = unsafe { ManuallyDrop::take(&mut *borrow) };
*borrow = ManuallyDrop::new(None);
val
} else {
None
}
}
}
use std::sync::atomic::{AtomicBool, Ordering};
static ENGINE_SHUTDOWN: AtomicBool = AtomicBool::new(false);
static ENGINE_HANDLE: OnceLock<mozjs::rust::JSEngineHandle> = OnceLock::new();
static ENGINE_INIT_LOCK: Mutex<()> = Mutex::new(());
thread_local! {
static ENGINE_TLS: NeverDrop<mozjs::rust::JSEngine> = NeverDrop::new();
static RUNTIME_TLS: NeverDrop<mozjs::rust::Runtime> = NeverDrop::new();
}
pub fn ensure_engine_handle() -> Result<mozjs::rust::JSEngineHandle, JsError> {
if let Some(handle) = ENGINE_HANDLE.get() {
return Ok(handle.clone());
}
let _guard = ENGINE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
ensure_engine_handle_locked()
}
fn ensure_engine_handle_locked() -> Result<mozjs::rust::JSEngineHandle, JsError> {
if let Some(handle) = ENGINE_HANDLE.get() {
return Ok(handle.clone());
}
let (engine, handle) = ENGINE_TLS.with(|tls| {
if tls.is_some() {
let handle = tls
.0
.borrow()
.as_ref()
.expect("ENGINE_TLS is Some but inner is None")
.handle();
return Ok((None, handle));
}
match mozjs::rust::JSEngine::init() {
Ok(engine) => {
let handle = engine.handle();
tls.set(Some(engine));
Ok((Some(handle.clone()), handle))
}
Err(mozjs::rust::JSEngineError::AlreadyInitialized) => {
if let Some(h) = mozjs::rust::JSEngine::process_handle() {
return Ok((Some(h.clone()), h));
}
for _ in 0..50 {
if let Some(h) = ENGINE_HANDLE.get() {
return Ok((None, h.clone()));
}
if let Some(h) = mozjs::rust::JSEngine::process_handle() {
return Ok((Some(h.clone()), h));
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
Err(JsError {
message: "Failed to init JSEngine: AlreadyInitialized \
(no process handle published)"
.into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
})
}
Err(e) => Err(JsError {
message: format!("Failed to init JSEngine: {:?}", e).into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
}),
}
})?;
if let Some(handle_to_store) = engine {
let global_handle = ENGINE_HANDLE.get_or_init(|| handle_to_store);
Ok(global_handle.clone())
} else {
let global_handle = ENGINE_HANDLE.get_or_init(|| handle.clone());
Ok(global_handle.clone())
}
}
impl JsContext {
pub fn init_runtime() -> Result<(Self, Option<SmRuntimeGuard>), JsError> {
if mozjs::rust::Runtime::get().is_some() {
let ctx = unsafe { Self::from_servo_runtime()? };
return Ok((ctx, None));
}
let handle = ensure_engine_handle()?;
let runtime = mozjs::rust::Runtime::new(handle);
let cx = mozjs::rust::Runtime::get().ok_or_else(|| JsError {
message: "Runtime::new failed to set CONTEXT TLS".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
})?;
let mut cx_wrap = unsafe { mozjs::context::JSContext::from_ptr(cx) };
if !JobQueue::init(&mut cx_wrap) {
return Err(JsError {
message: "Failed to init job queue".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
ModuleLoader::init_thread_local(&cx_wrap);
crate::module_loader::set_job_queue_drain(JobQueue::drain);
let guard = SmRuntimeGuard { runtime };
crate::dispatch_sm::BaoEventLoop::register_js_context(cx.as_ptr().cast());
Ok((
JsContext {
cx,
global_setup: None,
post_eval_hook: None,
realm_global: None,
},
Some(guard),
))
}
pub unsafe fn from_servo_runtime() -> Result<Self, JsError> {
let cx = mozjs::rust::Runtime::get().ok_or_else(|| JsError {
message: "servo Runtime not initialized — call JsContext::init_runtime() first".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
})?;
let mut cx_wrap = unsafe { mozjs::context::JSContext::from_ptr(cx) };
if !JobQueue::init(&mut cx_wrap) {
return Err(JsError {
message: "Failed to init job queue".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
ModuleLoader::init_thread_local(&cx_wrap);
crate::module_loader::set_job_queue_drain(JobQueue::drain);
crate::dispatch_sm::BaoEventLoop::register_js_context(cx.as_ptr().cast());
Ok(JsContext {
cx,
global_setup: None,
post_eval_hook: None,
realm_global: None,
})
}
#[doc(hidden)]
pub fn for_test() -> Result<Self, JsError> {
let _init_guard = ENGINE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
if ENGINE_SHUTDOWN.load(Ordering::SeqCst) {
return Err(JsError {
message: "JSEngine has been shut down — cannot create Runtime".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
if mozjs::rust::Runtime::get().is_some() {
let cx = unsafe { Self::from_servo_runtime()? };
return Ok(cx);
}
let engine_handle = ensure_engine_handle_locked()?;
let runtime = mozjs::rust::Runtime::new(engine_handle);
let cx = mozjs::rust::Runtime::get().ok_or_else(|| JsError {
message: "Runtime::new failed to set CONTEXT TLS".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
})?;
let mut cx_wrap = unsafe { mozjs::context::JSContext::from_ptr(cx) };
if !JobQueue::init(&mut cx_wrap) {
return Err(JsError {
message: "Failed to init job queue".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
ModuleLoader::init_thread_local(&cx_wrap);
crate::module_loader::set_job_queue_drain(JobQueue::drain);
RUNTIME_TLS.with(|tls| tls.set(Some(runtime)));
crate::dispatch_sm::BaoEventLoop::register_js_context(cx.as_ptr().cast());
Ok(JsContext {
cx,
global_setup: None,
post_eval_hook: None,
realm_global: None,
})
}
#[doc(hidden)]
pub fn shutdown_test_runtime() {
Self::shutdown_thread_sm();
}
#[doc(hidden)]
pub fn shutdown_thread_sm() {
unsafe {
mozjs::gc::RootedTraceableSet::clear();
}
RUNTIME_TLS.with(|tls| {
if tls.is_some() {
let _ = tls.take(); }
});
#[cfg(unix)]
{
let mut sa: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe {
sa.sa_sigaction = libc::SIG_DFL as usize;
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(libc::SIGSEGV, &sa, std::ptr::null_mut());
libc::sigaction(libc::SIGBUS, &sa, std::ptr::null_mut());
libc::sigaction(libc::SIGILL, &sa, std::ptr::null_mut());
}
}
}
pub fn shutdown_engine() {
if ENGINE_SHUTDOWN.swap(true, Ordering::SeqCst) {
return; }
Self::shutdown_thread_sm();
ENGINE_TLS.with(|tls| {
if tls.is_some() {
unsafe {
JS_ShutDown();
}
if let Some(engine) = tls.take() {
std::mem::forget(engine);
}
}
});
}
pub fn cx(&self) -> mozjs::context::JSContext {
unsafe { mozjs::context::JSContext::from_ptr(self.cx) }
}
pub fn raw_cx(&self) -> *mut RawJSContext {
self.cx.as_ptr()
}
pub fn set_global_setup(&mut self, setup: GlobalSetupFn) {
self.global_setup = Some(setup);
}
pub fn set_post_eval_hook(&mut self, hook: PostEvalHook) {
self.post_eval_hook = Some(hook);
}
pub fn global_setup(&self) -> Option<GlobalSetupFn> {
self.global_setup
}
pub fn post_eval_hook(&self) -> Option<PostEvalHook> {
self.post_eval_hook
}
pub fn eval(&mut self, source: &str, filename: &str) -> Result<JsValue, JsError> {
let global_setup = self.global_setup;
let post_eval_hook = self.post_eval_hook;
let mut cx = self.cx();
let cx = &mut cx;
let global_ptr = self.ensure_realm_global(cx, global_setup)?;
rooted!(&in(cx) let global = global_ptr);
let c_filename = std::ffi::CString::new(filename)
.unwrap_or_else(|_| std::ffi::CString::new("<eval>").unwrap());
let compile_opts = mozjs::rust::CompileOptionsWrapper::new(cx, c_filename, 1);
rooted!(&in(cx) let mut rval = UndefinedValue());
{
let mut realm = AutoRealm::new_from_handle(cx, global.handle());
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
let result = mozjs::rust::evaluate_script(
realm_cx,
global.handle(),
source,
rval.handle_mut(),
compile_opts,
);
if result.is_err() {
return Err(extract_exception(realm_cx));
}
unsafe {
let raw_cx = realm_cx.raw_cx();
mozjs::jsapi::js::RunJobs(raw_cx);
if let Some(hook) = post_eval_hook {
loop {
mozjs::jsapi::js::RunJobs(raw_cx);
if !hook(realm_cx) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
}
}
Ok(unsafe { jsval_to_jsvalue(cx.raw_cx_no_gc(), rval.get()) })
}
pub fn ensure_realm_global(
&mut self,
cx: &mut mozjs::context::JSContext,
global_setup: Option<GlobalSetupFn>,
) -> Result<*mut mozjs::jsapi::JSObject, JsError> {
if let Some(ref pg) = self.realm_global {
return Ok(pg.global_ptr());
}
let options = RealmOptions::default();
rooted!(&in(cx) let global = unsafe {
JS_NewGlobalObject(
cx,
&SIMPLE_GLOBAL_CLASS,
ptr::null_mut(),
OnNewGlobalHookOption::FireOnNewGlobalHook,
&*options,
)
});
if global.get().is_null() {
return Err(JsError {
message: "Failed to create realm global".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
{
let mut realm = AutoRealm::new_from_handle(cx, global.handle());
let realm_cx: &mut mozjs::context::JSContext = &mut realm;
host_fn::install_console(realm_cx, global.handle());
if let Some(setup) = global_setup {
unsafe { setup(realm_cx, global.handle()) };
}
}
let global_ptr = global.get();
let mut pg = Box::new(PersistentGlobal {
cx: self.cx.as_ptr(),
global_val: mozjs::jsval::ObjectValue(global_ptr),
});
let rooted = unsafe {
mozjs::jsapi::AddRawValueRoot(
self.cx.as_ptr(),
&mut pg.global_val,
b"jscontext_realm_global\0".as_ptr() as *const ::std::os::raw::c_char,
)
};
if !rooted {
return Err(JsError {
message: "AddRawValueRoot failed for realm global".into(),
filename: "<engine>".into(),
line: 0,
column: 0,
stack: None,
});
}
THREAD_REALM_GLOBAL.with(|c| c.set(global_ptr));
self.realm_global = Some(pg);
Ok(global_ptr)
}
}
#[allow(unsafe_op_in_unsafe_fn)]
fn extract_exception(cx: &mut mozjs::context::JSContext) -> JsError {
rooted!(&in(cx) let mut exn = UndefinedValue());
if let Some(info) = unsafe {
mozjs::rust::error_info_from_exception_stack(cx.raw_cx_no_gc(), exn.handle_mut().into())
} {
JsError {
message: info.message,
filename: info.filename,
line: info.line,
column: info.col,
stack: None,
}
} else {
JsError {
message: "Unknown JS error".into(),
filename: "<unknown>".into(),
line: 0,
column: 0,
stack: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jscontext_has_cx_ptr_not_runtime() {
assert!(!std::any::type_name::<JsContext>().contains("Runtime"));
}
#[test]
fn jscontext_realm_global_needs_drop() {
assert!(std::mem::needs_drop::<JsContext>());
}
#[test]
fn sm_runtime_guard_holds_runtime_only() {
let size = std::mem::size_of::<SmRuntimeGuard>();
assert!(size > 0, "SmRuntimeGuard must be non-zero sized");
}
}