Skip to main content

script/
script_runtime.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The script runtime contains common traits and structs commonly used by the
6//! script thread, the dom, and the worker threads.
7
8#![expect(dead_code)]
9
10use core::ffi::c_char;
11use std::cell::Cell;
12use std::ffi::{CStr, CString};
13use std::io::{Write, stdout};
14use std::ops::{Deref, DerefMut};
15use std::os::raw::c_void;
16use std::ptr::NonNull;
17use std::rc::{Rc, Weak};
18use std::sync::Mutex;
19use std::time::{Duration, Instant};
20use std::{os, ptr, thread};
21
22use background_hang_monitor_api::ScriptHangAnnotation;
23use js::context::JSContext;
24use js::conversions::jsstr_to_string;
25use js::gc::StackGCVector;
26use js::glue::{
27    CreateJobQueue, DeleteJobQueue, DispatchablePointer, JS_GetReservedSlot, JobQueueTraps,
28    RUST_js_GetErrorMessage, RegisterScriptEnvironmentPreparer,
29    RunScriptEnvironmentPreparerClosure, SetBuildId, StreamConsumerConsumeChunk,
30    StreamConsumerNoteResponseURLs, StreamConsumerStreamEnd, StreamConsumerStreamError,
31};
32use js::jsapi::{
33    AsmJSOption, BuildIdCharVector, CompilationType, Dispatchable_MaybeShuttingDown, GCDescription,
34    GCOptions, GCProgress, GCReason, GetPromiseUserInputEventHandlingState, Handle as RawHandle,
35    HandleObject, HandleString, HandleValue as RawHandleValue, Heap, JS_SetReservedSlot,
36    JSCLASS_RESERVED_SLOTS_MASK, JSCLASS_RESERVED_SLOTS_SHIFT, JSClass, JSClassOps,
37    JSContext as RawJSContext, JSGCParamKey, JSGCStatus, JSJitCompilerOption, JSObject,
38    JSSecurityCallbacks, JSString, JSTracer, JobQueue, MimeType, MutableHandleObject,
39    MutableHandleString, PromiseRejectionHandlingState, PromiseUserInputEventHandlingState,
40    RuntimeCode, ScriptEnvironmentPreparer_Closure, SetProcessBuildIdOp,
41    StreamConsumer as JSStreamConsumer,
42};
43use js::jsval::{JSVal, ObjectValue, UndefinedValue};
44use js::panic::wrap_panic;
45use js::realm::CurrentRealm;
46pub(crate) use js::rust::ThreadSafeJSContext;
47use js::rust::wrappers2::{
48    CollectServoSizes, ContextOptionsRef, DispatchableRun, InitConsumeStreamCallback,
49    JS_AddExtraGCRootsTracer, JS_GetPromiseResult, JS_InitDestroyPrincipalsCallback,
50    JS_InitReadPrincipalsCallback, JS_NewObject, JS_NewStringCopyUTF8N, JS_SetGCCallback,
51    JS_SetGCParameter, JS_SetGlobalJitCompilerOption, JS_SetOffthreadIonCompilationEnabled,
52    JS_SetSecurityCallbacks, SetDOMCallbacks, SetGCSliceCallback, SetJobQueue,
53    SetPreserveWrapperCallbacks, SetPromiseRejectionTrackerCallback, SetUpEventLoopDispatch,
54};
55use js::rust::{
56    Handle, HandleObject as RustHandleObject, HandleValue, IntoHandle, JSEngine, JSEngineError,
57    JSEngineHandle, ParentRuntime, Runtime as RustRuntime, Trace,
58};
59use malloc_size_of::MallocSizeOfOps;
60use malloc_size_of_derive::MallocSizeOf;
61use profile_traits::mem::{Report, ReportKind};
62use profile_traits::path;
63use profile_traits::time::ProfilerCategory;
64use script_bindings::reflector::DomObject;
65use script_bindings::script_runtime::{mark_runtime_dead, runtime_is_alive, temp_cx};
66use script_bindings::settings_stack::run_a_script;
67use servo_config::opts::{self, DiagnosticsLoggingOption};
68use servo_config::pref;
69use style::thread_state::{self, ThreadState};
70
71use crate::dom::bindings::codegen::Bindings::PromiseBinding::PromiseJobCallback;
72use crate::dom::bindings::codegen::Bindings::ResponseBinding::Response_Binding::ResponseMethods;
73use crate::dom::bindings::codegen::Bindings::ResponseBinding::ResponseType as DOMResponseType;
74use crate::dom::bindings::codegen::UnionTypes::TrustedScriptOrString;
75use crate::dom::bindings::conversions::{
76    get_dom_class, private_from_object, root_from_handleobject, root_from_object,
77};
78use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception};
79use crate::dom::bindings::inheritance::Castable;
80use crate::dom::bindings::refcounted::{
81    LiveDOMReferences, Trusted, TrustedPromise, trace_refcounted_objects,
82};
83use crate::dom::bindings::reflector::DomGlobal;
84use crate::dom::bindings::root::trace_roots;
85use crate::dom::bindings::str::DOMString;
86use crate::dom::bindings::utils::DOM_CALLBACKS;
87use crate::dom::bindings::{principals, settings_stack};
88use crate::dom::console::stringify_handle_value;
89use crate::dom::csp::CspReporting;
90use crate::dom::event::{Event, EventBubbles, EventCancelable};
91use crate::dom::eventtarget::EventTarget;
92use crate::dom::globalscope::GlobalScope;
93use crate::dom::promise::Promise;
94use crate::dom::promiserejectionevent::PromiseRejectionEvent;
95use crate::dom::response::Response;
96use crate::dom::trustedtypes::trustedscript::TrustedScript;
97use crate::messaging::{CommonScriptMsg, ScriptEventLoopSender};
98use crate::microtask::{EnqueuedPromiseCallback, MicrotaskQueue};
99use crate::modules::script_module::EnsureModuleHooksInitialized;
100use crate::realms::enter_auto_realm;
101use crate::tasks::task_source::TaskSourceName;
102use crate::{DomTypeHolder, ScriptThread};
103
104static JOB_QUEUE_TRAPS: JobQueueTraps = JobQueueTraps {
105    getHostDefinedData: Some(get_host_defined_data),
106    enqueuePromiseJob: Some(enqueue_promise_job),
107    runJobs: Some(run_jobs),
108    empty: Some(empty),
109    pushNewInterruptQueue: Some(push_new_interrupt_queue),
110    popInterruptQueue: Some(pop_interrupt_queue),
111    dropInterruptQueues: Some(drop_interrupt_queues),
112};
113
114static SECURITY_CALLBACKS: JSSecurityCallbacks = JSSecurityCallbacks {
115    contentSecurityPolicyAllows: Some(content_security_policy_allows),
116    codeForEvalGets: Some(code_for_eval_gets),
117    subsumes: Some(principals::subsumes),
118};
119
120#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
121pub(crate) enum ScriptThreadEventCategory {
122    SpawnPipeline,
123    ConstellationMsg,
124    DatabaseAccessEvent,
125    DevtoolsMsg,
126    DocumentEvent,
127    FileRead,
128    FontLoading,
129    FormPlannedNavigation,
130    GeolocationEvent,
131    ImageCacheMsg,
132    InputEvent,
133    NavigationAndTraversalEvent,
134    NetworkEvent,
135    PortMessage,
136    Rendering,
137    Resize,
138    ScriptEvent,
139    SetScrollState,
140    SetViewport,
141    StylesheetLoad,
142    TimerEvent,
143    UpdateReplacedElement,
144    WebSocketEvent,
145    WorkerEvent,
146    WorkletEvent,
147    ServiceWorkerEvent,
148    EnterFullscreen,
149    ExitFullscreen,
150    PerformanceTimelineTask,
151    #[cfg(feature = "webgpu")]
152    WebGPUMsg,
153}
154
155impl From<ScriptThreadEventCategory> for ProfilerCategory {
156    fn from(category: ScriptThreadEventCategory) -> Self {
157        match category {
158            ScriptThreadEventCategory::SpawnPipeline => ProfilerCategory::ScriptSpawnPipeline,
159            ScriptThreadEventCategory::ConstellationMsg => ProfilerCategory::ScriptConstellationMsg,
160            ScriptThreadEventCategory::DatabaseAccessEvent => {
161                ProfilerCategory::ScriptDatabaseAccessEvent
162            },
163            ScriptThreadEventCategory::DevtoolsMsg => ProfilerCategory::ScriptDevtoolsMsg,
164            ScriptThreadEventCategory::DocumentEvent => ProfilerCategory::ScriptDocumentEvent,
165            ScriptThreadEventCategory::EnterFullscreen => ProfilerCategory::ScriptEnterFullscreen,
166            ScriptThreadEventCategory::ExitFullscreen => ProfilerCategory::ScriptExitFullscreen,
167            ScriptThreadEventCategory::FileRead => ProfilerCategory::ScriptFileRead,
168            ScriptThreadEventCategory::FontLoading => ProfilerCategory::ScriptFontLoading,
169            ScriptThreadEventCategory::FormPlannedNavigation => {
170                ProfilerCategory::ScriptPlannedNavigation
171            },
172            ScriptThreadEventCategory::GeolocationEvent => ProfilerCategory::ScriptGeolocationEvent,
173            ScriptThreadEventCategory::NavigationAndTraversalEvent => {
174                ProfilerCategory::ScriptNavigationAndTraversalEvent
175            },
176            ScriptThreadEventCategory::ImageCacheMsg => ProfilerCategory::ScriptImageCacheMsg,
177            ScriptThreadEventCategory::InputEvent => ProfilerCategory::ScriptInputEvent,
178            ScriptThreadEventCategory::NetworkEvent => ProfilerCategory::ScriptNetworkEvent,
179            ScriptThreadEventCategory::PerformanceTimelineTask => {
180                ProfilerCategory::ScriptPerformanceEvent
181            },
182            ScriptThreadEventCategory::PortMessage => ProfilerCategory::ScriptPortMessage,
183            ScriptThreadEventCategory::Resize => ProfilerCategory::ScriptResize,
184            ScriptThreadEventCategory::Rendering => ProfilerCategory::ScriptRendering,
185            ScriptThreadEventCategory::ScriptEvent => ProfilerCategory::ScriptEvent,
186            ScriptThreadEventCategory::ServiceWorkerEvent => {
187                ProfilerCategory::ScriptServiceWorkerEvent
188            },
189            ScriptThreadEventCategory::SetScrollState => ProfilerCategory::ScriptSetScrollState,
190            ScriptThreadEventCategory::SetViewport => ProfilerCategory::ScriptSetViewport,
191            ScriptThreadEventCategory::StylesheetLoad => ProfilerCategory::ScriptStylesheetLoad,
192            ScriptThreadEventCategory::TimerEvent => ProfilerCategory::ScriptTimerEvent,
193            ScriptThreadEventCategory::UpdateReplacedElement => {
194                ProfilerCategory::ScriptUpdateReplacedElement
195            },
196            ScriptThreadEventCategory::WebSocketEvent => ProfilerCategory::ScriptWebSocketEvent,
197            ScriptThreadEventCategory::WorkerEvent => ProfilerCategory::ScriptWorkerEvent,
198            ScriptThreadEventCategory::WorkletEvent => ProfilerCategory::ScriptWorkletEvent,
199            #[cfg(feature = "webgpu")]
200            ScriptThreadEventCategory::WebGPUMsg => ProfilerCategory::ScriptWebGPUMsg,
201        }
202    }
203}
204
205impl From<ScriptThreadEventCategory> for ScriptHangAnnotation {
206    fn from(category: ScriptThreadEventCategory) -> Self {
207        match category {
208            ScriptThreadEventCategory::SpawnPipeline => ScriptHangAnnotation::SpawnPipeline,
209            ScriptThreadEventCategory::ConstellationMsg => ScriptHangAnnotation::ConstellationMsg,
210            ScriptThreadEventCategory::DatabaseAccessEvent => {
211                ScriptHangAnnotation::DatabaseAccessEvent
212            },
213            ScriptThreadEventCategory::DevtoolsMsg => ScriptHangAnnotation::DevtoolsMsg,
214            ScriptThreadEventCategory::DocumentEvent => ScriptHangAnnotation::DocumentEvent,
215            ScriptThreadEventCategory::InputEvent => ScriptHangAnnotation::InputEvent,
216            ScriptThreadEventCategory::FileRead => ScriptHangAnnotation::FileRead,
217            ScriptThreadEventCategory::FontLoading => ScriptHangAnnotation::FontLoading,
218            ScriptThreadEventCategory::FormPlannedNavigation => {
219                ScriptHangAnnotation::FormPlannedNavigation
220            },
221            ScriptThreadEventCategory::GeolocationEvent => ScriptHangAnnotation::GeolocationEvent,
222            ScriptThreadEventCategory::NavigationAndTraversalEvent => {
223                ScriptHangAnnotation::NavigationAndTraversalEvent
224            },
225            ScriptThreadEventCategory::ImageCacheMsg => ScriptHangAnnotation::ImageCacheMsg,
226            ScriptThreadEventCategory::NetworkEvent => ScriptHangAnnotation::NetworkEvent,
227            ScriptThreadEventCategory::Rendering => ScriptHangAnnotation::Rendering,
228            ScriptThreadEventCategory::Resize => ScriptHangAnnotation::Resize,
229            ScriptThreadEventCategory::ScriptEvent => ScriptHangAnnotation::ScriptEvent,
230            ScriptThreadEventCategory::SetScrollState => ScriptHangAnnotation::SetScrollState,
231            ScriptThreadEventCategory::SetViewport => ScriptHangAnnotation::SetViewport,
232            ScriptThreadEventCategory::StylesheetLoad => ScriptHangAnnotation::StylesheetLoad,
233            ScriptThreadEventCategory::TimerEvent => ScriptHangAnnotation::TimerEvent,
234            ScriptThreadEventCategory::UpdateReplacedElement => {
235                ScriptHangAnnotation::UpdateReplacedElement
236            },
237            ScriptThreadEventCategory::WebSocketEvent => ScriptHangAnnotation::WebSocketEvent,
238            ScriptThreadEventCategory::WorkerEvent => ScriptHangAnnotation::WorkerEvent,
239            ScriptThreadEventCategory::WorkletEvent => ScriptHangAnnotation::WorkletEvent,
240            ScriptThreadEventCategory::ServiceWorkerEvent => {
241                ScriptHangAnnotation::ServiceWorkerEvent
242            },
243            ScriptThreadEventCategory::EnterFullscreen => ScriptHangAnnotation::EnterFullscreen,
244            ScriptThreadEventCategory::ExitFullscreen => ScriptHangAnnotation::ExitFullscreen,
245            ScriptThreadEventCategory::PerformanceTimelineTask => {
246                ScriptHangAnnotation::PerformanceTimelineTask
247            },
248            ScriptThreadEventCategory::PortMessage => ScriptHangAnnotation::PortMessage,
249            #[cfg(feature = "webgpu")]
250            ScriptThreadEventCategory::WebGPUMsg => ScriptHangAnnotation::WebGPUMsg,
251        }
252    }
253}
254
255static HOST_DEFINED_DATA: JSClassOps = JSClassOps {
256    addProperty: None,
257    delProperty: None,
258    enumerate: None,
259    newEnumerate: None,
260    resolve: None,
261    mayResolve: None,
262    finalize: None,
263    call: None,
264    construct: None,
265    trace: None,
266};
267
268static HOST_DEFINED_DATA_CLASS: JSClass = JSClass {
269    name: c"HostDefinedData".as_ptr(),
270    flags: (HOST_DEFINED_DATA_SLOTS & JSCLASS_RESERVED_SLOTS_MASK) << JSCLASS_RESERVED_SLOTS_SHIFT,
271    cOps: &HOST_DEFINED_DATA,
272    spec: ptr::null(),
273    ext: ptr::null(),
274    oOps: ptr::null(),
275};
276
277const INCUMBENT_SETTING_SLOT: u32 = 0;
278const HOST_DEFINED_DATA_SLOTS: u32 = 1;
279
280/// <https://searchfox.org/mozilla-central/rev/2a8a30f4c9b918b726891ab9d2d62b76152606f1/xpcom/base/CycleCollectedJSContext.cpp#316>
281#[expect(unsafe_code)]
282unsafe extern "C" fn get_host_defined_data(
283    _: *const c_void,
284    cx: *mut RawJSContext,
285    data: MutableHandleObject,
286) -> bool {
287    let mut cx = unsafe {
288        // SAFETY: We are in SM hook
289        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
290    };
291    wrap_panic(&mut || {
292        let Some(incumbent_global) = GlobalScope::incumbent() else {
293            data.set(ptr::null_mut());
294            return;
295        };
296
297        let mut realm = enter_auto_realm(&mut cx, &*incumbent_global);
298        let cx = &mut realm.current_realm();
299
300        rooted!(&in(cx) let result = unsafe { JS_NewObject(cx, &HOST_DEFINED_DATA_CLASS)});
301        assert!(!result.is_null());
302
303        unsafe {
304            JS_SetReservedSlot(
305                *result,
306                INCUMBENT_SETTING_SLOT,
307                &ObjectValue(*incumbent_global.reflector().get_jsobject()),
308            )
309        };
310
311        data.set(result.get());
312    });
313    true
314}
315
316#[expect(unsafe_code)]
317unsafe extern "C" fn run_jobs(microtask_queue: *const c_void, cx: *mut RawJSContext) {
318    let mut cx = unsafe {
319        // SAFETY: We are in SM hook
320        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
321    };
322    wrap_panic(&mut || {
323        let microtask_queue = unsafe { &*(microtask_queue as *const MicrotaskQueue) };
324        // TODO: run Promise- and User-variant Microtasks, and do #notify-about-rejected-promises.
325        // Those will require real `globalscopes` values.
326        microtask_queue.checkpoint(&mut cx, vec![]);
327    });
328}
329
330#[expect(unsafe_code)]
331unsafe extern "C" fn empty(extra: *const c_void) -> bool {
332    let mut result = false;
333    wrap_panic(&mut || {
334        let microtask_queue = unsafe { &*(extra as *const MicrotaskQueue) };
335        result = microtask_queue.empty()
336    });
337    result
338}
339
340#[expect(unsafe_code)]
341unsafe extern "C" fn push_new_interrupt_queue(interrupt_queues: *mut c_void) -> *const c_void {
342    let mut result = std::ptr::null();
343    wrap_panic(&mut || {
344        let mut interrupt_queues =
345            unsafe { Box::from_raw(interrupt_queues as *mut Vec<Rc<MicrotaskQueue>>) };
346        let new_queue = Rc::new(MicrotaskQueue::default());
347        result = Rc::as_ptr(&new_queue) as *const c_void;
348        interrupt_queues.push(new_queue);
349        std::mem::forget(interrupt_queues);
350    });
351    result
352}
353
354#[expect(unsafe_code)]
355unsafe extern "C" fn pop_interrupt_queue(interrupt_queues: *mut c_void) -> *const c_void {
356    let mut result = std::ptr::null();
357    wrap_panic(&mut || {
358        let mut interrupt_queues =
359            unsafe { Box::from_raw(interrupt_queues as *mut Vec<Rc<MicrotaskQueue>>) };
360        let popped_queue: Rc<MicrotaskQueue> =
361            interrupt_queues.pop().expect("Guaranteed by SpiderMonkey?");
362        // Dangling, but jsglue.cpp will only use this for pointer comparison.
363        result = Rc::as_ptr(&popped_queue) as *const c_void;
364        std::mem::forget(interrupt_queues);
365    });
366    result
367}
368
369#[expect(unsafe_code)]
370unsafe extern "C" fn drop_interrupt_queues(interrupt_queues: *mut c_void) {
371    wrap_panic(&mut || {
372        let interrupt_queues =
373            unsafe { Box::from_raw(interrupt_queues as *mut Vec<Rc<MicrotaskQueue>>) };
374        drop(interrupt_queues);
375    });
376}
377
378/// <https://searchfox.org/mozilla-central/rev/2a8a30f4c9b918b726891ab9d2d62b76152606f1/xpcom/base/CycleCollectedJSContext.cpp#355>
379/// SM callback for promise job resolution. Adds a promise callback to the current
380/// global's microtask queue.
381#[expect(unsafe_code)]
382unsafe extern "C" fn enqueue_promise_job(
383    extra: *const c_void,
384    cx: *mut RawJSContext,
385    promise: HandleObject,
386    job: HandleObject,
387    _allocation_site: HandleObject,
388    host_defined_data: HandleObject,
389) -> bool {
390    // SAFETY: it is safe to construct a JSContext from engine hook.
391    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
392    let cx = &mut cx;
393
394    let mut result = false;
395    wrap_panic(&mut || {
396        let microtask_queue = unsafe { &*(extra as *const MicrotaskQueue) };
397        let global = if !host_defined_data.is_null() {
398            let mut incumbent_global = UndefinedValue();
399            unsafe {
400                JS_GetReservedSlot(
401                    host_defined_data.get(),
402                    INCUMBENT_SETTING_SLOT,
403                    &mut incumbent_global,
404                );
405                GlobalScope::from_object(incumbent_global.to_object())
406            }
407        } else {
408            let mut realm = CurrentRealm::assert(cx);
409            GlobalScope::from_current_realm(&mut realm)
410        };
411        let interaction = if promise.get().is_null() {
412            PromiseUserInputEventHandlingState::DontCare
413        } else {
414            unsafe { GetPromiseUserInputEventHandlingState(promise) }
415        };
416        let is_user_interacting =
417            interaction == PromiseUserInputEventHandlingState::HadUserInteractionAtCreation;
418        microtask_queue.enqueue(
419            cx,
420            Box::new(EnqueuedPromiseCallback {
421                callback: unsafe { PromiseJobCallback::new(cx, job.get()) },
422                global: global.as_traced(),
423                is_user_interacting,
424            }),
425        );
426        result = true
427    });
428    result
429}
430
431#[expect(unsafe_code)]
432/// <https://html.spec.whatwg.org/multipage/#the-hostpromiserejectiontracker-implementation>
433unsafe extern "C" fn promise_rejection_tracker(
434    cx: *mut RawJSContext,
435    muted_errors: bool,
436    promise: HandleObject,
437    state: PromiseRejectionHandlingState,
438    _data: *mut c_void,
439) {
440    // Step 1. Let script be the running script.
441    // Step 2. If script is a classic script and script's muted errors is true, then return.
442    if muted_errors {
443        return;
444    }
445
446    // Step 3.
447    // SAFETY: it is safe to construct a JSContext from engine hook.
448    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
449    let mut realm = CurrentRealm::assert(&mut cx);
450
451    let global = GlobalScope::from_current_realm(&mut realm);
452    let cx = &mut realm;
453
454    wrap_panic(&mut || {
455        match state {
456            // Step 4.
457            PromiseRejectionHandlingState::Unhandled => {
458                global.add_uncaught_rejection(promise);
459            },
460            // Step 5.
461            PromiseRejectionHandlingState::Handled => {
462                // Step 5-1.
463                if global
464                    .get_uncaught_rejections()
465                    .borrow()
466                    .contains(&Heap::boxed(promise.get()))
467                {
468                    global.remove_uncaught_rejection(promise);
469                    return;
470                }
471
472                // Step 5-2.
473                if !global
474                    .get_consumed_rejections()
475                    .borrow()
476                    .contains(&Heap::boxed(promise.get()))
477                {
478                    return;
479                }
480
481                // Step 5-3.
482                global.remove_consumed_rejection(promise);
483
484                let target = Trusted::new(global.upcast::<EventTarget>());
485                let promise =
486                    Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise) });
487                let trusted_promise = TrustedPromise::new(promise);
488
489                // Step 5-4.
490                global.task_manager().dom_manipulation_task_source().queue(
491                task!(rejection_handled_event: move |cx| {
492                    let target = target.root();
493                    let root_promise = trusted_promise.root();
494
495                    rooted!(&in(cx) let mut reason = UndefinedValue());
496                    unsafe {
497                        JS_GetPromiseResult(root_promise.reflector().get_jsobject(), reason.handle_mut());
498                    }
499
500                    let event = PromiseRejectionEvent::new(
501                        cx,
502                        &target.global(),
503                        atom!("rejectionhandled"),
504                        EventBubbles::DoesNotBubble,
505                        EventCancelable::Cancelable,
506                        root_promise,
507                        reason.handle(),
508                    );
509
510                    event.upcast::<Event>().fire(cx, &target);
511                })
512                );
513            },
514        };
515    })
516}
517
518#[expect(unsafe_code)]
519fn safely_convert_null_to_string(cx: &JSContext, str_: HandleString) -> DOMString {
520    DOMString::from(match std::ptr::NonNull::new(*str_) {
521        None => "".to_owned(),
522        Some(str_) => unsafe { jsstr_to_string(cx, str_) },
523    })
524}
525
526#[expect(unsafe_code)]
527unsafe extern "C" fn code_for_eval_gets(
528    cx: *mut RawJSContext,
529    code: HandleObject,
530    code_for_eval: MutableHandleString,
531) -> bool {
532    // SAFETY: We are in SM hook
533    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
534    let cx = &mut cx;
535    if let Ok(trusted_script) = unsafe { root_from_object::<TrustedScript>(cx, code.get()) } {
536        let script_str = trusted_script.data().str();
537        let s = js::conversions::Utf8Chars::from(&*script_str);
538        let new_string = unsafe { JS_NewStringCopyUTF8N(cx, &*s as *const _) };
539        code_for_eval.set(new_string);
540    }
541    true
542}
543
544#[expect(unsafe_code)]
545unsafe extern "C" fn content_security_policy_allows(
546    cx: *mut RawJSContext,
547    runtime_code: RuntimeCode,
548    code_string: HandleString,
549    compilation_type: CompilationType,
550    parameter_strings: RawHandle<StackGCVector<*mut JSString>>,
551    body_string: HandleString,
552    parameter_args: RawHandle<StackGCVector<JSVal>>,
553    body_arg: RawHandleValue,
554    can_compile_strings: *mut bool,
555) -> bool {
556    let mut allowed = false;
557    // SAFETY: We are in SM hook
558    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
559    let cx = &mut cx;
560    wrap_panic(&mut || {
561        // SpiderMonkey provides null pointer when executing webassembly.
562        let mut realm = CurrentRealm::assert(cx);
563        let global = GlobalScope::from_current_realm(&mut realm);
564        let csp_list = global.get_csp_list();
565
566        // If we don't have any CSP checks to run, short-circuit all logic here
567        allowed = csp_list.is_none() ||
568            match runtime_code {
569                RuntimeCode::JS => {
570                    let parameter_strings = unsafe { Handle::from_raw(parameter_strings) };
571                    let parameter_strings_length = parameter_strings.len();
572                    let mut parameter_strings_vec =
573                        Vec::with_capacity(parameter_strings_length as usize);
574
575                    for i in 0..parameter_strings_length {
576                        let Some(str_) = parameter_strings.at(i) else {
577                            unreachable!();
578                        };
579                        parameter_strings_vec.push(safely_convert_null_to_string(cx, str_.into()));
580                    }
581
582                    let parameter_args = unsafe { Handle::from_raw(parameter_args) };
583                    let parameter_args_length = parameter_args.len();
584                    let mut parameter_args_vec = Vec::with_capacity(parameter_args_length as usize);
585
586                    for i in 0..parameter_args_length {
587                        let Some(arg) = parameter_args.at(i) else {
588                            unreachable!();
589                        };
590                        let value = arg.into_handle().get();
591                        if value.is_object() {
592                            if let Ok(trusted_script) =
593                                unsafe { root_from_object::<TrustedScript>(cx, value.to_object()) }
594                            {
595                                parameter_args_vec
596                                    .push(TrustedScriptOrString::TrustedScript(trusted_script));
597                            } else {
598                                // It's not a trusted script but a different object. Treat it
599                                // as if it is a string, since we don't need the actual contents
600                                // of the object.
601                                parameter_args_vec
602                                    .push(TrustedScriptOrString::String(DOMString::new()));
603                            }
604                        } else if value.is_string() {
605                            // We don't need to know the specific string, only that it is untrusted
606                            parameter_args_vec
607                                .push(TrustedScriptOrString::String(DOMString::new()));
608                        } else {
609                            unreachable!();
610                        }
611                    }
612
613                    let code_string = safely_convert_null_to_string(cx, code_string);
614                    let body_string = safely_convert_null_to_string(cx, body_string);
615
616                    TrustedScript::can_compile_string_with_trusted_type(
617                        cx,
618                        &global,
619                        code_string,
620                        compilation_type,
621                        parameter_strings_vec,
622                        body_string,
623                        parameter_args_vec,
624                        unsafe { HandleValue::from_raw(body_arg) },
625                    )
626                },
627                RuntimeCode::WASM => global
628                    .get_csp_list()
629                    .is_wasm_evaluation_allowed(cx, &global),
630            };
631    });
632    unsafe { *can_compile_strings = allowed };
633    true
634}
635
636#[expect(unsafe_code)]
637/// <https://html.spec.whatwg.org/multipage/#notify-about-rejected-promises>
638pub(crate) fn notify_about_rejected_promises(cx: &mut JSContext, global: &GlobalScope) {
639    // Step 1. Let list be a clone of global's about-to-be-notified rejected promises list.
640    let uncaught_rejections: Vec<TrustedPromise> = global
641        .get_uncaught_rejections()
642        .borrow_mut()
643        .drain(..)
644        .map(|promise| {
645            let promise =
646                Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise.handle()) });
647
648            TrustedPromise::new(promise)
649        })
650        .collect();
651
652    // Step 2. If list is empty, then return.
653    if uncaught_rejections.is_empty() {
654        return;
655    }
656
657    // Step 3. Empty global's about-to-be-notified rejected promises list.
658    // NOTE: We did this as part of Step 1. using the "drain(..)" call.
659
660    // Step 4. Queue a global task on the DOM manipulation task source given global to run the following step:
661    let target = Trusted::new(global.upcast::<EventTarget>());
662    global.task_manager().dom_manipulation_task_source().queue(
663        task!(unhandled_rejection_event: move |cx| {
664            let target = target.root();
665
666            // Step 4.1 For each promise p of list:
667            for promise in uncaught_rejections {
668                let promise = promise.root();
669
670                // 4.1.1 If p.[[PromiseIsHandled]] is true, then continue.
671                if promise.get_promise_is_handled() {
672                    continue;
673                }
674
675                // Step 4.1.2 Let notCanceled be the result of firing an event named unhandledrejection at global,
676                // using PromiseRejectionEvent, with the cancelable attribute initialized to true,
677                // the promise attribute initialized to p, and the reason attribute initialized to p.[[PromiseResult]].
678                rooted!(&in(cx) let mut reason = UndefinedValue());
679                unsafe {
680                    JS_GetPromiseResult(promise.reflector().get_jsobject(), reason.handle_mut());
681                }
682
683                log::error!(
684                    "Unhandled promise rejection: {}",
685                    stringify_handle_value( cx, reason.handle())
686                );
687
688                let event = PromiseRejectionEvent::new(
689                    cx,
690                    &target.global(),
691                    atom!("unhandledrejection"),
692                    EventBubbles::DoesNotBubble,
693                    EventCancelable::Cancelable,
694                    promise.clone(),
695                    reason.handle(),
696                );
697                event.upcast::<Event>().fire(cx, &target);
698
699                // TODO: Step 4.1.3 If notCanceled is true, then the user agent may report
700                // p.[[PromiseResult]] to a developer console.
701
702                // Step 4.1.4 If p.[[PromiseIsHandled]] is false, then append p to global's outstanding
703                // rejected promises weak set.
704                if !promise.get_promise_is_handled() {
705                    target.global().add_consumed_rejection(promise.reflector().get_jsobject().into_handle());
706                }
707            }
708        })
709    );
710}
711
712/// Data that is sent to SpiderMonkey runtime callbacks as a pointer, which allows access
713/// to the `Runtime` state.
714#[derive(Default, JSTraceable, MallocSizeOf)]
715struct RuntimeCallbackData {
716    script_event_loop_sender: Option<ScriptEventLoopSender>,
717    #[no_trace]
718    #[ignore_malloc_size_of = "ScriptThread measures its own memory itself."]
719    script_thread: Option<Weak<ScriptThread>>,
720}
721
722#[derive(JSTraceable, MallocSizeOf)]
723pub(crate) struct Runtime {
724    #[ignore_malloc_size_of = "Type from mozjs"]
725    rt: RustRuntime,
726    /// Our actual microtask queue, which is preserved and untouched by the debugger when running debugger scripts.
727    #[conditional_malloc_size_of]
728    pub(crate) microtask_queue: Rc<MicrotaskQueue>,
729    #[ignore_malloc_size_of = "Type from mozjs"]
730    job_queue: *mut JobQueue,
731    /// The data that is set on the SpiderMonkey runtime callbacks as a pointer.
732    runtime_callback_data: Box<RuntimeCallbackData>,
733}
734
735impl Runtime {
736    /// Create a new runtime, optionally with the given [`SendableTaskSource`] for networking.
737    ///
738    /// # Safety
739    ///
740    /// If panicking does not abort the program, any threads with child runtimes will continue
741    /// executing after the thread with the parent runtime panics, but they will be in an
742    /// invalid and undefined state.
743    ///
744    /// This, like many calls to SpiderMoney API, is unsafe.
745    #[expect(unsafe_code)]
746    pub(crate) fn new(main_thread_sender: Option<ScriptEventLoopSender>) -> Runtime {
747        unsafe { Self::new_with_parent(None, main_thread_sender) }
748    }
749
750    #[allow(unsafe_code)]
751    /// ## Safety
752    /// - only one `JSContext` can exist on the thread at a time (see note in [JSContext::from_ptr])
753    /// - the `JSContext` must not outlive the `Runtime`
754    pub(crate) unsafe fn cx(&self) -> JSContext {
755        unsafe { JSContext::from_ptr(RustRuntime::get().unwrap()) }
756    }
757
758    /// Create a new runtime, optionally with the given [`ParentRuntime`] and [`SendableTaskSource`]
759    /// for networking.
760    ///
761    /// # Safety
762    ///
763    /// If panicking does not abort the program, any threads with child runtimes will continue
764    /// executing after the thread with the parent runtime panics, but they will be in an
765    /// invalid and undefined state.
766    ///
767    /// The `parent` pointer in the [`ParentRuntime`] argument must point to a valid object in memory.
768    ///
769    /// This, like many calls to the SpiderMoney API, is unsafe.
770    #[expect(unsafe_code)]
771    pub(crate) unsafe fn new_with_parent(
772        parent: Option<ParentRuntime>,
773        script_event_loop_sender: Option<ScriptEventLoopSender>,
774    ) -> Runtime {
775        let mut runtime = if let Some(parent) = parent {
776            unsafe { RustRuntime::create_with_parent(parent) }
777        } else {
778            RustRuntime::new(JS_ENGINE.lock().unwrap().as_ref().unwrap().clone())
779        };
780        let cx = runtime.cx();
781
782        let have_event_loop_sender = script_event_loop_sender.is_some();
783        let runtime_callback_data = Box::new(RuntimeCallbackData {
784            script_event_loop_sender,
785            script_thread: None,
786        });
787        let runtime_callback_data = Box::into_raw(runtime_callback_data);
788
789        unsafe {
790            JS_AddExtraGCRootsTracer(
791                cx,
792                Some(trace_rust_roots),
793                runtime_callback_data as *mut c_void,
794            );
795
796            JS_SetSecurityCallbacks(cx, &SECURITY_CALLBACKS);
797
798            JS_InitDestroyPrincipalsCallback(cx, Some(principals::destroy_servo_jsprincipal));
799            JS_InitReadPrincipalsCallback(cx, Some(principals::read_jsprincipal));
800
801            // Needed for debug assertions about whether GC is running.
802            if cfg!(debug_assertions) {
803                JS_SetGCCallback(cx, Some(debug_gc_callback), ptr::null_mut());
804            }
805
806            if opts::get()
807                .debug
808                .is_enabled(DiagnosticsLoggingOption::GcProfile)
809            {
810                SetGCSliceCallback(cx, Some(gc_slice_callback));
811            }
812        }
813
814        unsafe extern "C" fn empty_wrapper_callback(_: *mut RawJSContext, _: HandleObject) -> bool {
815            true
816        }
817        unsafe extern "C" fn empty_has_released_callback(_: HandleObject) -> bool {
818            // fixme: return true when the Drop impl for a DOM object has been invoked
819            false
820        }
821
822        unsafe {
823            SetDOMCallbacks(cx, &DOM_CALLBACKS);
824            SetPreserveWrapperCallbacks(
825                cx,
826                Some(empty_wrapper_callback),
827                Some(empty_has_released_callback),
828            );
829        }
830
831        unsafe extern "C" fn dispatch_to_event_loop(
832            data: *mut c_void,
833            dispatchable: *mut DispatchablePointer,
834        ) -> bool {
835            let runtime_callback_data: &RuntimeCallbackData =
836                unsafe { &*(data as *mut RuntimeCallbackData) };
837            let Some(script_event_loop_sender) =
838                runtime_callback_data.script_event_loop_sender.as_ref()
839            else {
840                return false;
841            };
842
843            let runnable = Runnable(dispatchable);
844            let task = task!(dispatch_to_event_loop_message: move |cx| {
845                runnable.run(cx, Dispatchable_MaybeShuttingDown::NotShuttingDown);
846            });
847
848            script_event_loop_sender
849                .send(CommonScriptMsg::Task(
850                    ScriptThreadEventCategory::NetworkEvent,
851                    Box::new(task),
852                    None, /* pipeline_id */
853                    TaskSourceName::Networking,
854                ))
855                .is_ok()
856        }
857
858        if have_event_loop_sender {
859            unsafe {
860                SetUpEventLoopDispatch(
861                    cx,
862                    Some(dispatch_to_event_loop),
863                    runtime_callback_data as *mut c_void,
864                );
865            }
866        }
867
868        unsafe {
869            InitConsumeStreamCallback(cx, Some(consume_stream), Some(report_stream_error));
870        }
871
872        let microtask_queue = Rc::new(MicrotaskQueue::default());
873
874        // Extra queues for debugger scripts (“interrupts”) via AutoDebuggerJobQueueInterruption and saveJobQueue().
875        // Moved indefinitely to mozjs via CreateJobQueue(), borrowed from mozjs via JobQueueTraps, and moved back from
876        // mozjs for dropping via DeleteJobQueue().
877        let interrupt_queues: Box<Vec<Rc<MicrotaskQueue>>> = Box::default();
878
879        let cx_opts;
880        let job_queue;
881        unsafe {
882            let cx = runtime.cx();
883            job_queue = CreateJobQueue(
884                &JOB_QUEUE_TRAPS,
885                &*microtask_queue as *const _ as *const c_void,
886                Box::into_raw(interrupt_queues) as *mut c_void,
887            );
888            SetJobQueue(cx, job_queue);
889            SetPromiseRejectionTrackerCallback(
890                cx,
891                Some(promise_rejection_tracker),
892                ptr::null_mut(),
893            );
894
895            RegisterScriptEnvironmentPreparer(
896                cx.raw_cx(),
897                Some(invoke_script_environment_preparer),
898            );
899
900            EnsureModuleHooksInitialized(runtime.rt());
901
902            let cx = runtime.cx();
903
904            set_gc_zeal_options(cx.raw_cx());
905
906            // Enable or disable the JITs.
907            cx_opts = &mut *ContextOptionsRef(cx);
908            JS_SetGlobalJitCompilerOption(
909                cx,
910                JSJitCompilerOption::JSJITCOMPILER_BASELINE_INTERPRETER_ENABLE,
911                pref!(js_baseline_interpreter_enabled) as u32,
912            );
913            JS_SetGlobalJitCompilerOption(
914                cx,
915                JSJitCompilerOption::JSJITCOMPILER_BASELINE_ENABLE,
916                pref!(js_baseline_jit_enabled) as u32,
917            );
918            JS_SetGlobalJitCompilerOption(
919                cx,
920                JSJitCompilerOption::JSJITCOMPILER_ION_ENABLE,
921                pref!(js_ion_enabled) as u32,
922            );
923        }
924        cx_opts.compileOptions_.asmJSOption_ = if pref!(js_asmjs_enabled) {
925            AsmJSOption::Enabled
926        } else {
927            AsmJSOption::DisabledByAsmJSPref
928        };
929        cx_opts.compileOptions_.set_importAttributes_(true);
930        let wasm_enabled = pref!(js_wasm_enabled);
931        cx_opts.set_wasm_(wasm_enabled);
932        if wasm_enabled {
933            // If WASM is enabled without setting the buildIdOp,
934            // initializing a module will report an out of memory error.
935            // https://dxr.mozilla.org/mozilla-central/source/js/src/wasm/WasmTypes.cpp#458
936            unsafe { SetProcessBuildIdOp(Some(servo_build_id)) };
937        }
938        cx_opts.set_wasmBaseline_(pref!(js_wasm_baseline_enabled));
939        cx_opts.set_wasmIon_(pref!(js_wasm_ion_enabled));
940
941        unsafe {
942            let cx = runtime.cx();
943            // TODO: handle js.throw_on_asmjs_validation_failure (needs new Spidermonkey)
944            JS_SetGlobalJitCompilerOption(
945                cx,
946                JSJitCompilerOption::JSJITCOMPILER_NATIVE_REGEXP_ENABLE,
947                pref!(js_native_regex_enabled) as u32,
948            );
949            JS_SetOffthreadIonCompilationEnabled(cx, pref!(js_offthread_compilation_enabled));
950            JS_SetGlobalJitCompilerOption(
951                cx,
952                JSJitCompilerOption::JSJITCOMPILER_BASELINE_WARMUP_TRIGGER,
953                if pref!(js_baseline_jit_unsafe_eager_compilation_enabled) {
954                    0
955                } else {
956                    u32::MAX
957                },
958            );
959            JS_SetGlobalJitCompilerOption(
960                cx,
961                JSJitCompilerOption::JSJITCOMPILER_ION_NORMAL_WARMUP_TRIGGER,
962                if pref!(js_ion_unsafe_eager_compilation_enabled) {
963                    0
964                } else {
965                    u32::MAX
966                },
967            );
968            // TODO: handle js.discard_system_source.enabled
969            // TODO: handle js.asyncstack.enabled (needs new Spidermonkey)
970            // TODO: handle js.throw_on_debugee_would_run (needs new Spidermonkey)
971            // TODO: handle js.dump_stack_on_debugee_would_run (needs new Spidermonkey)
972            // TODO: handle js.shared_memory.enabled
973            JS_SetGCParameter(
974                cx,
975                JSGCParamKey::JSGC_MAX_BYTES,
976                in_range(pref!(js_mem_max), 1, 0x100)
977                    .map(|val| (val * 1024 * 1024) as u32)
978                    .unwrap_or(u32::MAX),
979            );
980
981            // Pre-barriers aren't implemented correctly at the moment, so this preference
982            // defaults to false.
983            JS_SetGCParameter(
984                cx,
985                JSGCParamKey::JSGC_INCREMENTAL_GC_ENABLED,
986                pref!(js_mem_gc_incremental_enabled) as u32,
987            );
988
989            JS_SetGCParameter(
990                cx,
991                JSGCParamKey::JSGC_PER_ZONE_GC_ENABLED,
992                pref!(js_mem_gc_per_zone_enabled) as u32,
993            );
994            if let Some(val) = in_range(pref!(js_mem_gc_incremental_slice_ms), 0, 100_000) {
995                JS_SetGCParameter(cx, JSGCParamKey::JSGC_SLICE_TIME_BUDGET_MS, val as u32);
996            }
997            JS_SetGCParameter(
998                cx,
999                JSGCParamKey::JSGC_COMPACTING_ENABLED,
1000                pref!(js_mem_gc_compacting_enabled) as u32,
1001            );
1002
1003            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_time_limit_ms), 0, 10_000) {
1004                JS_SetGCParameter(cx, JSGCParamKey::JSGC_HIGH_FREQUENCY_TIME_LIMIT, val as u32);
1005            }
1006            if let Some(val) = in_range(pref!(js_mem_gc_low_frequency_heap_growth), 0, 10_000) {
1007                JS_SetGCParameter(cx, JSGCParamKey::JSGC_LOW_FREQUENCY_HEAP_GROWTH, val as u32);
1008            }
1009            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_heap_growth_min), 0, 10_000)
1010            {
1011                JS_SetGCParameter(
1012                    cx,
1013                    JSGCParamKey::JSGC_HIGH_FREQUENCY_LARGE_HEAP_GROWTH,
1014                    val as u32,
1015                );
1016            }
1017            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_heap_growth_max), 0, 10_000)
1018            {
1019                JS_SetGCParameter(
1020                    cx,
1021                    JSGCParamKey::JSGC_HIGH_FREQUENCY_SMALL_HEAP_GROWTH,
1022                    val as u32,
1023                );
1024            }
1025            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_low_limit_mb), 0, 10_000) {
1026                JS_SetGCParameter(cx, JSGCParamKey::JSGC_SMALL_HEAP_SIZE_MAX, val as u32);
1027            }
1028            if let Some(val) = in_range(pref!(js_mem_gc_high_frequency_high_limit_mb), 0, 10_000) {
1029                JS_SetGCParameter(cx, JSGCParamKey::JSGC_LARGE_HEAP_SIZE_MIN, val as u32);
1030            }
1031            if let Some(val) = in_range(pref!(js_mem_gc_empty_chunk_count_min), 0, 10_000) {
1032                JS_SetGCParameter(cx, JSGCParamKey::JSGC_MIN_EMPTY_CHUNK_COUNT, val as u32);
1033            }
1034        }
1035        Runtime {
1036            rt: runtime,
1037            microtask_queue,
1038            job_queue,
1039            runtime_callback_data: unsafe { Box::from_raw(runtime_callback_data) },
1040        }
1041    }
1042
1043    pub(crate) fn set_script_thread(&mut self, script_thread: Weak<ScriptThread>) {
1044        self.runtime_callback_data
1045            .script_thread
1046            .replace(script_thread);
1047    }
1048
1049    pub(crate) fn thread_safe_js_context(&self) -> ThreadSafeJSContext {
1050        self.rt.thread_safe_js_context()
1051    }
1052}
1053
1054impl Drop for Runtime {
1055    #[expect(unsafe_code)]
1056    fn drop(&mut self) {
1057        // Clear our main microtask_queue.
1058        self.microtask_queue.clear();
1059
1060        // Delete the RustJobQueue in mozjs, which will destroy our interrupt queues.
1061        unsafe {
1062            DeleteJobQueue(self.job_queue);
1063        }
1064        LiveDOMReferences::destruct();
1065        mark_runtime_dead();
1066    }
1067}
1068
1069impl Deref for Runtime {
1070    type Target = RustRuntime;
1071    fn deref(&self) -> &RustRuntime {
1072        &self.rt
1073    }
1074}
1075
1076impl DerefMut for Runtime {
1077    fn deref_mut(&mut self) -> &mut RustRuntime {
1078        &mut self.rt
1079    }
1080}
1081
1082pub struct JSEngineSetup(Option<JSEngine>);
1083
1084impl Default for JSEngineSetup {
1085    fn default() -> Self {
1086        // BAO PATCH (BCE-20260627-009): Idempotent JSEngine init.
1087        // mozjs's `JSEngine::init()` uses a process-global `ENGINE_STATE` mutex
1088        // that returns `Err(AlreadyInitialized)` on any re-init. The original
1089        // servo code did `JSEngine::init().unwrap()`, which panics when a second
1090        // `BaoRuntime` (cargo multi-threaded test runner, or production
1091        // multi-tenant) creates a second `Servo` instance in the same process.
1092        // Each `Servo::new` spawns a `ScriptThread` -> `script::init()` -> this
1093        // `JSEngineSetup::default()`.
1094        //
1095        // Strategy: the FIRST caller initializes the engine and stores its handle
1096        // in `JS_ENGINE`. Subsequent callers reuse that handle without owning the
1097        // engine itself (return `JSEngineSetup(None)`). Only the owner (the first
1098        // `JSEngineSetup`) will `Drop` the real engine and shut it down. This
1099        // keeps the outstanding-handles refcount correct (no double-decrement)
1100        // and the engine alive until the owning ScriptThread is torn down.
1101        let engine = match JSEngine::init() {
1102            Ok(engine) => {
1103                *JS_ENGINE.lock().unwrap() = Some(engine.handle());
1104                Some(engine)
1105            }
1106            Err(JSEngineError::AlreadyInitialized) => {
1107                // Someone else (another ScriptThread / BaoRuntime / bao
1108                // ensure_engine_handle) already owns the engine. Prefer
1109                // mozjs::JSEngine::process_handle() (BAO PATCH SSOT), then
1110                // fall back to spinning on JS_ENGINE for legacy owners.
1111                let mut attempts = 0;
1112                loop {
1113                    if let Some(h) = JSEngine::process_handle() {
1114                        let mut slot = JS_ENGINE.lock().unwrap();
1115                        if slot.is_none() {
1116                            *slot = Some(h);
1117                        }
1118                        break;
1119                    }
1120                    if JS_ENGINE.lock().unwrap().is_some() {
1121                        break;
1122                    }
1123                    attempts += 1;
1124                    if attempts > 50 {
1125                        break;
1126                    }
1127                    thread::sleep(Duration::from_millis(1));
1128                }
1129                // Do NOT take ownership of the engine - the first owner keeps it.
1130                None
1131            }
1132            Err(JSEngineError::AlreadyShutDown) => {
1133                // BAO PATCH (BCE-20260627-009): Engine was previously
1134                // initialized AND shut down. We cannot recover the handle from
1135                // `JS_ENGINE` (it was cleared on the owner's Drop). Return
1136                // `None` and let the runtime proceed - the bao layer ensures
1137                // the first BaoRuntime's engine stays alive when needed.
1138                None
1139            }
1140            Err(e) => panic!("JSEngine::init() failed: {:?}", e),
1141        };
1142        Self(engine)
1143    }
1144}
1145
1146impl Drop for JSEngineSetup {
1147    fn drop(&mut self) {
1148        // BAO PATCH (BCE-20260627-009): Do NOT clear JS_ENGINE and do NOT drop
1149        // the engine. The engine is a process-global singleton; its handle must
1150        // persist in JS_ENGINE across BaoRuntime teardown so subsequent
1151        // BaoRuntime instances reuse it.
1152        //
1153        // mozjs JSEngine is a process-global singleton with an irreversible
1154        // state machine (Uninitialized->Initialized->ShutDown). Once
1155        // `JS_ShutDown()` runs (catalyzed by `JSEngine::drop`), the same
1156        // process can never re-init. This breaks bao's multi-BaoRuntime model
1157        // (cargo test runner). Fix: leak the engine (`std::mem::forget`) AND
1158        // keep its handle in `JS_ENGINE` (do not clear it). The OS reclaims all
1159        // JS engine resources on process exit; behaviorally equivalent, with no
1160        // memory-safety regression, and correct for an embedded single-process
1161        // runtime that must tolerate repeated construction and teardown.
1162        let Some(engine) = self.0.take() else {
1163            return;
1164        };
1165        std::mem::forget(engine);
1166    }
1167}
1168
1169static JS_ENGINE: Mutex<Option<JSEngineHandle>> = Mutex::new(None);
1170
1171fn in_range<T: PartialOrd + Copy>(val: T, min: T, max: T) -> Option<T> {
1172    if val < min || val >= max {
1173        None
1174    } else {
1175        Some(val)
1176    }
1177}
1178
1179thread_local!(static MALLOC_SIZE_OF_OPS: Cell<*mut MallocSizeOfOps> = const { Cell::new(ptr::null_mut()) });
1180
1181#[expect(unsafe_code)]
1182unsafe extern "C" fn get_size(obj: *mut JSObject) -> usize {
1183    match unsafe { get_dom_class(obj) } {
1184        Ok(v) => {
1185            let dom_object = unsafe { private_from_object(obj) as *const c_void };
1186
1187            if dom_object.is_null() {
1188                return 0;
1189            }
1190            let ops = MALLOC_SIZE_OF_OPS.get();
1191            unsafe { (v.malloc_size_of)(&mut *ops, dom_object) }
1192        },
1193        Err(_e) => 0,
1194    }
1195}
1196
1197thread_local!(static GC_CYCLE_START: Cell<Option<Instant>> = const { Cell::new(None) });
1198thread_local!(static GC_SLICE_START: Cell<Option<Instant>> = const { Cell::new(None) });
1199
1200#[expect(unsafe_code)]
1201unsafe extern "C" fn gc_slice_callback(
1202    _cx: *mut RawJSContext,
1203    progress: GCProgress,
1204    desc: *const GCDescription,
1205) {
1206    match progress {
1207        GCProgress::GC_CYCLE_BEGIN => GC_CYCLE_START.with(|start| {
1208            start.set(Some(Instant::now()));
1209            println!("GC cycle began");
1210        }),
1211        GCProgress::GC_SLICE_BEGIN => GC_SLICE_START.with(|start| {
1212            start.set(Some(Instant::now()));
1213            println!("GC slice began");
1214        }),
1215        GCProgress::GC_SLICE_END => GC_SLICE_START.with(|start| {
1216            let duration = start.get().unwrap().elapsed();
1217            start.set(None);
1218            println!("GC slice ended: duration={:?}", duration);
1219        }),
1220        GCProgress::GC_CYCLE_END => GC_CYCLE_START.with(|start| {
1221            let duration = start.get().unwrap().elapsed();
1222            start.set(None);
1223            println!("GC cycle ended: duration={:?}", duration);
1224        }),
1225    };
1226    if !desc.is_null() {
1227        let desc: &GCDescription = unsafe { &*desc };
1228        let options = match desc.options_ {
1229            GCOptions::Normal => "Normal",
1230            GCOptions::Shrink => "Shrink",
1231            GCOptions::Shutdown => "Shutdown",
1232        };
1233        println!("  isZone={}, options={}", desc.isZone_, options);
1234    }
1235    let _ = stdout().flush();
1236}
1237
1238#[expect(unsafe_code)]
1239unsafe extern "C" fn debug_gc_callback(
1240    _cx: *mut RawJSContext,
1241    status: JSGCStatus,
1242    _reason: GCReason,
1243    _data: *mut os::raw::c_void,
1244) {
1245    match status {
1246        JSGCStatus::JSGC_BEGIN => thread_state::enter(ThreadState::IN_GC),
1247        JSGCStatus::JSGC_END => thread_state::exit(ThreadState::IN_GC),
1248    }
1249}
1250
1251#[expect(unsafe_code)]
1252unsafe extern "C" fn trace_rust_roots(tr: *mut JSTracer, data: *mut os::raw::c_void) {
1253    if !runtime_is_alive() {
1254        return;
1255    }
1256    trace!("starting custom root handler");
1257
1258    let runtime_callback_data = unsafe { &*(data as *const RuntimeCallbackData) };
1259    if let Some(script_thread) = runtime_callback_data
1260        .script_thread
1261        .as_ref()
1262        .and_then(Weak::upgrade)
1263    {
1264        trace!("tracing fields of ScriptThread");
1265        unsafe { script_thread.trace(tr) };
1266    };
1267
1268    unsafe {
1269        trace_roots(tr);
1270        trace_refcounted_objects(tr);
1271        settings_stack::trace(tr);
1272    }
1273    trace!("done custom root handler");
1274}
1275
1276#[expect(unsafe_code)]
1277unsafe extern "C" fn servo_build_id(build_id: *mut BuildIdCharVector) -> bool {
1278    let servo_id = b"Servo\0";
1279    unsafe { SetBuildId(build_id, servo_id[0] as *const c_char, servo_id.len()) }
1280}
1281
1282#[expect(unsafe_code)]
1283#[cfg(feature = "debugmozjs")]
1284unsafe fn set_gc_zeal_options(cx: *mut RawJSContext) {
1285    use js::jsapi::SetGCZeal;
1286
1287    let level = match pref!(js_mem_gc_zeal_level) {
1288        level @ 0..=14 => level as u8,
1289        _ => return,
1290    };
1291    let frequency = match pref!(js_mem_gc_zeal_frequency) {
1292        frequency if frequency >= 0 => frequency as u32,
1293        // https://searchfox.org/mozilla-esr128/source/js/public/GCAPI.h#1392
1294        _ => 5000,
1295    };
1296    unsafe {
1297        SetGCZeal(cx, level, frequency);
1298    }
1299}
1300
1301#[expect(unsafe_code)]
1302#[cfg(not(feature = "debugmozjs"))]
1303unsafe fn set_gc_zeal_options(_: *mut RawJSContext) {}
1304
1305#[expect(unsafe_code)]
1306pub(crate) fn get_reports(
1307    cx: &mut JSContext,
1308    path_seg: String,
1309    ops: &mut MallocSizeOfOps,
1310) -> Vec<Report> {
1311    MALLOC_SIZE_OF_OPS.with(|ops_tls| ops_tls.set(ops));
1312    let stats = unsafe {
1313        let mut stats = ::std::mem::zeroed();
1314        if !CollectServoSizes(cx, &mut stats, Some(get_size)) {
1315            return vec![];
1316        }
1317        stats
1318    };
1319    MALLOC_SIZE_OF_OPS.with(|ops| ops.set(ptr::null_mut()));
1320
1321    let mut reports = vec![];
1322    let mut report = |mut path_suffix, kind, size| {
1323        let mut path = path![path_seg, "js"];
1324        path.append(&mut path_suffix);
1325        reports.push(Report { path, kind, size })
1326    };
1327
1328    // A note about possibly confusing terminology: the JS GC "heap" is allocated via
1329    // mmap/VirtualAlloc, which means it's not on the malloc "heap", so we use
1330    // `ExplicitNonHeapSize` as its kind.
1331    report(
1332        path!["gc-heap", "used"],
1333        ReportKind::ExplicitNonHeapSize,
1334        stats.gcHeapUsed,
1335    );
1336
1337    report(
1338        path!["gc-heap", "unused"],
1339        ReportKind::ExplicitNonHeapSize,
1340        stats.gcHeapUnused,
1341    );
1342
1343    report(
1344        path!["gc-heap", "admin"],
1345        ReportKind::ExplicitNonHeapSize,
1346        stats.gcHeapAdmin,
1347    );
1348
1349    report(
1350        path!["gc-heap", "decommitted"],
1351        ReportKind::ExplicitNonHeapSize,
1352        stats.gcHeapDecommitted,
1353    );
1354
1355    // SpiderMonkey uses the system heap, not jemalloc.
1356    report(
1357        path!["malloc-heap"],
1358        ReportKind::ExplicitSystemHeapSize,
1359        stats.mallocHeap,
1360    );
1361
1362    report(
1363        path!["non-heap"],
1364        ReportKind::ExplicitNonHeapSize,
1365        stats.nonHeap,
1366    );
1367    reports
1368}
1369
1370pub(crate) struct StreamConsumer(*mut JSStreamConsumer);
1371
1372#[expect(unsafe_code)]
1373impl StreamConsumer {
1374    pub(crate) fn consume_chunk(&self, stream: &[u8]) -> bool {
1375        unsafe {
1376            let stream_ptr = stream.as_ptr();
1377            StreamConsumerConsumeChunk(self.0, stream_ptr, stream.len())
1378        }
1379    }
1380
1381    pub(crate) fn stream_end(&self) {
1382        unsafe {
1383            StreamConsumerStreamEnd(self.0);
1384        }
1385    }
1386
1387    pub(crate) fn stream_error(&self, error_code: usize) {
1388        unsafe {
1389            StreamConsumerStreamError(self.0, error_code);
1390        }
1391    }
1392
1393    pub(crate) fn note_response_urls(
1394        &self,
1395        maybe_url: Option<String>,
1396        maybe_source_map_url: Option<String>,
1397    ) {
1398        unsafe {
1399            let maybe_url = maybe_url.map(|url| CString::new(url).unwrap());
1400            let maybe_source_map_url = maybe_source_map_url.map(|url| CString::new(url).unwrap());
1401
1402            let maybe_url_param = match maybe_url.as_ref() {
1403                Some(url) => url.as_ptr(),
1404                None => ptr::null(),
1405            };
1406            let maybe_source_map_url_param = match maybe_source_map_url.as_ref() {
1407                Some(url) => url.as_ptr(),
1408                None => ptr::null(),
1409            };
1410
1411            StreamConsumerNoteResponseURLs(self.0, maybe_url_param, maybe_source_map_url_param);
1412        }
1413    }
1414}
1415
1416/// Implements the steps to compile webassembly response mentioned here
1417/// <https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response>
1418#[expect(unsafe_code)]
1419unsafe extern "C" fn consume_stream(
1420    cx: *mut RawJSContext,
1421    obj: HandleObject,
1422    _mime_type: MimeType,
1423    _consumer: *mut JSStreamConsumer,
1424) -> bool {
1425    let mut cx = unsafe {
1426        // SAFETY: We are in SM hook
1427        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1428    };
1429    let cx = &mut cx;
1430    let mut realm = CurrentRealm::assert(cx);
1431    let global = GlobalScope::from_current_realm(&mut realm);
1432
1433    // Step 2.1 Upon fulfillment of source, store the Response with value unwrappedSource.
1434    if let Ok(unwrapped_source) =
1435        unsafe { root_from_handleobject::<Response>(cx, RustHandleObject::from_raw(obj)) }
1436    {
1437        // Step 2.2 Let mimeType be the result of extracting a MIME type from response’s header list.
1438        let mimetype = unwrapped_source.Headers(cx).extract_mime_type();
1439
1440        // Step 2.3 If mimeType is not `application/wasm`, return with a TypeError and abort these substeps.
1441        if !&mimetype[..].eq_ignore_ascii_case(b"application/wasm") {
1442            throw_dom_exception(
1443                cx,
1444                &global,
1445                Error::Type(c"Response has unsupported MIME type".to_owned()),
1446            );
1447            return false;
1448        }
1449
1450        // Step 2.4 If response is not CORS-same-origin, return with a TypeError and abort these substeps.
1451        match unwrapped_source.Type() {
1452            DOMResponseType::Basic | DOMResponseType::Cors | DOMResponseType::Default => {},
1453            _ => {
1454                throw_dom_exception(
1455                    cx,
1456                    &global,
1457                    Error::Type(c"Response.type must be 'basic', 'cors' or 'default'".to_owned()),
1458                );
1459                return false;
1460            },
1461        }
1462
1463        // Step 2.5 If response’s status is not an ok status, return with a TypeError and abort these substeps.
1464        if !unwrapped_source.Ok() {
1465            throw_dom_exception(
1466                cx,
1467                &global,
1468                Error::Type(c"Response does not have ok status".to_owned()),
1469            );
1470            return false;
1471        }
1472
1473        // Step 2.6.1 If response body is locked, return with a TypeError and abort these substeps.
1474        if unwrapped_source.is_locked() {
1475            throw_dom_exception(
1476                cx,
1477                &global,
1478                Error::Type(c"There was an error consuming the Response".to_owned()),
1479            );
1480            return false;
1481        }
1482
1483        // Step 2.6.2 If response body is alreaady consumed, return with a TypeError and abort these substeps.
1484        if unwrapped_source.is_disturbed() {
1485            throw_dom_exception(
1486                cx,
1487                &global,
1488                Error::Type(c"Response already consumed".to_owned()),
1489            );
1490            return false;
1491        }
1492        unwrapped_source.set_stream_consumer(Some(StreamConsumer(_consumer)));
1493    } else {
1494        // Step 3 Upon rejection of source, return with reason.
1495        throw_dom_exception(
1496            cx,
1497            &global,
1498            Error::Type(c"expected Response or Promise resolving to Response".to_owned()),
1499        );
1500        return false;
1501    }
1502    true
1503}
1504
1505#[expect(unsafe_code)]
1506unsafe extern "C" fn report_stream_error(_cx: *mut RawJSContext, error_code: usize) {
1507    error!("Error initializing StreamConsumer: {:?}", unsafe {
1508        RUST_js_GetErrorMessage(ptr::null_mut(), error_code as u32)
1509    });
1510}
1511
1512#[expect(unsafe_code)]
1513unsafe extern "C" fn invoke_script_environment_preparer(
1514    global: HandleObject,
1515    closure: *mut ScriptEnvironmentPreparer_Closure,
1516) {
1517    // SAFETY: always safe from a JS engine hook.
1518    let mut cx = unsafe { temp_cx() };
1519    let global = unsafe { GlobalScope::from_object(global.get()) };
1520    let mut realm = enter_auto_realm(&mut cx, &*global);
1521    let cx = &mut realm.current_realm();
1522
1523    run_a_script::<DomTypeHolder, _, _>(cx, &global, |cx| {
1524        if unsafe { !RunScriptEnvironmentPreparerClosure(cx.raw_cx(), closure) } {
1525            report_pending_exception(cx);
1526        };
1527    });
1528}
1529
1530pub(crate) struct Runnable(*mut DispatchablePointer);
1531
1532#[expect(unsafe_code)]
1533unsafe impl Sync for Runnable {}
1534#[expect(unsafe_code)]
1535unsafe impl Send for Runnable {}
1536
1537#[expect(unsafe_code)]
1538impl Runnable {
1539    fn run(&self, cx: &mut JSContext, maybe_shutting_down: Dispatchable_MaybeShuttingDown) {
1540        unsafe {
1541            DispatchableRun(cx, self.0, maybe_shutting_down);
1542        }
1543    }
1544}
1545
1546/// `introductionType` values in SpiderMonkey TransitiveCompileOptions.
1547///
1548/// Value definitions are based on the SpiderMonkey Debugger API docs:
1549/// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Source.html#introductiontype>
1550// TODO: squish `scriptElement` <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/server/actors/source.js#199-201>
1551pub(crate) struct IntroductionType;
1552impl IntroductionType {
1553    /// `introductionType` for code passed to `eval`.
1554    pub const EVAL: &CStr = c"eval";
1555    pub const EVAL_STR: &str = "eval";
1556
1557    /// `introductionType` for code evaluated by debugger.
1558    /// This includes code run via the devtools repl, even if the thread is not paused.
1559    pub const DEBUGGER_EVAL: &CStr = c"debugger eval";
1560    pub const DEBUGGER_EVAL_STR: &str = "debugger eval";
1561
1562    /// `introductionType` for code passed to the `Function` constructor.
1563    pub const FUNCTION: &CStr = c"Function";
1564    pub const FUNCTION_STR: &str = "Function";
1565
1566    /// `introductionType` for code loaded by worklet.
1567    pub const WORKLET: &CStr = c"Worklet";
1568    pub const WORKLET_STR: &str = "Worklet";
1569
1570    /// `introductionType` for code assigned to DOM elements’ event handler IDL attributes as a string.
1571    pub const EVENT_HANDLER: &CStr = c"eventHandler";
1572    pub const EVENT_HANDLER_STR: &str = "eventHandler";
1573
1574    /// `introductionType` for code belonging to `<script src="file.js">` elements.
1575    /// This includes `<script type="module" src="...">`.
1576    pub const SRC_SCRIPT: &CStr = c"srcScript";
1577    pub const SRC_SCRIPT_STR: &str = "srcScript";
1578
1579    /// `introductionType` for code belonging to `<script>code;</script>` elements.
1580    /// This includes `<script type="module" src="...">`.
1581    pub const INLINE_SCRIPT: &CStr = c"inlineScript";
1582    pub const INLINE_SCRIPT_STR: &str = "inlineScript";
1583
1584    /// `introductionType` for code belonging to scripts that *would* be `"inlineScript"` except that they were not
1585    /// part of the initial file itself.
1586    /// For example, scripts created via:
1587    /// - `document.write("<script>code;</script>")`
1588    /// - `var s = document.createElement("script"); s.text = "code";`
1589    pub const INJECTED_SCRIPT: &CStr = c"injectedScript";
1590    pub const INJECTED_SCRIPT_STR: &str = "injectedScript";
1591
1592    /// `introductionType` for code that was loaded indirectly by being imported by another script
1593    /// using ESM static or dynamic imports.
1594    pub const IMPORTED_MODULE: &CStr = c"importedModule";
1595    pub const IMPORTED_MODULE_STR: &str = "importedModule";
1596
1597    /// `introductionType` for code presented in `javascript:` URLs.
1598    pub const JAVASCRIPT_URL: &CStr = c"javascriptURL";
1599    pub const JAVASCRIPT_URL_STR: &str = "javascriptURL";
1600
1601    /// `introductionType` for code passed to `setTimeout`/`setInterval` as a string.
1602    pub const DOM_TIMER: &CStr = c"domTimer";
1603    pub const DOM_TIMER_STR: &str = "domTimer";
1604
1605    /// `introductionType` for web workers.
1606    /// FIXME: only documented in older(?) devtools user docs
1607    /// <https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.source/index.html>
1608    pub const WORKER: &CStr = c"Worker";
1609    pub const WORKER_STR: &str = "Worker";
1610}