Skip to main content

script/engine/
handle.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
5use std::sync::Mutex;
6use std::thread;
7use std::time::Duration;
8
9use js::rust::{JSEngine, JSEngineError, JSEngineHandle};
10
11static JS_ENGINE: Mutex<Option<JSEngineHandle>> = Mutex::new(None);
12
13pub(crate) fn current_js_engine_handle() -> JSEngineHandle {
14    JS_ENGINE.lock().unwrap().as_ref().unwrap().clone()
15}
16
17pub struct JSEngineSetup(Option<JSEngine>);
18
19impl Default for JSEngineSetup {
20    fn default() -> Self {
21        // BAO PATCH (BCE-20260627-009): Idempotent JSEngine init.
22        // mozjs's `JSEngine::init()` uses a process-global `ENGINE_STATE` mutex
23        // that returns `Err(AlreadyInitialized)` on any re-init. The original
24        // servo code did `JSEngine::init().unwrap()`, which panics when a second
25        // `BaoRuntime` (cargo multi-threaded test runner, or production
26        // multi-tenant) creates a second `Servo` instance in the same process.
27        // Each `Servo::new` spawns a `ScriptThread` -> `script::init()` -> this
28        // `JSEngineSetup::default()`.
29        //
30        // Strategy: the FIRST caller initializes the engine and stores its handle
31        // in `JS_ENGINE`. Subsequent callers reuse that handle without owning the
32        // engine itself (return `JSEngineSetup(None)`). Only the owner (the first
33        // `JSEngineSetup`) will `Drop` the real engine and shut it down. This
34        // keeps the outstanding-handles refcount correct (no double-decrement)
35        // and the engine alive until the owning ScriptThread is torn down.
36        let engine = match JSEngine::init() {
37            Ok(engine) => {
38                *JS_ENGINE.lock().unwrap() = Some(engine.handle());
39                Some(engine)
40            }
41            Err(JSEngineError::AlreadyInitialized) => {
42                // Someone else (another ScriptThread / BaoRuntime / bao
43                // ensure_engine_handle) already owns the engine. Prefer
44                // mozjs::JSEngine::process_handle() (BAO PATCH SSOT), then
45                // fall back to spinning on JS_ENGINE for legacy owners.
46                let mut attempts = 0;
47                loop {
48                    if let Some(h) = JSEngine::process_handle() {
49                        let mut slot = JS_ENGINE.lock().unwrap();
50                        if slot.is_none() {
51                            *slot = Some(h);
52                        }
53                        break;
54                    }
55                    if JS_ENGINE.lock().unwrap().is_some() {
56                        break;
57                    }
58                    attempts += 1;
59                    if attempts > 50 {
60                        break;
61                    }
62                    thread::sleep(Duration::from_millis(1));
63                }
64                // Do NOT take ownership of the engine - the first owner keeps it.
65                None
66            }
67            Err(JSEngineError::AlreadyShutDown) => {
68                // BAO PATCH (BCE-20260627-009): Engine was previously
69                // initialized AND shut down. We cannot recover the handle from
70                // `JS_ENGINE` (it was cleared on the owner's Drop). Return
71                // `None` and let the runtime proceed - the bao layer ensures
72                // the first BaoRuntime's engine stays alive when needed.
73                None
74            }
75            Err(e) => panic!("JSEngine::init() failed: {:?}", e),
76        };
77        Self(engine)
78    }
79}
80
81impl Drop for JSEngineSetup {
82    fn drop(&mut self) {
83        // BAO PATCH (BCE-20260627-009): Do NOT clear JS_ENGINE and do NOT drop
84        // the engine. The engine is a process-global singleton; its handle must
85        // persist in JS_ENGINE across BaoRuntime teardown so subsequent
86        // BaoRuntime instances reuse it.
87        //
88        // mozjs JSEngine is a process-global singleton with an irreversible
89        // state machine (Uninitialized->Initialized->ShutDown). Once
90        // `JS_ShutDown()` runs (catalyzed by `JSEngine::drop`), the same
91        // process can never re-init. This breaks bao's multi-BaoRuntime model
92        // (cargo test runner). Fix: leak the engine (`std::mem::forget`) AND
93        // keep its handle in `JS_ENGINE` (do not clear it). The OS reclaims all
94        // JS engine resources on process exit; behaviorally equivalent, with no
95        // memory-safety regression, and correct for an embedded single-process
96        // runtime that must tolerate repeated construction and teardown.
97        let Some(engine) = self.0.take() else {
98            return;
99        };
100        std::mem::forget(engine);
101    }
102}