use std::ffi::c_void;
use std::ptr::NonNull;
use std::rc::Rc;
use js::context::JSContext;
use js::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps, RustJobQueue};
use js::jsapi::{
GetExecutionGlobalFromJSMicroTask, GetPromiseUserInputEventHandlingState, IsJSMicroTask,
JSContext as RawJSContext, JSTracer, MaybeGetPromiseFromJSMicroTask, MutableHandleObject,
PromiseUserInputEventHandlingState, ToMaybeWrappedJSMicroTask,
};
use js::jsval::{JSVal, PrivateValue};
use js::panic::wrap_panic;
use js::realm::AutoRealm;
use js::rust::wrappers2::{
EnqueueMicroTask, GetJobQueue, HasAnyMicroTasks, JS_DequeueNextMicroTask, JobQueueIsEmpty,
JobQueueMayNotBeEmpty, MaybeGetHostDefinedDataFromJSMicroTask, RunJSMicroTask, SetJobQueue,
};
use malloc_size_of::MallocSizeOf;
use script_bindings::reflector::DomObject as _;
use script_bindings::root::Dom;
use script_bindings::settings_stack::{run_a_callback, run_a_script};
use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::event_loop::script_thread::ScriptThread;
use crate::realms::enter_auto_realm;
use crate::runtime::script_runtime::notify_about_rejected_promises;
use crate::{DomTypeHolder, JSTraceable};
pub(crate) static JOB_QUEUE_TRAPS: JobQueueTraps = JobQueueTraps {
getHostDefinedData: Some(get_host_defined_data),
getHostDefinedGlobal: Some(get_host_defined_global),
runJobs: Some(run_jobs),
traceNonGCThingMicroTask: Some(trace_non_gc_things_micro_task),
};
pub(crate) struct JobQueue(*mut RustJobQueue);
#[expect(unsafe_code)]
unsafe impl JSTraceable for JobQueue {
unsafe fn trace(&self, _trc: *mut JSTracer) {
}
}
impl MallocSizeOf for JobQueue {
fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
0
}
}
impl JobQueue {
#[expect(unsafe_code)]
pub(crate) fn new() -> Self {
JobQueue(unsafe { CreateJobQueue(&JOB_QUEUE_TRAPS) })
}
#[expect(unsafe_code)]
pub(crate) fn set_on_context(&self, cx: &JSContext) {
unsafe { SetJobQueue(cx, self.0 as *mut _) };
}
}
impl Drop for JobQueue {
#[expect(unsafe_code)]
fn drop(&mut self) {
unsafe {
DeleteJobQueue(self.0);
}
}
}
#[expect(unsafe_code)]
unsafe extern "C" fn get_host_defined_data(
cx: *mut RawJSContext,
incumbent_global: MutableHandleObject,
data: MutableHandleObject,
) -> bool {
incumbent_global.set(std::ptr::null_mut());
data.set(std::ptr::null_mut());
if !unsafe { get_host_defined_global(cx, incumbent_global) } {
return false;
}
if incumbent_global.is_null() {
return true;
}
true
}
#[allow(unsafe_code)]
unsafe extern "C" fn get_host_defined_global(
_cx: *mut RawJSContext,
out: MutableHandleObject,
) -> bool {
wrap_panic(&mut || {
let Some(incumbent_global) = GlobalScope::incumbent() else {
return;
};
out.set(incumbent_global.reflector().get_jsobject().get());
});
true
}
#[expect(unsafe_code)]
unsafe extern "C" fn run_jobs(cx: *mut RawJSContext) {
let mut cx = unsafe {
JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
};
wrap_panic(&mut || {
job_queue_microtask_checkpoint(&mut cx, vec![]);
});
}
#[derive(JSTraceable, MallocSizeOf)]
pub struct NotifyMutationObserversMicrotask;
impl NotifyMutationObserversMicrotask {
pub(crate) fn new() -> Self {
Self
}
}
impl MicrotaskRunnable for NotifyMutationObserversMicrotask {
fn handler(&self, cx: &mut JSContext) {
ScriptThread::mutation_observers().notify_mutation_observers(cx);
}
}
#[derive(JSTraceable, MallocSizeOf)]
pub struct CustomElementReactionMicrotask;
impl CustomElementReactionMicrotask {
pub(crate) fn new() -> Self {
Self
}
}
impl MicrotaskRunnable for CustomElementReactionMicrotask {
fn handler(&self, cx: &mut JSContext) {
ScriptThread::invoke_backup_element_queue(cx);
}
}
pub(crate) trait MicrotaskRunnable: JSTraceable + MallocSizeOf {
fn handler(&self, _cx: &mut JSContext) {}
}
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct UserMicrotask {
#[conditional_malloc_size_of]
pub(crate) callback: Rc<VoidFunction>,
pub(crate) global: Dom<GlobalScope>,
}
impl MicrotaskRunnable for UserMicrotask {
fn handler(&self, cx: &mut JSContext) {
let mut realm = enter_auto_realm(cx, &*self.global);
let cx = &mut realm;
let _ = self
.callback
.Call_(cx, &*self.global, ExceptionHandling::Report);
}
}
fn microtask_from_jsval(val: JSVal) -> *mut Box<dyn MicrotaskRunnable> {
val.to_private() as *const Box<dyn MicrotaskRunnable> as *mut Box<dyn MicrotaskRunnable>
}
#[expect(unsafe_code)]
pub(crate) fn enqueue(cx: &JSContext, task: Box<dyn MicrotaskRunnable>) {
let task = Box::new(task);
let raw = Box::into_raw(task);
unsafe { JobQueueMayNotBeEmpty(cx) };
assert!(unsafe { EnqueueMicroTask(cx, &PrivateValue(raw as *const c_void)) });
}
#[expect(unsafe_code)]
pub(crate) fn job_queue_microtask_checkpoint(
cx: &mut JSContext,
globalscopes: Vec<DomRoot<GlobalScope>>,
) {
let job_queue: *mut RustJobQueue = unsafe { GetJobQueue(cx) } as _;
if unsafe { (*job_queue).draining } {
return;
}
unsafe {
(*job_queue).draining = true;
}
debug!("Now performing a microtask checkpoint");
rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
rooted!(&in(cx) let mut js_micro_task: *mut js::jsapi::JSMicroTask);
rooted!(&in(cx) let mut execution_global: *mut js::jsapi::JSObject);
rooted!(&in(cx) let mut incumbent_global: *mut js::jsapi::JSObject);
rooted!(&in(cx) let mut data: *mut js::jsapi::JSObject);
while unsafe { HasAnyMicroTasks(cx) } {
unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
if !unsafe { HasAnyMicroTasks(cx) } {
unsafe { JobQueueIsEmpty(cx) };
}
if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
rooted!(&in(cx) let task = unsafe {
Box::from_raw(
microtask_from_jsval(*generic_task),
)
});
task.handler(cx);
continue;
}
js_micro_task.set(unsafe { ToMaybeWrappedJSMicroTask(generic_task.as_ptr()) });
execution_global.set(unsafe { GetExecutionGlobalFromJSMicroTask(js_micro_task.get()) });
if execution_global.get().is_null() {
continue;
}
if !unsafe {
MaybeGetHostDefinedDataFromJSMicroTask(
js_micro_task.get(),
incumbent_global.handle_mut(),
data.handle_mut(),
)
} {
continue;
}
let interaction = if let Some(promise) =
NonNull::new(unsafe { MaybeGetPromiseFromJSMicroTask(js_micro_task.get()) })
{
unsafe { GetPromiseUserInputEventHandlingState(promise.as_ptr()) }
} else {
PromiseUserInputEventHandlingState::DontCare
};
let _maybe_user_interacting_guard =
if interaction == PromiseUserInputEventHandlingState::HadUserInteractionAtCreation {
Some(ScriptThread::user_interacting_guard())
} else {
None
};
let global_scope = unsafe { GlobalScope::from_object(execution_global.get()) };
run_a_script::<DomTypeHolder, _, _>(cx, &global_scope, |cx| {
let mut r = || {
let mut realm = AutoRealm::new_from_handle(cx, execution_global.handle());
let _ = unsafe { RunJSMicroTask(&mut realm, js_micro_task.handle()) };
};
if incumbent_global.get().is_null() {
r();
} else {
let global_scope = unsafe { GlobalScope::from_object(incumbent_global.get()) };
run_a_callback::<DomTypeHolder, _>(&global_scope, r);
}
});
}
for global in globalscopes.clone().into_iter() {
notify_about_rejected_promises(cx, &global);
}
for global in globalscopes.iter() {
if let Some(factory) = global.indexeddb_factory() {
let _ = factory.cleanup_indexeddb_transactions(cx);
}
}
unsafe {
(*job_queue).draining = false;
}
}
#[expect(unsafe_code)]
pub(crate) fn job_queue_clear(cx: &JSContext) {
rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
while unsafe { HasAnyMicroTasks(cx) } {
unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
let task = unsafe { Box::from_raw(microtask_from_jsval(*generic_task)) };
drop(task);
}
}
}
#[expect(unsafe_code)]
unsafe extern "C" fn trace_non_gc_things_micro_task(trc: *mut JSTracer, val: *mut JSVal) {
wrap_panic(&mut || {
let task = microtask_from_jsval(unsafe { *val });
unsafe { (**task).trace(trc) };
})
}