diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 2ae6a1d809f2..c46d8dfb7b49 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -1153,7 +1153,7 @@ pub(crate) fn resolver_bundle_options_subset( } impl<'a> Transpiler<'a> { - /// Called by [`init_runtime_state`](../runtime/jsc_hooks.rs) + /// Called by [`bun_runtime_init_runtime_state`](../runtime/jsc_hooks.rs) /// to write `vm.transpiler`. Builds on: /// * [`options::BundleOptions::from_api`] — `bun_bundler::options` /// * [`Resolver::init1`] — `bun_resolver` @@ -1177,8 +1177,9 @@ impl<'a> Transpiler<'a> { /// In-place sibling of [`Self::init`]: builds the `Transpiler` directly into /// `dst` rather than returning it by value, so callers that already own its /// final storage — most importantly `VirtualMachine.transpiler`, written by - /// [`init_runtime_state`](../runtime/jsc_hooks.rs) once per VM — avoid the - /// multi-KB `stack temporary → return slot → final home` double `memcpy`. + /// [`bun_runtime_init_runtime_state`](../runtime/jsc_hooks.rs) once per VM — + /// avoid the multi-KB `stack temporary → return slot → final home` double + /// `memcpy`. /// /// On `Ok(())`, every field of `dst` is initialised. On `Err`, `dst` is /// untouched (all fallible work happens before the first field write), so the diff --git a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs index e7c63e78ba66..c3e1fefe0cb3 100644 --- a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs +++ b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs @@ -410,10 +410,9 @@ impl HTTPClient { // §Dispatch (cycle-break): `RareData.defaultClientSslCtx()` and // `RareData.sslCtxCache().getOrCreateOpts()` reach // `RuntimeState.ssl_ctx_cache` (high-tier `bun_runtime`); routed - // through `RuntimeHooks` so this crate stays below `bun_runtime`. + // through the `bun_runtime_*` link-time externs so this crate stays + // below `bun_runtime`. let secure_ptr: Option<*mut uws::SslCtx> = if SSL { - let hooks = - bun_jsc::virtual_machine::runtime_hooks().expect("RuntimeHooks not installed"); 'brk: { if let Some(config) = &client_ref.ssl_config { if config.requires_custom_request_ctx { @@ -424,7 +423,7 @@ impl HTTPClient { // SAFETY: `vm_ptr` is the live per-thread VM (caller // contract); JS thread. let ctx = unsafe { - (hooks.ssl_ctx_cache_get_or_create)( + bun_jsc::virtual_machine::bun_runtime_ssl_ctx_cache_get_or_create( vm_ptr, &config.as_usockets_for_client_verification(), &mut err, @@ -450,7 +449,9 @@ impl HTTPClient { } } // SAFETY: `vm_ptr` is the live per-thread VM; JS thread. - Some(unsafe { (hooks.default_client_ssl_ctx)(vm_ptr) }) + Some(unsafe { + bun_jsc::virtual_machine::bun_runtime_default_client_ssl_ctx(vm_ptr) + }) } } else { None diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index b7efd2426321..60ccb397d1a6 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -22,7 +22,8 @@ use bun_resolver::package_json::{ use crate::expr_jsc::ExprJsc; use bun_jsc::js_property_iterator::JSPropertyIteratorOptions; use bun_jsc::virtual_machine::{ - InitOptions as VirtualMachineInitOptions, MacroModeGuard, VirtualMachine, runtime_hooks, + InitOptions as VirtualMachineInitOptions, MacroModeGuard, VirtualMachine, + bun_runtime_body_mixin_get_blob, }; #[allow(deprecated)] use bun_jsc::{ @@ -418,7 +419,7 @@ impl Macro { // The resolver's forward-decl `BundleOptions` does not carry // `transform_options` (the canonical owner is the bundler's // `BundleOptions<'a>`), and - // `RuntimeHooks::init_runtime_state` builds the macro VM's + // `bun_runtime_init_runtime_state` builds the macro VM's // transpiler from a fresh `TransformOptions` value rather than // borrowing the caller's, so there is nothing to mutate-and-restore // on `resolver.opts` here. `log`/`env_loader` *are* threaded so the @@ -663,11 +664,11 @@ impl<'a> Run<'a> { // LAYERING: `Response`/`Request` (and their `BodyMixin:: // get_blob_without_call_frame`) live in `bun_runtime:: // webcore`, which depends on this crate. The downcast + - // body-extract is dispatched through `RuntimeHooks` (the + // body-extract is dispatched through the + // `bun_runtime_body_mixin_get_blob` link-time extern (the // established §Dispatch cycle-break) so the data shapes // stay in the high tier. - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); - if let Some(body_blob) = (hooks.body_mixin_get_blob)(value, self.global)? { + if let Some(body_blob) = bun_runtime_body_mixin_get_blob(value, self.global)? { return self.run(body_blob); } else if let Some(resp) = value.as_::() { blob_ = Some(resp); diff --git a/src/jsc/AbortSignal.rs b/src/jsc/AbortSignal.rs index 1c32228b8fae..b6b1315a3223 100644 --- a/src/jsc/AbortSignal.rs +++ b/src/jsc/AbortSignal.rs @@ -276,7 +276,7 @@ impl AbortReason { // // LAYERING: `EventLoopTimer` + `TimerFlags` live in `bun_event_loop` (lower // tier). The per-VM timer heap (`Timer::All`) lives in `bun_runtime` (higher -// tier) and is reached through `RuntimeHooks::{timer_insert,timer_remove}` — +// tier) and is reached through `bun_runtime_timer_{insert,remove}` — // see `VirtualMachine::timer_insert/remove`. C++ only ever sees `*mut Timeout` // as an opaque token round-tripped through `create`/`run`/`deinit`, so the // concrete layout is private to Rust; `repr(C)` is here so `offset_of!` is diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 06af46f40785..8f02d5003a89 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -506,12 +506,10 @@ fn message_with_type_and_level_( let writer: &mut dyn bun_io::Write = raw_writer; // LAYERING: `Jest::runner()` lives in `bun_runtime::test_runner` (forward - // dep on the high tier). Dispatch through `RuntimeHooks` instead — the - // high-tier hook checks `Jest.runner` and calls `onBeforePrint()`; no-op - // when `bun test` isn't running or hooks aren't installed. - if let Some(hooks) = crate::virtual_machine::runtime_hooks() { - (hooks.console_on_before_print)(); - } + // dep on the high tier). Dispatch through the link-time extern instead — + // the high-tier body checks `Jest.runner` and calls `onBeforePrint()`; + // no-op when `bun test` isn't running. + crate::virtual_machine::bun_runtime_console_on_before_print(); let mut print_length = len; // Get console depth from CLI options or bunfig, fallback to default. @@ -4689,21 +4687,20 @@ pub mod formatter { // `Response`/`Request`/`Blob`/`S3Client`/`Archive`/`BuildArtifact`/ // `FetchHeaders`/`TimeoutObject`/`ImmediateObject`/`BuildMessage`/ // `ResolveMessage`/Jest asymmetric matchers — all of which live in - // `bun_runtime` (forward-dep). Dispatch through `RuntimeHooks` so - // the high tier owns the downcasts. Hook returns `true` when it - // formatted `value`; otherwise we fall through to the generic - // object printer below. - if let Some(hooks) = crate::virtual_machine::runtime_hooks() { - // The hook only ever - // seeds a `ZigString` that `get_class_name` immediately - // overwrites with JSC-owned bytes, so a shared zero buffer is - // sufficient and keeps 512B off every recursive frame. - static NAME_BUF: [u8; 512] = [0; 512]; - let handled = - (hooks.console_print_runtime_object)(self, writer_, value, &NAME_BUF, C)?; - if handled { - return Ok(()); - } + // `bun_runtime` (forward-dep). Dispatch through the link-time + // extern so the high tier owns the downcasts. It returns `true` + // when it formatted `value`; otherwise we fall through to the + // generic object printer below. + // + // The hook only ever + // seeds a `ZigString` that `get_class_name` immediately + // overwrites with JSC-owned bytes, so a shared zero buffer is + // sufficient and keeps 512B off every recursive frame. + static NAME_BUF: [u8; 512] = [0; 512]; + if crate::virtual_machine::bun_runtime_console_print_runtime_object( + self, writer_, value, &NAME_BUF, C, + )? { + return Ok(()); } // `DOMFormData` is a C++-backed WebCore type — no `JsClass` derive, diff --git a/src/jsc/Debugger.rs b/src/jsc/Debugger.rs index 643f516cb7cd..6fbd78c704fe 100644 --- a/src/jsc/Debugger.rs +++ b/src/jsc/Debugger.rs @@ -5,7 +5,7 @@ //! compiles against the `bun_jsc` crate's available dependency set. //! `retroactively_report_discovered_tests` reaches into the `bun:test` runner //! (`bun_runtime::test_runner`) — a forward-dep cycle — so it dispatches -//! through [`RuntimeHooks::retroactively_report_discovered_tests`]. +//! through [`bun_runtime_retroactively_report_discovered_tests`]. use core::cell::Cell; use core::ffi::{c_int, c_void}; @@ -15,7 +15,7 @@ use bun_core::String as BunString; use bun_io::KeepAlive; use bun_io::posix_event_loop::{AllocatorType, get_vm_ctx}; -use crate::virtual_machine::{VirtualMachine, runtime_hooks}; +use crate::virtual_machine::{VirtualMachine, bun_runtime_retroactively_report_discovered_tests}; use crate::{self as jsc, CallFrame, JSGlobalObject, ZigException}; bun_core::declare_scope!(debugger, visible); @@ -415,7 +415,7 @@ impl Debugger { pub fn start_js_debugger_thread(other_vm: *mut VirtualMachine) { // The global allocator is mimalloc and `InitOptions` does not carry // `allocator`/`env_loader` (those are wired by - // `RuntimeHooks::init_runtime_state`). + // `bun_runtime_init_runtime_state`). bun_core::Output::Source::configure_named_thread(bun_core::zstr!("Debugger")); bun_core::scoped_log!(debugger, "startJSDebuggerThread"); jsc::mark_binding(); @@ -795,13 +795,11 @@ pub fn test_reporter_agent_enable(agent: *mut TestReporterHandle) { // // LAYERING: `retroactivelyReportDiscoveredTests` reaches into // the test runner (`bun_test.DescribeScope`), which lives in `bun_runtime::test_runner` - // — a forward-dep cycle. Dispatched through [`RuntimeHooks`]. - if let Some(hooks) = runtime_hooks() { - // SAFETY: `handle` is the live C++ agent just stored above. - unsafe { - (hooks.retroactively_report_discovered_tests)(dbg.test_reporter_agent.handle) - }; - } + // — a forward-dep cycle. Dispatched through the link-time extern. + // SAFETY: `handle` is the live C++ agent just stored above. + unsafe { + bun_runtime_retroactively_report_discovered_tests(dbg.test_reporter_agent.handle) + }; } } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 5af7f93e15e2..5060e9d42ecb 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -77,21 +77,22 @@ pub struct EntryPointResult { /// Downstream-compat alias: lib.rs previously exposed `virtual_machine::InitOptions`. /// Carries the cross-tier subset of `Options` that [`init`] and -/// `RuntimeHooks::init_runtime_state` need. `transform_options`/`debugger` +/// `bun_runtime_init_runtime_state` need. `transform_options`/`debugger` /// live in `bun_options_types` (already a dep of `bun_jsc`), so they thread /// through here instead of being dropped at the CLI call-site. pub struct InitOptions { - /// The CLI's `api.TransformOptions`. Consumed by `RuntimeHooks::init_runtime_state` + /// The CLI's `api.TransformOptions`. Consumed by + /// `bun_runtime_init_runtime_state` /// → `Transpiler::init(.., configureTransformOptionsForBunVM(args), ..)`. pub transform_options: bun_options_types::schema::api::TransformOptions, - /// Consumed by `RuntimeHooks::init_runtime_state` → `configureDebugger`. + /// Consumed by `bun_runtime_init_runtime_state` → `configureDebugger`. pub debugger: bun_options_types::context::Debugger, /// When `Some`, [`init`] adopts /// the caller's log instead of boxing a fresh one (CLI-path macros pass the /// transpiler's log so macro load errors land in the bundle output). pub log: Option>, /// Forwarded to - /// `RuntimeHooks::init_runtime_state` so the high-tier `Transpiler::init` + /// `bun_runtime_init_runtime_state` so the high-tier `Transpiler::init` /// reuses the caller's env loader. pub env_loader: Option>>, pub graph: Option<&'static dyn bun_resolver::StandaloneModuleGraph>, @@ -170,13 +171,13 @@ pub struct VirtualMachine { pub node_fs: Option<*mut c_void>, /// Opaque per-VM `bun_runtime` state (boxed `timer::All` + /// `Body::Value::HiveAllocator` + …). Set by - /// `RuntimeHooks::init_runtime_state` in [`init`]; reclaimed by - /// `RuntimeHooks::deinit_runtime_state` in [`destroy`]. Null when no high - /// tier is installed (e.g. `bun_jsc` unit tests). + /// `bun_runtime_init_runtime_state` in [`init`]; reclaimed by + /// `bun_runtime_deinit_runtime_state` in [`destroy`]. Null before [`init`] + /// installs it. /// /// Note: the per-VM timer state and body-value pool live inside this box — /// both types are owned by `bun_runtime` (forward dep). Access goes through - /// [`RuntimeHooks::timer_insert`] / [`RuntimeHooks::body_value_hive_ref`]. + /// [`bun_runtime_timer_insert`] / [`bun_runtime_timer_remove`]. pub runtime_state: *mut c_void, pub event_loop_handle: Option<*mut PlatformEventLoop>, /// Pending `unref` count drained by the event-loop thread. Atomic because @@ -1258,40 +1259,11 @@ impl VirtualMachine { self.had_errors = false; // The actual print path needs `ConsoleObject::Formatter` + - // `ZigException` (high tier). Dispatch through `RuntimeHooks` — - // mirroring `auto_tick`/`ensure_debugger` — so the error is actually - // emitted to stderr before callers hard-exit. With no hook installed - // (low-tier unit tests), fail loudly: PORTING.md §Forbidden bans a - // silent no-op here since the real path has observable logic. - if let Some(hooks) = runtime_hooks() { - (hooks.print_exception)(self, result, exception_list); - } else { - // Low-tier fallback (no `bun_runtime` installed — unit tests): - // we cannot reach `ConsoleObject::Formatter`, so emit a degraded - // one-line render via the buffered error writer. The full path - // routes through `printErrorlikeObject` - // (which formats name/message/stack); the closest we can do here - // without the high tier is the value's own `toString`. - let _ = exception_list; - let writer = bun_core::Output::error_writer(); - let global = self.global(); - let display = result - .to_error() - .unwrap_or(result) - .get_zig_string(global) - .ok(); - match display { - Some(zs) => { - let utf8 = zs.to_owned_slice(); - let _ = writer.write_all(utf8.as_slice()); - let _ = writer.write_all(b"\n"); - } - None => { - let _ = writer.write_all(b"[unhandled exception]\n"); - } - } - let _ = writer.flush(); - } + // `ZigException` (high tier), so the body lives in + // `bun_runtime::jsc_hooks` — mirroring `auto_tick`/`ensure_debugger` + // — and the error is actually emitted to stderr before callers + // hard-exit. + bun_runtime_print_exception(self, result, exception_list); // The hook does not unwind across the dispatch boundary, so restore // linearly. @@ -1356,14 +1328,11 @@ impl VirtualMachine { } /// The body lives in `bun_runtime` (it constructs `bun.api.Debugger`), so - /// dispatch through [`RuntimeHooks::ensure_debugger`] like - /// [`reload_entry_point`] does. No-op when hooks aren't installed (pure - /// `bun_jsc` unit tests). + /// dispatch through [`bun_runtime_ensure_debugger`] like + /// [`reload_entry_point`] does. pub fn ensure_debugger(&mut self, block_until_connected: bool) -> Result<(), bun_core::Error> { - if let Some(hooks) = runtime_hooks() { - // SAFETY: hook contract — `self` is the live per-thread VM. - unsafe { (hooks.ensure_debugger)(self, block_until_connected) }; - } + // SAFETY: hook contract — `self` is the live per-thread VM. + unsafe { bun_runtime_ensure_debugger(self, block_until_connected) }; Ok(()) } @@ -1391,12 +1360,12 @@ impl VirtualMachine { return true; } - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); if self.is_handling_uncaught_exception { self.run_error_handler(err, None); - // SAFETY: `global_object` is the live VM global; `process_exit` is - // `bun_runtime::node::process::exit` (main-thread `noreturn`). - unsafe { (hooks.process_exit)(global_object.as_ptr(), 7) }; + // SAFETY: `global_object` is the live VM global; + // `bun_runtime_process_exit` is `bun_runtime::node::process::exit` + // (main-thread `noreturn`). + unsafe { bun_runtime_process_exit(global_object.as_ptr(), 7) }; panic!("Uncaught exception while handling uncaught exception"); } self.is_handling_uncaught_exception = true; @@ -1416,7 +1385,7 @@ impl VirtualMachine { // that re-entry exits 7 ("handler threw") instead of 1. self.is_handling_uncaught_exception = false; // SAFETY: see above. - unsafe { (hooks.process_exit)(global_object.as_ptr(), 1) }; + unsafe { bun_runtime_process_exit(global_object.as_ptr(), 1) }; panic!("made it past process.exit()"); } // TODO maybe we want a separate code path for uncaught exceptions @@ -1530,21 +1499,17 @@ impl VirtualMachine { // them instead of leaking. Must precede `close_all_socket_groups` // and `~RunLoop::Timer` so no dangling `WTFTimer` heap node is // observed during the walk. - if let Some(hooks) = runtime_hooks() { - // SAFETY: `self` is the live per-thread VM on the JS thread; - // `runtime_state` is still installed (it's torn down in - // `destroy()`, well after `global_exit`). - unsafe { (hooks.cancel_all_timers)(core::ptr::from_mut(self)) }; - } + // SAFETY: `self` is the live per-thread VM on the JS thread; + // `runtime_state` is still installed (it's torn down in + // `destroy()`, well after `global_exit`). + unsafe { bun_runtime_cancel_all_timers(core::ptr::from_mut(self)) }; // Detached worker threads may still be in startVM()/spin() using // the process-global resolver BSSMap singletons. transpiler.deinit() // below frees those singletons, so request termination of every // live worker and wait for each to reach shutdown() first. - if let Some(hooks) = runtime_hooks() { - // Main-thread only; futex-waits on every registered worker - // until each unparks at shutdown(). - (hooks.terminate_all_workers_and_wait)(10_000); - } + // Main-thread only; futex-waits on every registered worker + // until each unparks at shutdown(). + bun_runtime_terminate_all_workers_and_wait(10_000); // Every worker has now posted its close task to our concurrent // queue (OUTSTANDING is decremented after dispatchExit). Drop @@ -1605,15 +1570,16 @@ impl VirtualMachine { extern crate alloc; // ────────────────────────────────────────────────────────────────────────── -// §Dispatch — `bun_runtime` vtable. +// §Dispatch — upward calls into `bun_runtime`. // // `init` / `load_entry_point` / the `bun -e` path reach into types that live // in the higher-tier `bun_runtime` crate (`api::Timer::All`, `node::fs`, // `webcore::Body`, the bundler entry-point generator, …). Per PORTING.md -// §Dispatch (cold-path), the low tier defines a manual vtable; `bun_runtime` -// defines the `#[no_mangle]` static `__BUN_RUNTIME_HOOKS`. The fn-ptr -// indirection at every call site below is acceptable — each does -// real work (I/O, JS callback, allocation). +// §Dispatch (cold-path), each `bun_runtime_*` fn below is declared +// `extern "Rust"` here and has exactly one `#[no_mangle]` definition in +// `bun_runtime::jsc_hooks`, resolved at link time. No fn-pointer table, no +// init-order hazard; every call does real work (I/O, JS callback, +// allocation), so the cross-crate call is not a hot-path concern. // ────────────────────────────────────────────────────────────────────────── /// Opaque per-VM state owned by `bun_runtime` (Timer::All, NodeFS, Body hive @@ -1621,7 +1587,7 @@ extern crate alloc; /// casts back on the other side of each hook. pub type RuntimeState = *mut c_void; -pub struct RuntimeHooks { +unsafe extern "Rust" { /// `bun.api.Timer.All.init()` + `Body.Value.HiveAllocator.init()` + /// `configureDebugger()` — everything `init()` does that names a /// `bun_runtime` type. Called once with the freshly-boxed VM AFTER @@ -1630,98 +1596,109 @@ pub struct RuntimeHooks { /// `Transpiler::init` fails (e.g. a deleted cwd → `getcwd` ENOENT); the /// hook unwinds its own allocations, so [`VirtualMachine::init`] only has to /// propagate the error. - pub init_runtime_state: unsafe fn( + pub fn bun_runtime_init_runtime_state( vm: *mut VirtualMachine, opts: &mut InitOptions, - ) -> Result, - /// Reclaim the per-VM state boxed by `init_runtime_state`. Called from - /// [`VirtualMachine::destroy`] (worker teardown) with the exact opaque - /// pointer `init_runtime_state` returned (or null). The high tier - /// `heap::take`s it and clears its thread-local cache. Without this slot - /// every worker leaked one box. - pub deinit_runtime_state: unsafe fn(vm: *mut VirtualMachine, state: RuntimeState), + ) -> Result; + /// Reclaim the per-VM state boxed by `bun_runtime_init_runtime_state`. + /// Called from [`VirtualMachine::destroy`] (worker teardown) with the + /// exact opaque pointer `bun_runtime_init_runtime_state` returned (or + /// null). The high tier `heap::take`s it and clears its thread-local + /// cache. Without this hook every worker leaked one box. + pub fn bun_runtime_deinit_runtime_state(vm: *mut VirtualMachine, state: RuntimeState); /// `ServerEntryPoint.generate(watch, entry_path)` — produces the synthetic /// `bun:main` module body for `entry_path`. Returns `false` on error /// (error already logged into `vm.log`). - pub generate_entry_point: fn(vm: &VirtualMachine, watch: bool, entry_path: &[u8]) -> bool, + pub safe fn bun_runtime_generate_entry_point( + vm: &VirtualMachine, + watch: bool, + entry_path: &[u8], + ) -> bool; /// `loadPreloads()` — runs `--preload` scripts. Returns the first rejected /// preload promise if any, else null. Errors propagate /// (resolver failures / `ModuleNotFound`). - pub load_preloads: - unsafe fn(vm: *mut VirtualMachine) -> Result<*mut JSInternalPromise, bun_core::Error>, + pub fn bun_runtime_load_preloads( + vm: *mut VirtualMachine, + ) -> Result<*mut JSInternalPromise, bun_core::Error>; /// `ensureDebugger(block_until_connected)` — no-op when no debugger. - pub ensure_debugger: unsafe fn(vm: *mut VirtualMachine, block_until_connected: bool), + pub fn bun_runtime_ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool); /// `eventLoop().autoTick()` — needs `Timer::All` for the timeout calc. - /// Hoisted here so `event_loop.rs` doesn't need its own hook table. - pub auto_tick: unsafe fn(vm: *mut VirtualMachine), - /// `eventLoop().autoTickActive()` — like `auto_tick` but only sleeps in - /// the uSockets loop while it has active handles. - /// Separate slot because the body skips `runImminentGCTimer` / + /// Hoisted here so `event_loop.rs` doesn't need its own extern block. + pub fn bun_runtime_auto_tick(vm: *mut VirtualMachine); + /// `eventLoop().autoTickActive()` — like `bun_runtime_auto_tick` but only + /// sleeps in the uSockets loop while it has active handles. + /// Separate fn because the body skips `runImminentGCTimer` / /// `handleRejectedPromises` and falls through to `tickWithoutIdle` when - /// idle — folding it into `auto_tick` would change shutdown semantics. - pub auto_tick_active: unsafe fn(vm: *mut VirtualMachine), + /// idle — folding it into `bun_runtime_auto_tick` would change shutdown + /// semantics. + pub fn bun_runtime_auto_tick_active(vm: *mut VirtualMachine); /// `printException` / `printErrorlikeObject` — formats `value` (or its /// wrapped `JSC::Exception`) to stderr via `ConsoleObject::Formatter`. /// High tier /// owns the formatter; low tier dispatches here from /// [`VirtualMachine::run_error_handler`]. - pub print_exception: - fn(vm: &mut VirtualMachine, value: JSValue, exception_list: Option<&mut ExceptionList>), + pub safe fn bun_runtime_print_exception( + vm: &mut VirtualMachine, + value: JSValue, + exception_list: Option<&mut ExceptionList>, + ); /// `vm.timer.insert(&mut event_loop_timer)` — `Timer::All` lives in /// `bun_runtime::RuntimeState` (b2-cycle); low-tier callers - /// (`AbortSignal::Timeout`) reach it through this slot. - pub timer_insert: unsafe fn( + /// (`AbortSignal::Timeout`) reach it through this fn. + pub fn bun_runtime_timer_insert( vm: *mut VirtualMachine, timer: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, - ), - /// `vm.timer.remove(&mut event_loop_timer)` — see `timer_insert`. - pub timer_remove: unsafe fn( + ); + /// `vm.timer.remove(&mut event_loop_timer)` — see `bun_runtime_timer_insert`. + pub fn bun_runtime_timer_remove( vm: *mut VirtualMachine, timer: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, - ), + ); /// `RareData.defaultClientSslCtx()` — lazy default-trust-store client /// `SSL_CTX*`, shared by every `tls: true` outbound connection that didn't /// supply explicit options. The storage slot lives in `RareData` /// (low-tier) but population reaches `RuntimeState.ssl_ctx_cache` /// (`bun_runtime`, b2-cycle). - pub default_client_ssl_ctx: unsafe fn(vm: *mut VirtualMachine) -> *mut uws::SslCtx, + pub fn bun_runtime_default_client_ssl_ctx(vm: *mut VirtualMachine) -> *mut uws::SslCtx; /// `RareData.sslCtxCache().getOrCreateOpts(opts, &err)` — per-VM /// digest-keyed weak `SSL_CTX*` cache. Returns a +1 ref or `None` on /// BoringSSL rejection (`err` populated). `SSLContextCache` lives in /// `bun_runtime::RuntimeState` (b2-cycle). - pub ssl_ctx_cache_get_or_create: unsafe fn( + pub fn bun_runtime_ssl_ctx_cache_get_or_create( vm: *mut VirtualMachine, opts: &uws::SocketContext::BunSocketContextOptions, err: &mut uws::create_bun_socket_error_t, - ) -> Option<*mut uws::SslCtx>, + ) -> Option<*mut uws::SslCtx>; /// Lazy `NodeFS` creation. /// `NodeFS` lives in `bun_runtime`; the high tier boxes one and returns /// the type-erased pointer. Stored back into `vm.node_fs`. - pub create_node_fs: unsafe fn(vm: *mut VirtualMachine) -> *mut c_void, + pub fn bun_runtime_create_node_fs(vm: *mut VirtualMachine) -> *mut c_void; /// `ObjectURLRegistry` lookup. Registry lives in `bun_runtime::webcore`. - pub has_blob_url: fn(blob_id: &[u8]) -> bool, + pub safe fn bun_runtime_has_blob_url(blob_id: &[u8]) -> bool; /// `Response::get_blob_without_call_frame` / /// `Request::get_blob_without_call_frame`. If /// `value` downcasts to a `Response` or `Request` (both live in /// `bun_runtime::webcore`), return its body Blob wrapped in a resolved /// Promise; `Ok(None)` to fall through to the `Blob`/`BuildMessage`/ /// `ResolveMessage` arms in `Macro::Run::coerce`. - pub body_mixin_get_blob: - fn(value: JSValue, global: &JSGlobalObject) -> JsResult>, + pub safe fn bun_runtime_body_mixin_get_blob( + value: JSValue, + global: &JSGlobalObject, + ) -> JsResult>; /// `process.exit(global, code)`. Main-thread is `noreturn`; in a worker /// it returns and the caller `panic!`s. Lives in `bun_runtime::node` /// (forward-dep cycle), so [`uncaught_exception`] reaches it through this - /// slot instead of the linker. - pub process_exit: unsafe fn(global: *mut JSGlobalObject, code: u8), + /// fn. + pub fn bun_runtime_process_exit(global: *mut JSGlobalObject, code: u8); /// `node_cluster_binding.handleInternalMessageChild(global, data)`. - pub handle_ipc_internal_child: unsafe fn(global: *mut JSGlobalObject, data: JSValue), + pub fn bun_runtime_handle_ipc_internal_child(global: *mut JSGlobalObject, data: JSValue); /// `node_cluster_binding.child_singleton.deinit()`. - pub ipc_child_singleton_deinit: fn(), + pub safe fn bun_runtime_ipc_child_singleton_deinit(); /// `onBeforePrint()` for the `bun:test` runner, which lives in `bun_runtime`; /// `console.log` calls this so the test reporter can flush its line state /// before user output interleaves with it. No-op when `bun test` isn't /// running. - pub console_on_before_print: fn(), + pub safe fn bun_runtime_console_on_before_print(); /// `ConsoleObject.Formatter` runtime-type dispatch /// over `Response`/`Request`/`Blob`/`S3Client`/`Archive`/ /// `BuildArtifact`/`FetchHeaders`/`Timer`/`Immediate`/`BuildMessage`/ @@ -1731,21 +1708,21 @@ pub struct RuntimeHooks { /// Returns `Ok(true)` when `value` was one of the runtime types and was /// fully formatted into `writer`; `Ok(false)` to fall through to the /// generic object printer. - pub console_print_runtime_object: for<'a, 'f> fn( - formatter: &'a mut crate::console_object::Formatter<'f>, - writer: &'a mut dyn bun_io::Write, + pub safe fn bun_runtime_console_print_runtime_object( + formatter: &mut crate::console_object::Formatter<'_>, + writer: &mut dyn bun_io::Write, value: JSValue, - name_buf: &'a [u8; 512], + name_buf: &[u8; 512], enable_ansi_colors: bool, - ) -> JsResult, + ) -> JsResult; /// Applies `--compile`-baked runtime flags to the /// worker's transpiler. `graph` is the same trait object stored in /// `vm.standalone_module_graph` (the high tier downcasts to its concrete /// `bun_standalone_graph::Graph` — the sole implementor). - pub apply_standalone_runtime_flags: unsafe fn( + pub fn bun_runtime_apply_standalone_runtime_flags( transpiler: *mut Transpiler<'static>, graph: &'static dyn bun_resolver::StandaloneModuleGraph, - ), + ); /// Parse `execArgv` against the `RunCommand` /// param table and return the resulting `allow_addons` value /// (`!args.flag("--no-addons")`), or `None` if parsing failed. @@ -1754,24 +1731,25 @@ pub struct RuntimeHooks { /// the caller writes the returned bool back into /// `transform_options.allow_addons` so the override semantics /// ("override the existing even if it was set") match. - pub parse_worker_exec_argv_allow_addons: - unsafe fn(exec_argv: &[bun_core::WTFStringImpl]) -> Option, + pub fn bun_runtime_parse_worker_exec_argv_allow_addons( + exec_argv: &[bun_core::WTFStringImpl], + ) -> Option; /// `CronJob.clearAllForVM(vm, .teardown)`. `CronJob` lives in /// `bun_runtime::api::cron`. - pub cron_clear_all_teardown: fn(vm: &mut VirtualMachine), + pub safe fn bun_runtime_cron_clear_all_teardown(vm: &mut VirtualMachine); /// `WebWorker.terminateAllAndWait(timeout_ms)`. /// `WebWorker` lives in this crate but the /// `web_worker` module is above `virtual_machine` in the dep graph /// (forward use) AND the body re-enters `bun_runtime` for the worker /// thread's `event_loop().auto_tick()`, so [`global_exit`] reaches it - /// through this slot. Prevents detached worker threads from racing the + /// through this fn. Prevents detached worker threads from racing the /// freed resolver BSSMap singletons during `transpiler.deinit()`. - pub terminate_all_workers_and_wait: fn(timeout_ms: u64), + pub safe fn bun_runtime_terminate_all_workers_and_wait(timeout_ms: u64); /// `CronJob.clearAllForVM(vm, .reload)`. - /// Same impl as `cron_clear_all_teardown` but + /// Same impl as `bun_runtime_cron_clear_all_teardown` but /// the `.reload` mode preserves the next-fire schedule across the new /// global so timers re-register instead of being torn down. - pub cron_clear_all_reload: fn(vm: &mut VirtualMachine), + pub safe fn bun_runtime_cron_clear_all_reload(vm: &mut VirtualMachine); /// Standalone-graph sourcemap load. /// The concrete `bun_standalone_graph::Graph` / `File` / `LazySourceMap` /// live above `bun_jsc`; the high tier reaches them via the graph's own @@ -1781,8 +1759,9 @@ pub struct RuntimeHooks { /// strong-ref'd via the returned `Arc`). [`resolve_source_mapping`] /// caches it into `source_mappings` so subsequent lookups hit the fast /// path. The caller gates the call on `vm.standalone_module_graph`. - pub load_standalone_sourcemap: - fn(path: &[u8]) -> Option>, + pub safe fn bun_runtime_load_standalone_sourcemap( + path: &[u8], + ) -> Option>; /// `TestReporterAgent.retroactivelyReportDiscoveredTests(agent)`. /// Walks the active test file's /// scope tree and emits `reportTestFoundWithLocation` for every test @@ -1790,8 +1769,9 @@ pub struct RuntimeHooks { /// live in `bun_runtime::test_runner` (forward-dep cycle), so the body is /// hoisted to the high tier; low-tier `Bun__TestReporterAgentEnable` /// dispatches here. No-op when `bun test` isn't running. - pub retroactively_report_discovered_tests: - unsafe fn(agent: *mut crate::debugger::TestReporterHandle), + pub fn bun_runtime_retroactively_report_discovered_tests( + agent: *mut crate::debugger::TestReporterHandle, + ); /// Cancel every `TimeoutObject` / `ImmediateObject` still in the calling /// thread's `timer::All` heap so their JS pins and in-heap `+1` refs drop /// before the GC sweep. `timer::All` lives in `bun_runtime` (forward-dep); @@ -1800,7 +1780,7 @@ pub struct RuntimeHooks { /// # Safety /// `vm` is the live per-thread VM; `runtime_state` must still be installed /// and the JSC heap must not have been swept yet. - pub cancel_all_timers: unsafe fn(vm: *mut VirtualMachine), + pub fn bun_runtime_cancel_all_timers(vm: *mut VirtualMachine); } /// Canonical `EventLoopCtx` vtable for a `*mut VirtualMachine` owner — the JS @@ -1873,8 +1853,8 @@ impl VirtualMachine { } impl VirtualMachine { - /// `vm.timer.insert(timer)` — dispatches through `RuntimeHooks` because - /// `Timer::All` lives in `bun_runtime` (b2-cycle). + /// `vm.timer.insert(timer)` — dispatches into `bun_runtime` because + /// `Timer::All` lives there (b2-cycle). /// /// # Safety /// `timer` must point at a live `EventLoopTimer` not currently linked into @@ -1884,9 +1864,8 @@ impl VirtualMachine { vm: *mut Self, timer: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, ) { - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); // SAFETY: per fn contract; `vm` is the live per-thread VM. - unsafe { (hooks.timer_insert)(vm, timer) } + unsafe { bun_runtime_timer_insert(vm, timer) } } /// `vm.timer.remove(timer)` — see [`Self::timer_insert`]. @@ -1899,29 +1878,11 @@ impl VirtualMachine { vm: *mut Self, timer: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, ) { - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); // SAFETY: per fn contract; `vm` is the live per-thread VM. - unsafe { (hooks.timer_remove)(vm, timer) } + unsafe { bun_runtime_timer_remove(vm, timer) } } } -unsafe extern "Rust" { - /// The single `&'static` instance, defined `#[no_mangle]` in - /// `bun_runtime::jsc_hooks`. Link-time resolved — no `AtomicPtr`, no - /// init-order hazard. - /// `RuntimeHooks` is an immutable POD of fn-ptrs with a single definition; - /// reading it has no precondition beyond the link succeeding → `safe static`. - safe static __BUN_RUNTIME_HOOKS: RuntimeHooks; -} - -#[inline] -pub fn runtime_hooks() -> Option<&'static RuntimeHooks> { - // Link-time-resolved `&'static` Rust-ABI static. Always `Some` — - // kept as `Option` so existing call sites (`if let Some(hooks)`) compile - // unchanged; the branch folds away. - Some(&__BUN_RUNTIME_HOOKS) -} - #[allow(improper_ctypes)] // VirtualMachine is opaque to C++; passed as `void*` unsafe extern "C" { // safe: `console`/`worker_ptr` are opaque round-trip pointers C++ stores @@ -1970,7 +1931,7 @@ impl VirtualMachine { /// Note: every step that names a `bun_runtime` / `bun_webcore` type /// (`Timer.All.init`, `Body.Value.HiveAllocator`, `configureDebugger`, /// `Config.configureTransformOptionsForBunVM`, `ParentDeathWatchdog`) is - /// dispatched through `RuntimeHooks::init_runtime_state` so `bun_jsc` does + /// dispatched through `bun_runtime_init_runtime_state` so `bun_jsc` does /// not name those types directly. The hook receives the boxed VM after the /// JSC-tier fields are populated and finishes the rest. pub fn init(mut opts: InitOptions) -> Result<*mut VirtualMachine, bun_core::Error> { @@ -2124,19 +2085,17 @@ impl VirtualMachine { // `WTFTimer__update` (JSC's GC scheduler), which dereferences // `runtime_state().timer` — so this hook MUST run first or that path // null-derefs. - if let Some(hooks) = runtime_hooks() { - // SAFETY: hook contract — `vm` is the unique live VM on this - // thread. Write through the raw `vm` ptr (not `vm_ref`) so no - // `&mut VirtualMachine` is held live across the hook call — the - // hook body itself dereferences `vm`. - // - // `?`: on `Err` (e.g. a deleted cwd → `getcwd` ENOENT out of - // `Transpiler::init`) the hook already unwound its own per-VM state, - // so abort `init` here — `vm.transpiler` was never written, and - // bailing out before the CLI reads it turns the old segfault into a - // clean error + non-zero exit. - unsafe { (*vm).runtime_state = (hooks.init_runtime_state)(vm, &mut opts)? }; - } + // SAFETY: hook contract — `vm` is the unique live VM on this + // thread. Write through the raw `vm` ptr (not `vm_ref`) so no + // `&mut VirtualMachine` is held live across the hook call — the + // hook body itself dereferences `vm`. + // + // `?`: on `Err` (e.g. a deleted cwd → `getcwd` ENOENT out of + // `Transpiler::init`) the hook already unwound its own per-VM state, + // so abort `init` here — `vm.transpiler` was never written, and + // bailing out before the CLI reads it turns the old segfault into a + // clean error + non-zero exit. + unsafe { (*vm).runtime_state = bun_runtime_init_runtime_state(vm, &mut opts)? }; // JSGlobalObject creation. `ensure_waker()` must run before the FFI. // SAFETY: `vm` is the unique live VM on this thread; raw-ptr deref so @@ -2240,35 +2199,23 @@ impl VirtualMachine { self.event_loop_mut().wait_for_promise(promise); } - /// `eventLoop().autoTick()` — dispatched through the runtime hook + /// `eventLoop().autoTick()` — dispatched into `bun_runtime` /// (needs `Timer::All` for the poll timeout). #[inline] pub fn auto_tick(&mut self) { - if let Some(hooks) = runtime_hooks() { - // SAFETY: hook contract — `self` is the live per-thread VM. - unsafe { (hooks.auto_tick)(self) }; - } else { - // No high tier (unit tests) — fall back to a non-blocking tick. - self.event_loop_mut().tick(); - } + // SAFETY: hook contract — `self` is the live per-thread VM. + unsafe { bun_runtime_auto_tick(self) }; } /// `eventLoop().autoTickActive()` — like [`auto_tick`](Self::auto_tick) /// but only sleeps in the uSockets loop while it has active handles. - /// The real body lives in `event_loop.rs` - /// behind `` until the b2-cycle (`Timer::All`) breaks; until - /// then route through the same `auto_tick` hook so drain loops in - /// `on_before_exit` / `bun_main` still make forward progress. + /// The body lives in `bun_runtime::jsc_hooks` (it needs `Timer::All`), + /// so drain loops in `on_before_exit` / `bun_main` route through the + /// same upward dispatch as [`auto_tick`](Self::auto_tick). #[inline] pub fn auto_tick_active(&mut self) { - if let Some(hooks) = runtime_hooks() { - // SAFETY: `self` is the live per-thread VM (hook contract). - unsafe { (hooks.auto_tick_active)(self) }; - } else { - // No high-tier hook (unit tests) — drain JS tasks only so callers - // observe forward progress without blocking on the I/O loop. - self.event_loop_mut().tick(); - } + // SAFETY: `self` is the live per-thread VM (hook contract). + unsafe { bun_runtime_auto_tick_active(self) }; } /// `reloadEntryPoint(entry_path)` — set `main`, generate the synthetic @@ -2284,7 +2231,6 @@ impl VirtualMachine { self.main_hash = bun_watcher::Watcher::get_hash(entry_path); self.overridden_main.deinit(); - let hooks = runtime_hooks(); let _ = self.ensure_debugger(true); // Node.js `--trace-*` and `--stack-trace-limit` flags need @@ -2324,26 +2270,22 @@ impl VirtualMachine { } if !self.main_is_html_entrypoint { - if let Some(hooks) = hooks { - let watch = self.is_watcher_enabled(); - if !(hooks.generate_entry_point)(self, watch, entry_path) { - return Err(bun_core::err!("ServerEntryPointGenerate")); - } + let watch = self.is_watcher_enabled(); + if !bun_runtime_generate_entry_point(self, watch, entry_path) { + return Err(bun_core::err!("ServerEntryPointGenerate")); } } if !self.transpiler.options.disable_transpilation { if !self.preload.is_empty() { - if let Some(hooks) = hooks { - // SAFETY: hook contract. - let p = unsafe { (hooks.load_preloads)(self) }?; - if !p.is_null() { - JSValue::from_cell(p).ensure_still_alive(); - JSValue::from_cell(p).protect(); - self.pending_internal_promise = Some(p); - self.pending_internal_promise_is_protected = true; - return Ok(p); - } + // SAFETY: hook contract. + let p = unsafe { bun_runtime_load_preloads(self) }?; + if !p.is_null() { + JSValue::from_cell(p).ensure_still_alive(); + JSValue::from_cell(p).protect(); + self.pending_internal_promise = Some(p); + self.pending_internal_promise_is_protected = true; + return Ok(p); } // Check if Module.runMain was patched. @@ -2784,7 +2726,7 @@ pub struct Options { // both VM and resolver can hold it without the cycle. pub graph: Option<&'static dyn bun_resolver::StandaloneModuleGraph>, // Note: debugger - // configuration is plumbed through `RuntimeHooks::ensure_debugger` (the + // configuration is plumbed through `bun_runtime_ensure_debugger` (the // CLI option struct lives in `bun_cli`, a forward dep). See // `runtime/jsc_hooks.rs` for the `configureDebugger` call site. pub is_main_thread: bool, @@ -2860,11 +2802,9 @@ impl IPCInstance { crate::ipc::DecodedIPCMessage::Internal(data) => { bun_core::scoped_log!(IPC, "Received IPC internal message from parent"); event_loop.enter(); - if let Some(hooks) = runtime_hooks() { - // SAFETY: hook fn is supplied by `bun_runtime` at startup; - // `global_this` is the live VM global. - unsafe { (hooks.handle_ipc_internal_child)(global_this, data) }; - } + // SAFETY: the body lives in `bun_runtime::jsc_hooks`; + // `global_this` is the live VM global. + unsafe { bun_runtime_handle_ipc_internal_child(global_this, data) }; event_loop.exit(); } } @@ -2876,9 +2816,7 @@ impl IPCInstance { // SAFETY: VM singleton is process-lifetime. let vm = VirtualMachine::get().as_mut(); let event_loop = vm.event_loop_mut(); - if let Some(hooks) = runtime_hooks() { - (hooks.ipc_child_singleton_deinit)(); - } + bun_runtime_ipc_child_singleton_deinit(); event_loop.enter(); Process__emitDisconnectEvent(vm.global()); event_loop.exit(); @@ -3531,11 +3469,9 @@ impl VirtualMachine { bun_core::Output::enable_buffering(); } - if let Some(hooks) = runtime_hooks() { - // The hook walks the VM's cron-job list and detaches each job from - // the old global so the new global can re-register them post-reload. - (hooks.cron_clear_all_reload)(self); - } + // The hook walks the VM's cron-job list and detaches each job from + // the old global so the new global can re-register them post-reload. + bun_runtime_cron_clear_all_reload(self); // `JSGlobalObject::reload` drains microtasks + collects async + clears // the JSC module loader registry. self.global().reload().expect("Failed to reload"); @@ -3557,18 +3493,17 @@ impl VirtualMachine { /// `NodeFS` lives in `bun_runtime` (forward-dep on `bun_jsc`), so the /// field is stored type-erased and the lazy boxed allocation goes through - /// [`RuntimeHooks::create_node_fs`]. Callers in `bun_runtime` cast the + /// [`bun_runtime_create_node_fs`]. Callers in `bun_runtime` cast the /// returned pointer back to `*mut node::fs::NodeFS`. #[inline] pub fn node_fs(&mut self) -> *mut c_void { if let Some(existing) = self.node_fs { return existing; } - let hooks = runtime_hooks().expect("runtime hooks not installed"); // SAFETY: hook contract — `self` is the live per-thread VM. The hook // boxes a `NodeFS{ vm: self if standalone else null }` and returns // the leaked pointer. - let new = unsafe { (hooks.create_node_fs)(self) }; + let new = unsafe { bun_runtime_create_node_fs(self) }; self.node_fs = Some(new); new } @@ -3692,7 +3627,8 @@ impl VirtualMachine { }; // Route through // [`init`] (which already wires console / event-loop / global / jsc_vm - // / RuntimeHooks) and then patch the worker-specific fields. + // / the per-VM runtime state) and then patch the worker-specific + // fields. let vm = Self::init(init_opts)?; // SAFETY: `vm` is the unique live VM on this thread. let vm_ref = unsafe { &mut *vm }; @@ -4078,11 +4014,8 @@ impl VirtualMachine { if let Some(blob_id) = specifier.strip_prefix(b"blob:".as_slice()) { ret.result = None; // `WebCore.ObjectURLRegistry` lives in `bun_runtime`; routed - // through [`RuntimeHooks::has_blob_url`]. - let has = runtime_hooks() - .map(|h| (h.has_blob_url)(blob_id)) - .unwrap_or(false); - if has { + // through [`bun_runtime_has_blob_url`]. + if bun_runtime_has_blob_url(blob_id) { ret.path = self.dupe_resolved_path(specifier); return Ok(()); } @@ -4445,9 +4378,7 @@ impl VirtualMachine { // after `take()` is a no-op and `RareData::drop`'s // `debug_assert!(cron_jobs.is_empty())` fires. if self.rare_data.is_some() { - if let Some(hooks) = runtime_hooks() { - (hooks.cron_clear_all_teardown)(self); - } + bun_runtime_cron_clear_all_teardown(self); } if let Some(rare) = self.rare_data.take() { // Paired with `rare_data()`'s register_root_region. Without this, @@ -4481,13 +4412,12 @@ impl VirtualMachine { // `timer`/`entry_point` live in the high-tier `RuntimeState` box, so // dispatch the reclaim through the hook. - if let Some(hooks) = runtime_hooks() { - let state = core::mem::replace(&mut self.runtime_state, core::ptr::null_mut()); - // SAFETY: hook contract — `state` is exactly the pointer - // `init_runtime_state` returned for this VM (or null), handed back - // once on the same thread; `self` is the live per-thread VM. - unsafe { (hooks.deinit_runtime_state)(std::ptr::from_mut(self), state) }; - } + let state = core::mem::replace(&mut self.runtime_state, core::ptr::null_mut()); + // SAFETY: hook contract — `state` is exactly the pointer + // `bun_runtime_init_runtime_state` returned for this VM (or null), + // handed back once on the same thread; `self` is the live per-thread + // VM. + unsafe { bun_runtime_deinit_runtime_state(std::ptr::from_mut(self), state) }; self.has_terminated = true; } /// Note: takes the concrete @@ -4548,16 +4478,14 @@ impl VirtualMachine { let _ = self.ensure_debugger(true); if !self.transpiler.options.disable_transpilation { - if let Some(hooks) = runtime_hooks() { - // SAFETY: hook contract. - let p = unsafe { (hooks.load_preloads)(self) }?; - if !p.is_null() { - JSValue::from_cell(p).ensure_still_alive(); - self.pending_internal_promise = Some(p); - JSValue::from_cell(p).protect(); - self.pending_internal_promise_is_protected = true; - return Ok(p); - } + // SAFETY: hook contract. + let p = unsafe { bun_runtime_load_preloads(self) }?; + if !p.is_null() { + JSValue::from_cell(p).ensure_still_alive(); + self.pending_internal_promise = Some(p); + JSValue::from_cell(p).protect(); + self.pending_internal_promise_is_protected = true; + return Ok(p); } } @@ -4730,11 +4658,9 @@ impl VirtualMachine { // — the scheduler's queue and in-flight work-pool task belong to the // outgoing file, and its brief spin-wait is bounded by at most one // in-flight `stat()`. - if let Some(hooks) = runtime_hooks() { - // SAFETY: live per-thread VM on the JS thread; `runtime_state` - // stays installed for the whole test run. - unsafe { (hooks.cancel_all_timers)(core::ptr::from_mut(self)) }; - } + // SAFETY: live per-thread VM on the JS thread; `runtime_state` + // stays installed for the whole test run. + unsafe { bun_runtime_cancel_all_timers(core::ptr::from_mut(self)) }; self.overridden_main.deinit(); self.entry_point_result.value.deinit(); @@ -6369,16 +6295,15 @@ impl VirtualMachine { // Standalone-module-graph fallback: the sourcemap load reaches into // `bun_standalone_graph::{Graph,File,LazySourceMap}` (higher tier); - // dispatch through [`RuntimeHooks::load_standalone_sourcemap`] per + // dispatch through [`bun_runtime_load_standalone_sourcemap`] per // §Dispatch (cold path — one-time decode then cached below). // Gate only — the hook reaches the concrete graph via its own // `UnsafeCell` singleton (write-provenance), not via this read-only // trait object. let _ = self.standalone_module_graph?; - let hooks = runtime_hooks()?; // JS-thread call; hook mutates only per-`File` lazy caches under the // standalone graph's internal `INIT_LOCK`. - let map = (hooks.load_standalone_sourcemap)(path)?; + let map = bun_runtime_load_standalone_sourcemap(path)?; // The `Arc::clone` is the ref-bump; `into_raw` transfers that strong // ref into the table (reclaimed by `put_value`'s replace path / diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 46763240a0e1..cc199f1da313 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -5,9 +5,9 @@ //! per-`Task` switch and `ImmediateObject::runImmediateTask`) name //! `bun_runtime` types and are hoisted to that tier via link-time //! `extern "Rust"` (`__bun_tick_queue_with_count` / `__bun_run_immediate_task`); -//! `auto_tick`/`auto_tick_active` likewise -//! dispatch through `virtual_machine::RuntimeHooks` (need `Timer::All` for the -//! poll deadline). See PORTING.md §Dispatch. +//! `auto_tick`/`auto_tick_active` likewise dispatch through the link-time +//! `virtual_machine::bun_runtime_auto_tick{,_active}` externs (need +//! `Timer::All` for the poll deadline). See PORTING.md §Dispatch. use core::ptr::NonNull; use core::sync::atomic::{AtomicI32, AtomicPtr, Ordering}; @@ -901,8 +901,9 @@ impl EventLoop { } /// `eventLoop().autoTick()` — bounces through `VirtualMachine::auto_tick`, - /// which dispatches to the `bun_runtime` hook (needs `Timer::All` for the - /// poll timeout). The body lives in `bun_runtime::jsc_hooks::auto_tick`. + /// which dispatches into `bun_runtime` (needs `Timer::All` for the poll + /// timeout). The body is `bun_runtime_auto_tick` in + /// `bun_runtime::jsc_hooks`. #[inline] pub fn auto_tick(&mut self) { self.vm_ref().as_mut().auto_tick(); @@ -911,7 +912,7 @@ impl EventLoop { /// `eventLoop().autoTickActive()` — like [`auto_tick`](Self::auto_tick) but /// only sleeps in the uSockets loop while it has active handles. /// Dispatches through - /// `VirtualMachine::auto_tick_active` → `RuntimeHooks::auto_tick_active` + /// `VirtualMachine::auto_tick_active` → `bun_runtime_auto_tick_active` /// (body lives in `bun_runtime::jsc_hooks` — needs `Timer::All`). #[inline] pub fn auto_tick_active(&mut self) { diff --git a/src/jsc/web_worker.rs b/src/jsc/web_worker.rs index ceb045b8f5e6..97d5fe17a88f 100644 --- a/src/jsc/web_worker.rs +++ b/src/jsc/web_worker.rs @@ -68,7 +68,11 @@ use bun_core::{String as BunString, WTFStringImpl}; use bun_io::KeepAlive; use bun_threading::{Futex, Mutex}; -use crate::virtual_machine::{self, VirtualMachine, runtime_hooks}; +use crate::virtual_machine::{ + self, VirtualMachine, bun_runtime_apply_standalone_runtime_flags, + bun_runtime_cancel_all_timers, bun_runtime_cron_clear_all_teardown, bun_runtime_has_blob_url, + bun_runtime_parse_worker_exec_argv_allow_addons, +}; use crate::{self as jsc, JSGlobalObject, JSValue, JsError, LogJsc}; bun_core::define_scoped_log!(log, Worker, hidden); @@ -827,8 +831,6 @@ impl WebWorker { debug_assert!(self.status.get() == Status::Start); debug_assert!(self.vm_ptr().is_null()); - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); - // `parent` is a `BackRef` and outlives this worker while // `parent_poll_ref` is held (see file header). The parent VM runs // concurrently on its own thread, so we must NOT materialise a @@ -846,7 +848,7 @@ impl WebWorker { // Parse `execArgv` with the // RunCommand param table. The param table lives in // `bun_runtime::cli` (forward-dep), so dispatch through - // `RuntimeHooks::parse_worker_exec_argv_allow_addons`. Currently + // `bun_runtime_parse_worker_exec_argv_allow_addons`. Currently // only honours `--no-addons`; the hook owns the temporary UTF-8 // alloc + clap parse + `args.deinit()`. `None` on parse failure // (the parent's setting is kept). @@ -854,7 +856,7 @@ impl WebWorker { // SAFETY: `exec_argv` borrows C++ `WorkerOptions` kept alive by the // owning `WebCore::Worker` for `self`'s lifetime; the hook only // reads the slice and owns its own temporary allocations. - let parsed = unsafe { (hooks.parse_worker_exec_argv_allow_addons)(exec_argv) }; + let parsed = unsafe { bun_runtime_parse_worker_exec_argv_allow_addons(exec_argv) }; if let Some(allow_addons) = parsed { // override the existing even if it was set transform_options.allow_addons = Some(allow_addons); @@ -971,7 +973,7 @@ impl WebWorker { b.resolver.env_loader = NonNull::new(b.env); if let Some(graph) = parent.standalone_module_graph { - (hooks.apply_standalone_runtime_flags)(b, graph); + bun_runtime_apply_standalone_runtime_flags(b, graph); } } @@ -1226,17 +1228,15 @@ impl WebWorker { vm.jsc_vm().clear_has_termination_request(); vm.is_shutting_down = true; vm.on_exit(); - if let Some(hooks) = runtime_hooks() { - (hooks.cron_clear_all_teardown)(vm); - // Drain `TimeoutObject`s from this worker's timer heap before - // `close_all_socket_groups` / `WebWorker__teardownJSCVM` so - // their heap nodes are unlinked while `runtime_state` and the - // JSC heap are both still alive. - // SAFETY: `vm_ptr` was unpublished under `vm_lock` above, so - // this thread is the sole owner; `runtime_state` for this - // worker thread is still installed (torn down in `destroy()`). - unsafe { (hooks.cancel_all_timers)(vm_ptr) }; - } + bun_runtime_cron_clear_all_teardown(vm); + // Drain `TimeoutObject`s from this worker's timer heap before + // `close_all_socket_groups` / `WebWorker__teardownJSCVM` so + // their heap nodes are unlinked while `runtime_state` and the + // JSC heap are both still alive. + // SAFETY: `vm_ptr` was unpublished under `vm_lock` above, so + // this thread is the sole owner; `runtime_state` for this + // worker thread is still installed (torn down in `destroy()`). + unsafe { bun_runtime_cancel_all_timers(vm_ptr) }; // Embedded socket groups must drain while JSC is still alive — // closeAll() fires on_close → JS callbacks. RareData.deinit() runs // after teardownJSCVM and only deinit()s (asserts empty in debug). @@ -1616,8 +1616,7 @@ unsafe fn resolve_entry_point_specifier<'s>( // this arm and report "Blob URL is missing". const BLOB_SPECIFIER_LEN: usize = b"blob:".len() + crate::uuid::UUID::STRING_LENGTH; if str.len() >= BLOB_SPECIFIER_LEN && str.starts_with(b"blob:") { - let hooks = runtime_hooks().expect("RuntimeHooks not installed"); - if (hooks.has_blob_url)(&str[b"blob:".len()..]) { + if bun_runtime_has_blob_url(&str[b"blob:".len()..]) { return Some(str); } else { *error_message = BunString::static_(b"Blob URL is missing"); diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 0c6c7618f556..0d3523620a2e 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -27,7 +27,8 @@ pub use ::bun_install_types::resolver_hooks::AutoInstaller as PackageManagerTrai // LAYERING: `PackageManager.initWithRuntime` lives in // `bun_install`, which depends on this crate. The lazy-init body is defined // `#[no_mangle]` in `bun_install::auto_installer` and resolved at link time -// (same pattern as `__bun_regex_*` / `__BUN_RUNTIME_HOOKS`). `install` is the +// (same pattern as `__bun_regex_*` / the `bun_runtime_*` VM hooks). `install` +// is the // `?*Api.BunInstall` (`self.opts.install`); `env` is the `*DotEnv.Loader` // (lifetime-erased to `'static` — the install crate stores it as a raw // `NonNull>`). diff --git a/src/runtime/api/bun/SecureContext.rs b/src/runtime/api/bun/SecureContext.rs index ab44fd62145d..719db3554f4d 100644 --- a/src/runtime/api/bun/SecureContext.rs +++ b/src/runtime/api/bun/SecureContext.rs @@ -295,8 +295,9 @@ impl SecureContext { let state = crate::jsc_hooks::runtime_state(); debug_assert!(!state.is_null(), "RuntimeState not installed"); // SAFETY: `state` is the boxed per-thread `RuntimeState` installed by - // `init_runtime_state`; the embedded `ssl_ctx_cache` has a stable - // address for the VM's lifetime and is only touched from the JS thread. + // `bun_runtime_init_runtime_state`; the embedded `ssl_ctx_cache` has a + // stable address for the VM's lifetime and is only touched from the JS + // thread. let cache = unsafe { &mut (*state).ssl_ctx_cache }; let Some(ctx) = cache.get_or_create_digest(ctx_opts, d, &mut err) else { // `err` is only set for the input-validation paths (bad PEM, missing diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.rs b/src/runtime/api/bun/js_bun_spawn_bindings.rs index 1a2c3fc9cb33..1b5dc11fe8d3 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.rs +++ b/src/runtime/api/bun/js_bun_spawn_bindings.rs @@ -1604,9 +1604,9 @@ pub(crate) fn spawn_maybe_sync( nsec: ts.nsec, }; }); - // `Timer::All` lives in `bun_runtime`; reach it via the - // `RuntimeHooks` dispatch (`VirtualMachineRef::timer_insert`) which - // forwards to `crate::timer::All::insert`. + // `Timer::All` lives in `bun_runtime`; reach it via + // `VirtualMachineRef::timer_insert` (the `bun_runtime_timer_insert` + // dispatch), which forwards to `crate::timer::All::insert`. // SAFETY: `jsc_vm_ptr` is the live per-thread VM; the timer node is // owned by the boxed `Subprocess` and stays at a stable address // until `Subprocess::finalize` removes it from the heap. diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 49b21f9613dc..1e8884e2c882 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -943,9 +943,8 @@ Full documentation is available at https://bun.com/docs/cli/run // `bun_jsc::initialize` // is real (calls `JSCInitialize` over `bun_sys::environ()`); the - // dispatch hooks (`jsc_hooks::install_jsc_hooks`) are installed by - // `main.rs` before `Cli::start`, so `VirtualMachine::init` already sees - // a populated `RuntimeHooks` table. + // `bun_runtime_*` dispatch bodies `VirtualMachine::init` calls are + // link-time resolved (`jsc_hooks`), so nothing is installed at runtime. bun_jsc::initialize(ctx.runtime_options.eval.eval_and_print); bun_ast::initialize_store(); diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index bc593dc53905..e00e2066d607 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -67,7 +67,8 @@ type Sockaddr = netc::sockaddr; /// Helper: fetch the per-VM global DNS resolver (port of /// `RareData::globalDNSResolver`). The storage is /// [`crate::jsc_hooks::RuntimeState::global_dns_data`] — concrete -/// `Option>`, freed by `deinit_runtime_state` on VM teardown. +/// `Option>`, freed by `bun_runtime_deinit_runtime_state` on +/// VM teardown. /// /// R-2: returns `&Resolver` (shared). All Resolver mutation routes through /// `Cell` / `JsCell` fields, so a shared borrow is sufficient and avoids the @@ -2130,7 +2131,7 @@ impl Drop for GlobalData { fn drop(&mut self) { // `Resolver::deinit` ends with `heap::take(this)`, which is wrong for a // value field — open-code the channel teardown so the c-ares state - // frees when this box drops in `deinit_runtime_state`. + // frees when this box drops in `bun_runtime_deinit_runtime_state`. if let Some(channel) = self.resolver.channel.take() { // SAFETY: `channel` is the live handle from `ares_init_options`, owned by this resolver. unsafe { c_ares::Channel::destroy(channel) }; diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 7caf3e55e844..821ec7be10f0 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -1,20 +1,20 @@ //! `crate::jsc_hooks` — high-tier implementations for the §Dispatch -//! cold-path vtables that `bun_jsc` exposes (`virtual_machine::RuntimeHooks` -//! and `module_loader::LoaderHooks`). +//! cold paths that `bun_jsc` exposes (the `bun_runtime_*` externs declared in +//! `virtual_machine` and the `module_loader::LoaderHooks` table). //! //! Per `docs/PORTING.md` §Dispatch (cold path), `bun_jsc::VirtualMachine::init` //! / `ModuleLoader::*` cannot name `bun_runtime` types (`timer::All`, //! `bundler::entry_points::ServerEntryPoint`, `bundler::Transpiler`, //! `HardcodedModule`, …) directly without inverting the crate DAG. Instead the -//! low tier defines a manual fn-pointer table; this module owns the static -//! instances and the bodies as `#[no_mangle]` link-time-resolved symbols -//! (declared `extern "Rust"` on the low-tier side). +//! low tier declares each body `extern "Rust"`; this module owns the bodies as +//! `#[no_mangle]` link-time-resolved symbols. //! //! Layout: //! 1. [`RuntimeState`] — per-VM state the low tier stores as `*mut c_void` //! (owns `timer::All` + the synthetic `bun:main` `ServerEntryPoint`). -//! 2. `__BUN_RUNTIME_HOOKS` — `init_runtime_state` / `generate_entry_point` -//! / `load_preloads` / `ensure_debugger` / `auto_tick`. +//! 2. `bun_runtime_*` — `bun_runtime_init_runtime_state` / +//! `bun_runtime_generate_entry_point` / `bun_runtime_load_preloads` / +//! `bun_runtime_ensure_debugger` / `bun_runtime_auto_tick` / …. //! 3. `__BUN_LOADER_HOOKS` — `transpile_source_code` / //! `fetch_builtin_module` / `transpile_file`. //! 4. `__bun_get_vm_ctx` / `__bun_js_vm_get` / `__bun_stdio_blob_store_new` / @@ -31,9 +31,7 @@ use bun_jsc::module_loader::{ ArenaResetGuard, FetchBuiltinResult, FetchFlags, LoaderHooks, TranspileArgs, TranspileExtra, }; use bun_jsc::resolved_source::OwnedResolvedSource; -use bun_jsc::virtual_machine::{ - InitOptions, RuntimeHooks, RuntimeState as OpaqueRuntimeState, VirtualMachine, -}; +use bun_jsc::virtual_machine::{InitOptions, RuntimeState as OpaqueRuntimeState, VirtualMachine}; use bun_jsc::{ AnyPromise, ErrorableResolvedSource, ErrorableString, JSGlobalObject, JSInternalPromise, JSModuleLoader, JSValue, JsResult, ResolvedSource, @@ -80,7 +78,7 @@ pub struct RuntimeState { pub editor_context: crate::cli::open::EditorContext, /// `RareData.global_dns_data` — per-VM resolver + c-ares channel. /// Lazy-init by [`crate::dns_jsc::global_resolver`]; freed when this box - /// drops in [`deinit_runtime_state`]. + /// drops in [`bun_runtime_deinit_runtime_state`]. pub global_dns_data: core::cell::OnceCell>, /// Synthetic `bun:main` wrapper source. pub entry_point: ServerEntryPoint, @@ -109,8 +107,8 @@ pub type IsolationHandles = bun_collections::ArrayHashMap; thread_local! { /// One `RuntimeState` per JS thread (`VirtualMachine` is per-thread). - /// Cleared by [`deinit_runtime_state`] (dispatched from - /// `VirtualMachine::destroy` via `RuntimeHooks`). + /// Cleared by [`bun_runtime_deinit_runtime_state`] (dispatched from + /// `VirtualMachine::destroy`). static RUNTIME_STATE: Cell<*mut RuntimeState> = const { Cell::new(ptr::null_mut()) }; } @@ -132,8 +130,8 @@ pub(crate) fn runtime_state() -> *mut RuntimeState { /// /// Note: `bun_jsc::VirtualMachine.timer` is a `()` placeholder; /// the real `All` lives in [`RuntimeState::timer`] until that slot widens. -/// Null only before [`init_runtime_state`] has run (e.g. `bun_jsc` unit tests -/// with no high tier, or `Bun__Timer__getNextID` racing init). +/// Null only before [`bun_runtime_init_runtime_state`] has run (e.g. `bun_jsc` +/// unit tests with no high tier, or `Bun__Timer__getNextID` racing init). /// /// Returns `*mut` (NOT `&mut`) so callers that are themselves fields of `All` /// (`DateHeaderTimer`, `EventLoopDelayMonitor`, `FakeTimers`) can dereference @@ -222,7 +220,10 @@ pub(crate) unsafe fn runtime_state_of(vm: *mut VirtualMachine) -> *mut RuntimeSt /// /// # Safety /// `vm` must be the live per-thread VM; called only from the JS thread. -pub(crate) unsafe fn default_client_ssl_ctx(vm: *mut VirtualMachine) -> *mut bun_uws::SslCtx { +#[unsafe(no_mangle)] +pub(crate) unsafe fn bun_runtime_default_client_ssl_ctx( + vm: *mut VirtualMachine, +) -> *mut bun_uws::SslCtx { // SAFETY: per fn contract; `rare_data()` lazy-inits the box. let rare = unsafe { (*vm).rare_data() }; if rare.default_client_ssl_ctx.is_none() { @@ -254,13 +255,14 @@ pub(crate) unsafe fn default_client_ssl_ctx(vm: *mut VirtualMachine) -> *mut bun rare.default_client_ssl_ctx.unwrap() } -/// `RareData.sslCtxCache().getOrCreateOpts(opts, &err)` — RuntimeHooks slot +/// `RareData.sslCtxCache().getOrCreateOpts(opts, &err)` — link-time extern /// body. Per-VM digest-keyed weak `SSL_CTX*` cache; returns +1 ref or `None` /// on BoringSSL rejection (`err` populated). /// /// # Safety /// `vm` must be the live per-thread VM; called only from the JS thread. -unsafe fn ssl_ctx_cache_get_or_create( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_ssl_ctx_cache_get_or_create( _vm: *mut VirtualMachine, opts: &bun_uws::SocketContext::BunSocketContextOptions, err: &mut bun_uws::create_bun_socket_error_t, @@ -277,7 +279,7 @@ unsafe fn ssl_ctx_cache_get_or_create( } // ════════════════════════════════════════════════════════════════════════════ -// RuntimeHooks bodies +// `bun_runtime_*` extern bodies // ════════════════════════════════════════════════════════════════════════════ /// Timer state + body hive-allocator + debugger configuration — everything @@ -293,7 +295,8 @@ unsafe fn ssl_ctx_cache_get_or_create( /// # Safety /// `vm` is the freshly-boxed unique VM on this thread, with `vm.global` / /// `vm.jsc_vm` already populated by `bun_jsc::VirtualMachine::init`. -unsafe fn init_runtime_state( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_init_runtime_state( vm: *mut VirtualMachine, opts: &mut InitOptions, ) -> Result { @@ -307,8 +310,8 @@ unsafe fn init_runtime_state( // this hook), so no uws wiring is repeated here. // Note: `heap::alloc` is paired with `heap::take` in - // [`deinit_runtime_state`] below — called from `VirtualMachine::deinit` / - // worker `destroy()` via the `RuntimeHooks::deinit_runtime_state` slot. + // [`bun_runtime_deinit_runtime_state`] below — called from + // `VirtualMachine::deinit` / worker `destroy()`. // PORTING.md §Forbidden permits // `into_raw`-without-reclaim only for true process-lifetime singletons via // `OnceLock`, which this is not (per-VM / per-Worker-thread). @@ -471,8 +474,8 @@ unsafe fn init_runtime_state( /// /// # Safety /// `vm` is the freshly-boxed unique VM on this thread; `vm.transpiler` has -/// been written by [`init_runtime_state`] (the post-`isInspectorEnabled` tail -/// touches `transpiler.options`). +/// been written by [`bun_runtime_init_runtime_state`] (the +/// post-`isInspectorEnabled` tail touches `transpiler.options`). unsafe fn configure_debugger( vm: *mut VirtualMachine, cli_flag: &bun_options_types::context::Debugger, @@ -563,15 +566,18 @@ unsafe fn configure_debugger( } } -/// Reclaim the per-VM [`RuntimeState`] boxed in [`init_runtime_state`]. Called -/// from `VirtualMachine::deinit` / worker `destroy()` with the opaque pointer -/// returned by `init_runtime_state`. Clears the thread-local and drops the +/// Reclaim the per-VM [`RuntimeState`] boxed in +/// [`bun_runtime_init_runtime_state`]. Called from `VirtualMachine::deinit` / +/// worker `destroy()` with the opaque pointer returned by +/// `bun_runtime_init_runtime_state`. Clears the thread-local and drops the /// `Box`, freeing `timer` + `entry_point`. /// /// # Safety -/// `state` must be the exact pointer returned by [`init_runtime_state`] for -/// this thread (or null), and must not be used again after this call. -unsafe fn deinit_runtime_state(_vm: *mut VirtualMachine, state: OpaqueRuntimeState) { +/// `state` must be the exact pointer returned by +/// [`bun_runtime_init_runtime_state`] for this thread (or null), and must not +/// be used again after this call. +#[unsafe(no_mangle)] +unsafe fn bun_runtime_deinit_runtime_state(_vm: *mut VirtualMachine, state: OpaqueRuntimeState) { RUNTIME_STATE.with(|c| c.set(ptr::null_mut())); // Free the per-thread `TRANSPILE_PRINTER`. Workers lazy-init their own // copy in `transpile_file` / `transpile_virtual_module`; without this @@ -605,7 +611,8 @@ unsafe fn deinit_runtime_state(_vm: *mut VirtualMachine, state: OpaqueRuntimeSta /// `ServerEntryPoint.generate(watch, entry_path)` — produces the synthetic /// `bun:main` wrapper. Returns `false` on error (the error is already logged /// into `vm.log` by `generate`). -fn generate_entry_point(_vm: &VirtualMachine, watch: bool, entry_path: &[u8]) -> bool { +#[unsafe(no_mangle)] +fn bun_runtime_generate_entry_point(_vm: &VirtualMachine, watch: bool, entry_path: &[u8]) -> bool { let state = runtime_state(); if state.is_null() { return false; @@ -624,7 +631,8 @@ fn generate_entry_point(_vm: &VirtualMachine, watch: bool, entry_path: &[u8]) -> /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn load_preloads( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_load_preloads( vm: *mut VirtualMachine, ) -> Result<*mut JSInternalPromise, bun_core::Error> { // Note: reshaped for borrowck — `wait_for_promise` / `event_loop().tick()` @@ -814,7 +822,8 @@ unsafe fn load_preloads( /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) { // Note: `Debugger::create` / `wait_for_debugger_if_necessary` live in // `bun_jsc::debugger::Debugger` (Debugger.rs); the heavy bodies (futex // spin, debugger-thread spawn, deadline poll-loop) are there. This hook @@ -851,7 +860,8 @@ unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn auto_tick(vm: *mut VirtualMachine) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_auto_tick(vm: *mut VirtualMachine) { // Note: reshaped for borrowck — `EventLoop` is a value field of // `VirtualMachine`, so holding `&mut EventLoop` while also touching VM // siblings would alias. Dereference per-field via the raw `vm` ptr. @@ -996,14 +1006,16 @@ unsafe fn auto_tick(vm: *mut VirtualMachine) { } /// `eventLoop().autoTickActive()`. Same shape as -/// [`auto_tick`] but: no `runImminentGCTimer`, no `handleRejectedPromises` at +/// [`bun_runtime_auto_tick`] but: no `runImminentGCTimer`, no +/// `handleRejectedPromises` at /// the tail, and no debug sleep-timer logging. Used by `bun_main` / /// `on_before_exit` drain loops where blocking when the loop is idle would /// hang shutdown. /// /// # Safety /// `vm` is the live per-thread VM. -unsafe fn auto_tick_active(vm: *mut VirtualMachine) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_auto_tick_active(vm: *mut VirtualMachine) { // Note: reshaped for borrowck — see `auto_tick` above. // SAFETY: per fn contract — `vm` is the live per-thread VM. let el: *mut bun_jsc::event_loop::EventLoop = unsafe { &*vm }.event_loop; @@ -1105,7 +1117,8 @@ unsafe fn auto_tick_active(vm: *mut VirtualMachine) { /// `printException` / `printErrorlikeObject` — formats `value` to stderr via /// `ConsoleObject::Formatter`. Dispatched here so the high tier owns the /// formatter. -fn print_exception( +#[unsafe(no_mangle)] +fn bun_runtime_print_exception( vm_ref: &mut VirtualMachine, value: JSValue, exception_list: Option<&mut bun_jsc::virtual_machine::ExceptionList>, @@ -1150,7 +1163,8 @@ fn print_exception( /// # Safety /// `vm` is a live `VirtualMachine`; `t` points at a live unlinked /// `EventLoopTimer`. -unsafe fn timer_insert( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_timer_insert( vm: *mut VirtualMachine, t: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, ) { @@ -1163,11 +1177,12 @@ unsafe fn timer_insert( unsafe { &mut (*state).timer }.insert(t); } -/// `vm.timer.remove(timer)` — counterpart to [`timer_insert`]. +/// `vm.timer.remove(timer)` — counterpart to [`bun_runtime_timer_insert`]. /// /// # Safety /// `t` points at a live `EventLoopTimer` currently linked into the heap. -unsafe fn timer_remove( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_timer_remove( vm: *mut VirtualMachine, t: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, ) { @@ -1184,7 +1199,8 @@ unsafe fn timer_remove( /// # Safety /// `vm` is the live per-thread VM. The returned box is reclaimed (if at all) /// only by VM teardown. -unsafe fn create_node_fs(vm: *mut VirtualMachine) -> *mut c_void { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_create_node_fs(vm: *mut VirtualMachine) -> *mut c_void { use crate::node::fs::NodeFS; // `.vm` is set only when standalone-module-graph is active // (it gates the embedded-file `Bun.file()` lookups inside `node:fs`). @@ -1202,7 +1218,8 @@ unsafe fn create_node_fs(vm: *mut VirtualMachine) -> *mut c_void { } /// `WebCore.ObjectURLRegistry.singleton().has(specifier["blob:".len..])`. -fn has_blob_url(blob_id: &[u8]) -> bool { +#[unsafe(no_mangle)] +fn bun_runtime_has_blob_url(blob_id: &[u8]) -> bool { crate::webcore::object_url_registry::ObjectURLRegistry::singleton().has(blob_id) } @@ -1212,7 +1229,8 @@ fn has_blob_url(blob_id: &[u8]) -> bool { /// in this crate, above `bun_jsc` / `bun_js_parser_jsc`) and returns its body /// Blob wrapped in a resolved Promise; `Ok(None)` to fall through to the /// `Blob`/`BuildMessage`/`ResolveMessage` arms in `Macro::Run::coerce`. -fn body_mixin_get_blob( +#[unsafe(no_mangle)] +fn bun_runtime_body_mixin_get_blob( value: JSValue, global: &JSGlobalObject, ) -> bun_jsc::JsResult> { @@ -1238,7 +1256,8 @@ fn body_mixin_get_blob( /// /// # Safety /// `global` is the live VM global. -unsafe fn process_exit(global: *mut JSGlobalObject, code: u8) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_process_exit(global: *mut JSGlobalObject, code: u8) { // SAFETY: per fn contract — `global` is the live VM global. The deref is // performed once here in the hook shim so the user-facing `process::exit` // can take a safe `&JSGlobalObject`. @@ -1260,7 +1279,8 @@ unsafe fn process_exit(global: *mut JSGlobalObject, code: u8) { /// /// Called on the JS thread; `Graph::find` / `LazySourceMap::load` only mutate /// the per-`File` lazy caches (sourcemap decode is serialized by `INIT_LOCK`). -fn load_standalone_sourcemap( +#[unsafe(no_mangle)] +fn bun_runtime_load_standalone_sourcemap( path: &[u8], ) -> Option> { let graph = bun_standalone_graph::Graph::get()?; @@ -1277,7 +1297,8 @@ fn load_standalone_sourcemap( /// # Safety /// `global` is the live VM global; called on the JS thread inside an /// `event_loop.enter()` scope. -unsafe fn handle_ipc_internal_child(global: *mut JSGlobalObject, data: JSValue) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_handle_ipc_internal_child(global: *mut JSGlobalObject, data: JSValue) { // SAFETY: per fn contract. let global = unsafe { &*global }; // Spec discards a JS exception here (`catch |err| switch (err) { @@ -1291,7 +1312,8 @@ unsafe fn handle_ipc_internal_child(global: *mut JSGlobalObject, data: JSValue) /// `IPCInstance.handleIPCClose`. /// /// Called on the JS thread (the `CHILD_SINGLETON` static is JS-thread-only). -fn ipc_child_singleton_deinit() { +#[unsafe(no_mangle)] +fn bun_runtime_ipc_child_singleton_deinit() { // `InternalMsgHolder`'s owned fields (`Strong`s, map, `Vec`) all impl // `Drop`; taking the `Option` runs them. // SAFETY: JS-thread-only mutable static (see `child_singleton()` doc). @@ -1383,39 +1405,6 @@ mod vm_loader_ctx { } } -/// The static `RuntimeHooks` instance handed to `bun_jsc`. -#[unsafe(no_mangle)] -pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks { - init_runtime_state, - deinit_runtime_state, - generate_entry_point, - load_preloads, - ensure_debugger, - auto_tick, - auto_tick_active, - print_exception, - timer_insert, - timer_remove, - default_client_ssl_ctx, - ssl_ctx_cache_get_or_create, - create_node_fs, - has_blob_url, - body_mixin_get_blob, - process_exit, - handle_ipc_internal_child, - ipc_child_singleton_deinit, - console_on_before_print, - console_print_runtime_object, - load_standalone_sourcemap, - apply_standalone_runtime_flags, - parse_worker_exec_argv_allow_addons, - cron_clear_all_teardown, - cron_clear_all_reload, - terminate_all_workers_and_wait, - retroactively_report_discovered_tests, - cancel_all_timers, -}; - // ════════════════════════════════════════════════════════════════════════════ // WebWorker / Debugger runtime hooks // ════════════════════════════════════════════════════════════════════════════ @@ -1427,7 +1416,8 @@ pub(crate) static __BUN_RUNTIME_HOOKS: RuntimeHooks = RuntimeHooks { /// any other thread); `graph` is the process-lifetime trait object whose data /// pointer is a `bun_standalone_graph::Graph` (the only implementor — set in /// `init_with_module_graph` / inherited from the parent VM). -unsafe fn apply_standalone_runtime_flags( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_apply_standalone_runtime_flags( transpiler: *mut bun_bundler::Transpiler<'static>, graph: &'static dyn bun_resolver::StandaloneModuleGraph, ) { @@ -1461,7 +1451,8 @@ unsafe fn apply_standalone_runtime_flags( /// # Safety /// Each `WTFStringImpl` in `exec_argv` is a live WTF string (the C++ /// `Worker::create` array, kept alive for the worker's lifetime). -unsafe fn parse_worker_exec_argv_allow_addons( +#[unsafe(no_mangle)] +unsafe fn bun_runtime_parse_worker_exec_argv_allow_addons( exec_argv: &[bun_core::WTFStringImpl], ) -> Option { let mut no_addons = false; @@ -1491,32 +1482,35 @@ unsafe fn parse_worker_exec_argv_allow_addons( /// stops every in-process `Bun.cron()` job registered on /// this VM and releases the pending-promise ref so the struct frees (the event /// loop is dying; settle callbacks will never run). -fn cron_clear_all_teardown(vm: &mut VirtualMachine) { +#[unsafe(no_mangle)] +fn bun_runtime_cron_clear_all_teardown(vm: &mut VirtualMachine) { use crate::api::cron::{ClearMode, CronJob}; CronJob::clear_all_for_vm::<{ ClearMode::Teardown }>(vm); } /// `jsc.API.cron.CronJob.clearAllForVM(vm, .reload)` — -/// same impl as [`cron_clear_all_teardown`] but skips +/// same impl as [`bun_runtime_cron_clear_all_teardown`] but skips /// the pending-promise force-release (the event loop survives a hot reload, so /// settle callbacks will still run). -fn cron_clear_all_reload(vm: &mut VirtualMachine) { +#[unsafe(no_mangle)] +fn bun_runtime_cron_clear_all_reload(vm: &mut VirtualMachine) { use crate::api::cron::{ClearMode, CronJob}; CronJob::clear_all_for_vm::<{ ClearMode::Reload }>(vm); } /// `webcore.WebWorker.terminateAllAndWait(timeout_ms)` — /// forwards to the in-crate `bun_jsc::web_worker` -/// implementation; routed through `RuntimeHooks` because `virtual_machine.rs` -/// sits below `web_worker.rs` in the module DAG and the wait re-enters -/// `auto_tick` (this crate) on the worker side. +/// implementation; routed through the link-time extern because +/// `virtual_machine.rs` sits below `web_worker.rs` in the module DAG and the +/// wait re-enters `auto_tick` (this crate) on the worker side. /// /// Main-thread only; called from `global_exit` after `is_shutting_down` is set. -fn terminate_all_workers_and_wait(timeout_ms: u64) { +#[unsafe(no_mangle)] +fn bun_runtime_terminate_all_workers_and_wait(timeout_ms: u64) { bun_jsc::web_worker::terminate_all_and_wait(timeout_ms); } -/// `RuntimeHooks::cancel_all_timers` — cancel every `TimeoutObject` / +/// `bun_runtime_cancel_all_timers` — cancel every `TimeoutObject` / /// `ImmediateObject` still linked in the current thread's timer heap so the /// in-heap `+1` ref and the JS pin drop before the GC sweep / `~VM`. /// `timer::All` lives in `bun_runtime`; callers (`global_exit`, @@ -1525,7 +1519,8 @@ fn terminate_all_workers_and_wait(timeout_ms: u64) { /// # Safety /// `vm` is the live per-thread VM; `runtime_state()` must still be installed. /// Must run on the JS thread before JSC teardown. -unsafe fn cancel_all_timers(vm: *mut VirtualMachine) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_cancel_all_timers(vm: *mut VirtualMachine) { let state = runtime_state(); if state.is_null() { return; @@ -1584,7 +1579,10 @@ pub(crate) fn close_isolation_handles(vm: &mut VirtualMachine) { /// `agent` is a live C++ `Inspector::TestReporterAgent::Handle*` (just stored /// into `debugger.test_reporter_agent.handle` by the caller). Called on the JS /// thread. -unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::TestReporterHandle) { +#[unsafe(no_mangle)] +unsafe fn bun_runtime_retroactively_report_discovered_tests( + agent: *mut bun_jsc::debugger::TestReporterHandle, +) { use crate::test_runner::bun_test::{DescribeScope, Phase, TestScheduleEntry}; use crate::test_runner::jest::Jest; use bun_jsc::debugger::{TestReporterHandle, TestType}; @@ -1694,7 +1692,8 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T /// `Jest.runner.?.bun_test_root.onBeforePrint()` — flush the test reporter's /// line state before user `console.log` output interleaves with it. -fn console_on_before_print() { +#[unsafe(no_mangle)] +fn bun_runtime_console_on_before_print() { if let Some(runner) = crate::test_runner::jest::Jest::runner() { runner.bun_test_root.on_before_print(); } @@ -1702,14 +1701,15 @@ fn console_on_before_print() { use bun_io::AsFmt; -/// `ConsoleObject.Formatter.printAs(.Private, …)` runtime-type chain — see -/// [`RuntimeHooks::console_print_runtime_object`]. Returns `true` when `value` -/// matched one of the high-tier types and was fully formatted. -fn console_print_runtime_object<'a, 'f>( - formatter: &'a mut bun_jsc::Formatter<'f>, - writer: &'a mut dyn bun_io::Write, +/// `ConsoleObject.Formatter.printAs(.Private, …)` runtime-type chain — the +/// `bun_runtime_console_print_runtime_object` extern body. Returns `true` +/// when `value` matched one of the high-tier types and was fully formatted. +#[unsafe(no_mangle)] +fn bun_runtime_console_print_runtime_object( + formatter: &mut bun_jsc::Formatter<'_>, + writer: &mut dyn bun_io::Write, value: JSValue, - name_buf: &'a [u8; 512], + name_buf: &[u8; 512], enable_ansi_colors: bool, ) -> JsResult { if enable_ansi_colors { diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index 7f19e2ccdf49..8c1319b54745 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -55,8 +55,9 @@ fn with_ssl_ctx_cache( "runtime_state() before init_runtime_state" ); // SAFETY: `state` is the per-thread `RuntimeState` boxed in - // `init_runtime_state`, address-stable until VM teardown, and only the JS - // thread reaches here — so this `&mut` is unique for `f`'s duration. + // `bun_runtime_init_runtime_state`, address-stable until VM teardown, and + // only the JS thread reaches here — so this `&mut` is unique for `f`'s + // duration. f(unsafe { &mut (*state).ssl_ctx_cache }) } diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index 87aeb928f003..5e26a6302ba0 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -3278,8 +3278,9 @@ impl NewSocket { let cache = { let state = crate::jsc_hooks::runtime_state(); debug_assert!(!state.is_null(), "RuntimeState not installed"); - // SAFETY: per-thread `RuntimeState` boxed by `init_runtime_state`; - // stable address for the VM's lifetime, JS-thread-only access. + // SAFETY: per-thread `RuntimeState` boxed by + // `bun_runtime_init_runtime_state`; stable address for the + // VM's lifetime, JS-thread-only access. unsafe { &mut (*state).ssl_ctx_cache } }; owned_ctx = match cache.get_or_create(cfg, &mut create_err) { diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index d7a5c1093583..3a0e5be689f9 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -2812,7 +2812,7 @@ impl bun_jsc::ConsoleFormatter for Formatter<'_> { /// implemented for both [`Formatter`] (this module) and /// [`bun_jsc::console_object::Formatter`] so the same body serves the test /// runner *and* `console.log`'s `.Private` arm (via the -/// `RuntimeHooks::console_print_runtime_object` hook). +/// `bun_runtime_console_print_runtime_object` extern). pub trait AsymmetricMatcherFormatter { fn amf_add_for_new_line(&mut self, n: usize); fn amf_global_this(&self) -> &JSGlobalObject; diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index d86f406f3168..cdb4c7b81e7e 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -704,7 +704,7 @@ impl All { /// /// TODO: thread `vm: *mut VirtualMachine` through /// `insert`/`insert_lock_held`/`update` once - /// the `RuntimeHooks::timer_insert` slot widens — see jsc_hooks.rs. + /// the `bun_runtime_timer_insert` signature widens — see jsc_hooks.rs. #[cfg(windows)] fn ensure_uv_timer(&mut self) { // `vm` here means the OWNING VM (the one this timer is embedded in), diff --git a/src/runtime/valkey_jsc/js_valkey.rs b/src/runtime/valkey_jsc/js_valkey.rs index 2a5b66ae35e3..91de18a93dbb 100644 --- a/src/runtime/valkey_jsc/js_valkey.rs +++ b/src/runtime/valkey_jsc/js_valkey.rs @@ -1087,7 +1087,8 @@ impl JSValkeyClient { } }); // `vm.timer.insert(timer)` — `Timer::All` lives in `bun_runtime`; - // dispatched through `RuntimeHooks` (see VirtualMachine::timer_insert). + // dispatched through `bun_runtime_timer_insert` (see + // VirtualMachine::timer_insert). let vm = std::ptr::from_ref::(self.client.get().vm).cast_mut(); // SAFETY: `vm` is the live per-thread VM; `timer` is an unlinked // `EventLoopTimer` field of the boxed `JSValkeyClient` (stable address @@ -1622,7 +1623,7 @@ impl JSValkeyClient { valkey::TLS::None => None, valkey::TLS::Enabled => { // SAFETY: `vm_ptr` is the live per-thread VM (see above). - Some(unsafe { crate::jsc_hooks::default_client_ssl_ctx(vm_ptr) }) + Some(unsafe { crate::jsc_hooks::bun_runtime_default_client_ssl_ctx(vm_ptr) }) } valkey::TLS::Custom(_) => Some(self._secure.get().unwrap()), }; diff --git a/test/js/node/no-addons-worker-fixture.js b/test/js/node/no-addons-worker-fixture.js new file mode 100644 index 000000000000..b2f1bbf24ca3 --- /dev/null +++ b/test/js/node/no-addons-worker-fixture.js @@ -0,0 +1,12 @@ +onmessage = () => { + let error = null; + try { + process.dlopen({ exports: {} }, "./does-not-exist.node"); + } catch (e) { + error = e.message; + } + postMessage({ + execArgv: process.execArgv, + error, + }); +}; diff --git a/test/js/node/no-addons.test.ts b/test/js/node/no-addons.test.ts index 0df8cf5155e2..f8f78b880d03 100644 --- a/test/js/node/no-addons.test.ts +++ b/test/js/node/no-addons.test.ts @@ -15,3 +15,34 @@ test("--no-addons throws an error on process.dlopen", () => { expect(out).toBeEmpty(); expect(err).toContain("\nerror: Cannot load native addon because loading addons is disabled."); }); + +async function dlopenInWorker(execArgv: string[]): Promise { + const worker = new Worker(new URL("./no-addons-worker-fixture.js", import.meta.url).href, { + execArgv, + }); + const { promise, resolve, reject } = Promise.withResolvers(); + worker.onerror = reject; + worker.onmessage = e => resolve(e.data); + worker.postMessage("go"); + try { + return await promise; + } finally { + worker.terminate(); + } +} + +test.concurrent("worker execArgv --no-addons disables process.dlopen inside the worker", async () => { + expect(await dlopenInWorker(["--no-addons"])).toEqual({ + execArgv: ["--no-addons"], + error: "Cannot load native addon because loading addons is disabled.", + }); +}); + +test.concurrent("worker without --no-addons can call process.dlopen", async () => { + const result = await dlopenInWorker([]); + // dlopen ran: it fails because the path doesn't exist, not because addons + // are disabled for the worker. + expect(result.execArgv).toEqual([]); + expect(result.error).not.toBeNull(); + expect(result.error).not.toContain("loading addons is disabled"); +});