1use ::std::cell::RefCell;
3use ::std::collections::VecDeque;
4use ::std::ffi::CString;
5use ::std::os::raw::c_void;
6use ::std::ptr;
7use ::std::sync::atomic::{AtomicUsize, Ordering};
8use ::std::sync::OnceLock;
9
10use mozjs::glue::{CreateJobQueue, DeleteJobQueue, JobQueueTraps};
11use mozjs::jsapi::*;
12use mozjs::jsval::{JSVal, UndefinedValue};
13use mozjs::realm::AutoRealm;
14use mozjs::rooted;
15use mozjs::rust::wrappers2::{RunJobs, SetJobQueue};
16
17static JOB_COUNTER: AtomicUsize = AtomicUsize::new(0);
18
19pub type UncaughtExceptionHook = unsafe fn(cx: *mut JSContext, reason: JSVal);
32pub type FlushRejectionsHook = unsafe fn(cx: *mut JSContext);
33
34static UNCAUGHT_HOOK: OnceLock<UncaughtExceptionHook> = OnceLock::new();
35static FLUSH_HOOK: OnceLock<FlushRejectionsHook> = OnceLock::new();
36
37pub fn set_uncaught_hooks(uncaught: UncaughtExceptionHook, flush: FlushRejectionsHook) {
40 let _ = UNCAUGHT_HOOK.set(uncaught);
41 let _ = FLUSH_HOOK.set(flush);
42}
43
44thread_local! {
45 static JOB_IDS: RefCell<VecDeque<(usize, *mut mozjs::jsapi::JSObject)>> =
54 const { RefCell::new(VecDeque::new()) };
55 static QUEUE_PTR: RefCell<*mut mozjs::jsapi::JobQueue> = const { RefCell::new(ptr::null_mut()) };
56}
57
58fn job_prop_name(id: usize) -> CString {
59 CString::new(format!("__job_{}", id)).unwrap_or_default()
60}
61
62pub struct JobQueue;
63
64impl JobQueue {
65 pub fn init(cx: &mozjs::context::JSContext) -> bool {
66 let traps = JobQueueTraps {
67 getHostDefinedData: Some(get_host_defined_data),
68 enqueuePromiseJob: Some(enqueue_job),
69 runJobs: Some(run_jobs),
70 empty: Some(is_empty),
71 pushNewInterruptQueue: None,
72 popInterruptQueue: None,
73 dropInterruptQueues: None,
74 };
75
76 let queue = unsafe { CreateJobQueue(&traps, ptr::null(), ptr::null_mut()) };
77 if queue.is_null() {
78 return false;
79 }
80
81 QUEUE_PTR.with(|p| {
82 *p.borrow_mut() = queue;
83 });
84
85 unsafe { SetJobQueue(cx, queue) }
86 true
87 }
88
89 pub fn drain(cx: &mut mozjs::context::JSContext) {
90 unsafe { RunJobs(cx) }
91 }
92}
93
94impl Drop for JobQueue {
95 fn drop(&mut self) {
96 QUEUE_PTR.with(|p| {
97 let ptr = *p.borrow();
98 if !ptr.is_null() {
99 unsafe { DeleteJobQueue(ptr) };
100 *p.borrow_mut() = ptr::null_mut();
101 }
102 });
103 }
104}
105
106#[allow(unsafe_op_in_unsafe_fn)]
107unsafe extern "C" fn enqueue_job(
108 _queue: *const c_void,
109 cx: *mut JSContext,
110 _promise: Handle<*mut JSObject>,
111 job: Handle<*mut JSObject>,
112 _allocation_site: Handle<*mut JSObject>,
113 _host_defined_data: Handle<*mut JSObject>,
114) -> bool {
115 let job_obj = *job.ptr;
116 if job_obj.is_null() {
117 return true;
118 }
119
120 let id = JOB_COUNTER.fetch_add(1, Ordering::Relaxed);
121 let global = unsafe { CurrentGlobalOrNull(cx) };
122 if global.is_null() {
123 return true;
124 }
125
126 let prop = job_prop_name(id);
128 let mut wrapped_cx =
129 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
130 rooted!(&in(wrapped_cx) let job_root = mozjs::jsval::ObjectValue(job_obj));
131 rooted!(&in(wrapped_cx) let global_root = global);
132 unsafe {
133 JS_DefineProperty(
134 cx,
135 global_root.handle().into(),
136 prop.as_ptr(),
137 job_root.handle().into(),
138 0,
139 );
140 }
141
142 JOB_IDS.with(|q| {
143 q.borrow_mut().push_back((id, global));
144 });
145 true
146}
147
148#[allow(unsafe_op_in_unsafe_fn)]
149unsafe extern "C" fn run_jobs(_queue: *const c_void, cx: *mut JSContext) {
150 loop {
151 let job_entry = JOB_IDS.with(|q| q.borrow_mut().pop_front());
152 let Some((id, global)) = job_entry else {
153 break;
154 };
155
156 if global.is_null() {
157 continue;
158 }
159
160 let prop = job_prop_name(id);
166 let mut wrapped_cx =
167 mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
168 let mut realm = AutoRealm::new(
169 &mut wrapped_cx,
170 ::std::ptr::NonNull::new_unchecked(global),
171 );
172 let realm_cx: &mut mozjs::context::JSContext = &mut realm;
173 rooted!(&in(realm_cx) let global_root = global);
174 let mut job_val = UndefinedValue();
175 unsafe {
176 if !JS_GetProperty(
186 cx,
187 global_root.handle().into(),
188 prop.as_ptr(),
189 MutableHandle::<Value> {
190 _phantom_0: ::std::marker::PhantomData,
191 ptr: &mut job_val,
192 },
193 ) {
194 JS_ClearPendingException(cx);
195 continue;
196 }
197 }
198
199 if !job_val.is_object() {
200 continue;
201 }
202
203 let mut rval = UndefinedValue();
204 rooted!(&in(realm_cx) let obj_root = global);
205 rooted!(&in(realm_cx) let fval_root = job_val);
206 let empty_args = HandleValueArray::empty();
207 let rval_handle = MutableHandle::<Value> {
208 _phantom_0: ::std::marker::PhantomData,
209 ptr: &mut rval,
210 };
211
212 unsafe {
213 let ok = JS_CallFunctionValue(
214 cx,
215 obj_root.handle().into(),
216 fval_root.handle().into(),
217 &empty_args,
218 rval_handle,
219 );
220 if !ok {
221 let mut exn = UndefinedValue();
227 JS_GetPendingException(
228 cx,
229 MutableHandle::<Value> {
230 _phantom_0: ::std::marker::PhantomData,
231 ptr: &mut exn,
232 },
233 );
234 JS_ClearPendingException(cx);
235 rooted!(&in(realm_cx) let reason_root = exn);
236 if !exn.is_undefined() {
237 if let Some(&hook) = UNCAUGHT_HOOK.get() {
238 unsafe { hook(cx, exn) };
241 }
242 }
243 }
244 }
245
246 unsafe {
248 JS_DeleteProperty1(cx, global_root.handle().into(), prop.as_ptr());
249 }
250 }
251
252 if let Some(&hook) = FLUSH_HOOK.get() {
256 unsafe { hook(cx) };
258 }
259}
260
261#[allow(unsafe_op_in_unsafe_fn)]
262unsafe extern "C" fn get_host_defined_data(
263 _queue: *const c_void,
264 _cx: *mut JSContext,
265 data: MutableHandle<*mut JSObject>,
266) -> bool {
267 data.set(ptr::null_mut());
268 true
269}
270
271#[allow(unsafe_op_in_unsafe_fn)]
272unsafe extern "C" fn is_empty(_queue: *const c_void) -> bool {
273 JOB_IDS.with(|q| q.borrow().is_empty())
274}