script/runtime/
job_queue.rs1use std::ffi::c_void;
10use std::ptr::NonNull;
11use std::rc::Rc;
12
13use js::context::JSContext;
14use js::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps, RustJobQueue};
15use js::jsapi::{
16 GetExecutionGlobalFromJSMicroTask, GetPromiseUserInputEventHandlingState, IsJSMicroTask,
17 JSContext as RawJSContext, JSTracer, MaybeGetPromiseFromJSMicroTask, MutableHandleObject,
18 PromiseUserInputEventHandlingState, ToMaybeWrappedJSMicroTask,
19};
20use js::jsval::{JSVal, PrivateValue};
21use js::panic::wrap_panic;
22use js::realm::AutoRealm;
23use js::rust::wrappers2::{
24 EnqueueMicroTask, GetJobQueue, HasAnyMicroTasks, JS_DequeueNextMicroTask, JobQueueIsEmpty,
25 JobQueueMayNotBeEmpty, MaybeGetHostDefinedDataFromJSMicroTask, RunJSMicroTask, SetJobQueue,
26};
27use malloc_size_of::MallocSizeOf;
28use script_bindings::reflector::DomObject as _;
29use script_bindings::root::Dom;
30use script_bindings::settings_stack::{run_a_callback, run_a_script};
31
32use crate::dom::bindings::callback::ExceptionHandling;
33use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
34use crate::dom::bindings::root::DomRoot;
35use crate::dom::globalscope::GlobalScope;
36use crate::event_loop::script_thread::ScriptThread;
37use crate::realms::enter_auto_realm;
38use crate::runtime::script_runtime::notify_about_rejected_promises;
39use crate::{DomTypeHolder, JSTraceable};
40
41pub(crate) static JOB_QUEUE_TRAPS: JobQueueTraps = JobQueueTraps {
42 getHostDefinedData: Some(get_host_defined_data),
43 getHostDefinedGlobal: Some(get_host_defined_global),
44 runJobs: Some(run_jobs),
45 traceNonGCThingMicroTask: Some(trace_non_gc_things_micro_task),
46};
47
48pub(crate) struct JobQueue(*mut RustJobQueue);
49
50#[expect(unsafe_code)]
51unsafe impl JSTraceable for JobQueue {
52 unsafe fn trace(&self, _trc: *mut JSTracer) {
53 }
55}
56
57impl MallocSizeOf for JobQueue {
58 fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
59 0
61 }
62}
63
64impl JobQueue {
65 #[expect(unsafe_code)]
66 pub(crate) fn new() -> Self {
67 JobQueue(unsafe { CreateJobQueue(&JOB_QUEUE_TRAPS) })
68 }
69
70 #[expect(unsafe_code)]
71 pub(crate) fn set_on_context(&self, cx: &JSContext) {
72 unsafe { SetJobQueue(cx, self.0 as *mut _) };
73 }
74}
75
76impl Drop for JobQueue {
77 #[expect(unsafe_code)]
78 fn drop(&mut self) {
79 unsafe {
80 DeleteJobQueue(self.0);
81 }
82 }
83}
84
85#[expect(unsafe_code)]
87unsafe extern "C" fn get_host_defined_data(
88 cx: *mut RawJSContext,
89 incumbent_global: MutableHandleObject,
90 data: MutableHandleObject,
91) -> bool {
92 incumbent_global.set(std::ptr::null_mut());
93 data.set(std::ptr::null_mut());
94 if !unsafe { get_host_defined_global(cx, incumbent_global) } {
95 return false;
96 }
97
98 if incumbent_global.is_null() {
99 return true;
100 }
101
102 true
105}
106
107#[allow(unsafe_code)]
108unsafe extern "C" fn get_host_defined_global(
110 _cx: *mut RawJSContext,
111 out: MutableHandleObject,
112) -> bool {
113 wrap_panic(&mut || {
114 let Some(incumbent_global) = GlobalScope::incumbent() else {
115 return;
116 };
117
118 out.set(incumbent_global.reflector().get_jsobject().get());
119 });
120
121 true
122}
123
124#[expect(unsafe_code)]
125unsafe extern "C" fn run_jobs(cx: *mut RawJSContext) {
126 let mut cx = unsafe {
127 JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
129 };
130 wrap_panic(&mut || {
131 job_queue_microtask_checkpoint(&mut cx, vec![]);
134 });
135}
136
137#[derive(JSTraceable, MallocSizeOf)]
138pub struct NotifyMutationObserversMicrotask;
139
140impl NotifyMutationObserversMicrotask {
141 pub(crate) fn new() -> Self {
142 Self
143 }
144}
145
146impl MicrotaskRunnable for NotifyMutationObserversMicrotask {
147 fn handler(&self, cx: &mut JSContext) {
148 ScriptThread::mutation_observers().notify_mutation_observers(cx);
149 }
150}
151
152#[derive(JSTraceable, MallocSizeOf)]
153pub struct CustomElementReactionMicrotask;
154
155impl CustomElementReactionMicrotask {
156 pub(crate) fn new() -> Self {
157 Self
158 }
159}
160
161impl MicrotaskRunnable for CustomElementReactionMicrotask {
162 fn handler(&self, cx: &mut JSContext) {
163 ScriptThread::invoke_backup_element_queue(cx);
164 }
165}
166
167pub(crate) trait MicrotaskRunnable: JSTraceable + MallocSizeOf {
168 fn handler(&self, _cx: &mut JSContext) {}
170}
171
172#[derive(JSTraceable, MallocSizeOf)]
174#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
175pub(crate) struct UserMicrotask {
176 #[conditional_malloc_size_of]
177 pub(crate) callback: Rc<VoidFunction>,
178 pub(crate) global: Dom<GlobalScope>,
179}
180
181impl MicrotaskRunnable for UserMicrotask {
182 fn handler(&self, cx: &mut JSContext) {
183 let mut realm = enter_auto_realm(cx, &*self.global);
184 let cx = &mut realm;
185 let _ = self
186 .callback
187 .Call_(cx, &*self.global, ExceptionHandling::Report);
188 }
189}
190
191fn microtask_from_jsval(val: JSVal) -> *mut Box<dyn MicrotaskRunnable> {
192 val.to_private() as *const Box<dyn MicrotaskRunnable> as *mut Box<dyn MicrotaskRunnable>
193}
194
195#[expect(unsafe_code)]
198pub(crate) fn enqueue(cx: &JSContext, task: Box<dyn MicrotaskRunnable>) {
199 let task = Box::new(task);
200 let raw = Box::into_raw(task);
201 unsafe { JobQueueMayNotBeEmpty(cx) };
202 assert!(unsafe { EnqueueMicroTask(cx, &PrivateValue(raw as *const c_void)) });
203}
204
205#[expect(unsafe_code)]
208pub(crate) fn job_queue_microtask_checkpoint(
209 cx: &mut JSContext,
210 globalscopes: Vec<DomRoot<GlobalScope>>,
211) {
212 let job_queue: *mut RustJobQueue = unsafe { GetJobQueue(cx) } as _;
213 if unsafe { (*job_queue).draining } {
215 return;
216 }
217
218 unsafe {
220 (*job_queue).draining = true;
221 }
222
223 debug!("Now performing a microtask checkpoint");
224
225 rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
226 rooted!(&in(cx) let mut js_micro_task: *mut js::jsapi::JSMicroTask);
227 rooted!(&in(cx) let mut execution_global: *mut js::jsapi::JSObject);
228 rooted!(&in(cx) let mut incumbent_global: *mut js::jsapi::JSObject);
229 rooted!(&in(cx) let mut data: *mut js::jsapi::JSObject);
230
231 while unsafe { HasAnyMicroTasks(cx) } {
236 unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
237
238 if !unsafe { HasAnyMicroTasks(cx) } {
241 unsafe { JobQueueIsEmpty(cx) };
242 }
243
244 if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
246 rooted!(&in(cx) let task = unsafe {
247 Box::from_raw(
248 microtask_from_jsval(*generic_task),
249 )
250 });
251 task.handler(cx);
252 continue;
253 }
254
255 js_micro_task.set(unsafe { ToMaybeWrappedJSMicroTask(generic_task.as_ptr()) });
256 execution_global.set(unsafe { GetExecutionGlobalFromJSMicroTask(js_micro_task.get()) });
257 if execution_global.get().is_null() {
258 continue;
259 }
260 if !unsafe {
261 MaybeGetHostDefinedDataFromJSMicroTask(
262 js_micro_task.get(),
263 incumbent_global.handle_mut(),
264 data.handle_mut(),
265 )
266 } {
267 continue;
268 }
269
270 let interaction = if let Some(promise) =
271 NonNull::new(unsafe { MaybeGetPromiseFromJSMicroTask(js_micro_task.get()) })
272 {
273 unsafe { GetPromiseUserInputEventHandlingState(promise.as_ptr()) }
274 } else {
275 PromiseUserInputEventHandlingState::DontCare
276 };
277 let _maybe_user_interacting_guard =
278 if interaction == PromiseUserInputEventHandlingState::HadUserInteractionAtCreation {
279 Some(ScriptThread::user_interacting_guard())
280 } else {
281 None
282 };
283 let global_scope = unsafe { GlobalScope::from_object(execution_global.get()) };
284 run_a_script::<DomTypeHolder, _, _>(cx, &global_scope, |cx| {
285 let mut r = || {
286 let mut realm = AutoRealm::new_from_handle(cx, execution_global.handle());
287 let _ = unsafe { RunJSMicroTask(&mut realm, js_micro_task.handle()) };
288 };
289 if incumbent_global.get().is_null() {
290 r();
291 } else {
292 let global_scope = unsafe { GlobalScope::from_object(incumbent_global.get()) };
293 run_a_callback::<DomTypeHolder, _>(&global_scope, r);
294 }
295 });
296 }
297
298 for global in globalscopes.clone().into_iter() {
302 notify_about_rejected_promises(cx, &global);
303 }
304
305 for global in globalscopes.iter() {
311 if let Some(factory) = global.indexeddb_factory() {
312 let _ = factory.cleanup_indexeddb_transactions(cx);
313 }
314 }
315
316 unsafe {
320 (*job_queue).draining = false;
321 }
322 }
324
325#[expect(unsafe_code)]
326pub(crate) fn job_queue_clear(cx: &JSContext) {
327 rooted!(&in(cx) let mut generic_task: js::jsapi::GenericMicroTask);
328 while unsafe { HasAnyMicroTasks(cx) } {
329 unsafe { JS_DequeueNextMicroTask(cx, generic_task.handle_mut()) };
330 if !unsafe { IsJSMicroTask(generic_task.as_ptr()) } {
331 let task = unsafe { Box::from_raw(microtask_from_jsval(*generic_task)) };
332 drop(task);
333 }
334 }
335}
336
337#[expect(unsafe_code)]
338unsafe extern "C" fn trace_non_gc_things_micro_task(trc: *mut JSTracer, val: *mut JSVal) {
339 wrap_panic(&mut || {
340 let task = microtask_from_jsval(unsafe { *val });
341 unsafe { (**task).trace(trc) };
342 })
343}