Skip to main content

bao_engine/
job_queue.rs

1// @trace REQ-ENG-004
2use ::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
19// ── Uncaught-exception / unhandled-rejection hooks ─────────────────────────
20//
21// bao_engine cannot depend on bao_runtime (dependency edge is the other way),
22// so the runtime registers its exception router here after context init —
23// same indirection pattern as `module_loader::set_job_queue_drain`.
24//
25// `uncaught`: invoked when a job's JS_CallFunctionValue failed — the pending
26//             exception has been captured and cleared by the trap; the hook
27//             routes it (process.on('uncaughtException') or print + exit 1).
28// `flush`:    invoked at the run_jobs tail (job queue drained) so the runtime
29//             can dispatch unhandled promise rejections on a clean stack.
30
31pub 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
37/// Register the runtime's exception router. Idempotent (first registration
38/// wins — every bao_runtime context installs the same functions).
39pub 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    // Track job IDs in order — the actual JSObject* is stored as a global property
46    // (keyed by the id) on the global that was current at enqueue time. The
47    // global pointer is stored alongside the id because `run_jobs` may run
48    // outside any realm (event-loop tick / ConcurrentTask dispatch), where
49    // `CurrentGlobalOrNull(cx)` is NULL and the job's backing global cannot
50    // be rediscovered (BCE-BUG-ENG-370 companion fix). A realm's global
51    // outlives the realm's jobs and is kept alive by its realm (and every
52    // live job object is itself rooted as a property of that global).
53    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    // Store job as a property on the global object — GC-safe
127    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        // `run_jobs` is invoked from js::RunJobs which may fire outside any
161        // realm (event-loop tick, ConcurrentTask dispatch) — cx->realm_ is
162        // NULL there, so property access on `global` requires entering its
163        // realm first. AutoRealm restores the (possibly NULL) previous realm
164        // on drop.
165        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            // BCE (P0 browser startup panic, servo error.rs:74): the job pump
177            // probes the per-thread global (servo Window in browser mode) for
178            // the queued job closure. A failed JS_GetProperty (throwing
179            // accessor / proxy hook) returns false WITH the exception
180            // pending; the old code ignored the return, so the stale
181            // exception leaked onto the ScriptThread context and detonated
182            // servo's `assert!(!JS_IsExceptionPending)` in
183            // `throw_dom_exception` on the next error path. Consume it — the
184            // job reads as absent and is skipped.
185            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                // The job threw. Capture the pending exception, clear it, and
222                // hand it to the runtime's uncaught-exception router (Node:
223                // a queueMicrotask/job throw is an uncaught exception — NOT
224                // silently swallowed). `reason_root` keeps the value alive
225                // across the hook's JS dispatch.
226                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                        // SAFETY: cx is live (trap contract); hook roots its
239                        // argument before running JS.
240                        unsafe { hook(cx, exn) };
241                    }
242                }
243            }
244        }
245
246        // Clean up the property after execution
247        unsafe {
248            JS_DeleteProperty1(cx, global_root.handle().into(), prop.as_ptr());
249        }
250    }
251
252    // Job queue drained — dispatch unhandled promise rejections recorded by
253    // the runtime's rejection tracker. Runs after every drain (all pump
254    // paths funnel through this trap), on a clean JS stack.
255    if let Some(&hook) = FLUSH_HOOK.get() {
256        // SAFETY: cx is live (trap contract).
257        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}