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}