This page describes how Bun embeds WebKit's JavaScriptCore (JSC) engine, how the custom global object (Zig::GlobalObject) extends JSC's JSGlobalObject, and how the globalThis.Bun namespace is assembled from Rust host functions and C++ bindings.
For the event loop and async scheduling built on top of this layer, see 2.2. For how modules are resolved and loaded into the global, see 2.3. For the globalThis.Bun API surface from a user perspective, see 9.1.
Bun's runtime is structured as a thin orchestration layer around JSC. The critical path runs:
src/bun.js.zig).bun.jsc.initialize() to initialize the JSC engine, then creates a VirtualMachine.VirtualMachine owns a Zig::GlobalObject, Bun's C++ subclass of JSC's JSGlobalObject.Zig::GlobalObject installs the Bun namespace object (assembled in src/runtime/api/BunObject.rs) and all other host APIs on globalThis.Architecture diagram: Runtime layers and principal types
Sources: CLAUDE.md136-175 src/bun.js.zig1-15 src/jsc/bindings/IsolatedModuleCache.h1-12
JSC is a statically linked library from vendor/WebKit/. Before any VirtualMachine can be created, the engine itself must be initialized:
bun.jsc.initialize(eval_and_print: bool)
This call, visible in src/bun.js.zig, performs global JSC startup (initializing the garbage collector, intern tables, and built-in prototypes). It is called exactly once per process.
After initialization, VirtualMachine.init() or VirtualMachine.initWithModuleGraph() (for standalone executables) creates the per-VM state. Each VirtualMachine wraps a JSC::VM and a Zig::GlobalObject.
Sources: src/bun.js.zig28-114 src/bun.js.zig158-303
VirtualMachine (Rust: bun_jsc::virtual_machine::VirtualMachine) is the primary owner of runtime state. It is a singleton per JS thread.
| Field | Description |
|---|---|
global | Pointer to Zig::GlobalObject / JSGlobalObject |
jsc_vm | Raw pointer to the underlying JSC::VM |
transpiler | Bun's transpiler/resolver/bundler state |
event_loop | Task queue, timers, and I/O handles |
module_loader | Module cache and fetch hooks |
preload | Preload script paths |
argv | Passthrough argv for process.argv |
The public entry point for JS execution is:
vm.global.vm().holdAPILock(&run, callback)
holdAPILock acquires the JSC API lock and calls callback on the JS thread. All JS heap operations must occur within this lock. This is enforced by JSC internals.
Sequence diagram: Boot to first JS execution
Sources: src/bun.js.zig158-303 src/bun.js.zig310-560
Zig::GlobalObject is the C++ class (in src/jsc/bindings/) that extends JSC::JSGlobalObject. It is Bun's entire customization point over standard JSC behavior.
Responsibilities include:
globalThis.Bun: The BunObject namespace, a plain JSObject with registered host functions and lazy property callbacks.fetch, Request, Response, ReadableStream, Blob, FormData, URL, etc.process, Buffer, console, setImmediate, clearImmediate, __filename, __dirname.JSBlob, JSReadableStream, etc.) has a JSC Structure object (the hidden class descriptor). Zig::GlobalObject holds references to all of these so they don't have to be re-created on every object instantiation.BunClientData: Per-VM metadata (string atoms, identifier tables) attached to JSC::VM.In the C++ code, the class is referred to as both Zig::GlobalObject (its C++ namespace) and ZigGlobalObject (the name used in generate-classes.ts output and in legacy documentation).
Sources: src/jsc/bindings/IsolatedModuleCache.h1-12 CLAUDE.md213-228
Every custom class exposed to JS follows a three-class pattern:
| Class | Base | Role |
|---|---|---|
JSFoo | JSC::JSDestructibleObject | Main object with C++ fields |
JSFooPrototype | JSC::JSNonFinalObject | Prototype with method HashTableValues |
JSFooConstructor | JSC::InternalFunction | Constructor function, if public |
Structures (hidden class descriptors) for all of these are cached on Zig::GlobalObject. The code generator (src/codegen/generate-classes.ts) produces the C++ and Rust binding code from *.classes.ts definitions.
globalThis.Bunsrc/runtime/api/BunObject.rs implements the Bun namespace object. Its module-level doc comment states: "globalThis.Bun — top-level host functions and lazy-property getters."
The object is a JSObject installed on globalThis as the "Bun" property. Properties fall into two categories:
Static functions registered at global object creation time. Each is a native Rust function wrapped for the JSC call convention via static_adapters:
| JS name | Rust entry point |
|---|---|
Bun.connect | crate::socket::Listener::connect |
Bun.listen | crate::socket::Listener::listen |
Bun.udpSocket | UDPSocket::udp_socket |
Bun.spawn | crate::api::js_bun_spawn_bindings::spawn |
Bun.spawnSync | crate::api::js_bun_spawn_bindings::spawn_sync |
Bun.build | crate::api::js_bundler::JSBundler::build_fn |
Bun.$ (shell) | crate::shell::parsed_shell_script::CREATE_PARSED_SHELL_SCRIPT |
Bun.sha | Crypto::SHA512_256::hash_ |
PropertyCallback)Some properties are created on first access. Examples include Bun.redis (Valkey client) and Bun.sql (SQL connection). The lazy approach avoids initialization cost for APIs not used in a given process.
The startup code in src/bun.js.zig demonstrates how Bun.redis and Bun.sql are accessed eagerly when pre-connection flags are set — by calling vm.global.toJSValue().get(global, "Bun") and then accessing the property, which triggers the lazy getter:
// From src/bun.js.zig
let bun_object = vm.global.toJSValue().get(global, "Bun")
let redis = bun_object.get(global, "redis")
Sources: src/runtime/api/BunObject.rs1-213 src/bun.js.zig343-387
Diagram: BunObject property sources mapped to code modules
Sources: src/runtime/api/BunObject.rs90-213
The binding layer between Rust, C++, and JS is partially generated. Two main generators are relevant to the global object:
generate-classes.tsInput: src/jsc/bindings/*.classes.ts files
Output: C++ class skeletons + Rust FFI bindings
Each .classes.ts file describes a class's JS-visible interface (property names, types, visibility, whether it has a constructor). The generator emits:
finishCreation, visitChildrenImpl, JSType registration, and hash table entriesextern "C" declarations and safe wrapper typesWriteBarrier<> field declarations for GC safetybundle-modules.ts and bundle-functions.tsInput: src/js/{node,bun,builtins}/ TypeScript source files
Output: Embedded byte arrays (bundled JS) compiled into the binary
Modules like node:fs, node:path, and builtin functions like ReadableStream are written in TypeScript with special syntax ($isCallable, $putByIdDirectPrivate, etc.) and bundled at build time. These are loaded from memory (not disk) at startup.
Diagram: Code generation flow
Sources: CLAUDE.md226-245 src/js/CLAUDE.md1-70
When JS code calls a Bun.* function, execution flows through several layers:
CallFrame on the stack.JSGlobalObject* and CallFrame*.bun_runtime. Arguments are read from CallFrame using typed accessors. Exceptions are propagated via JsResult<JSValue> (the ? operator translates Err into a pending JSC exception).JSValue is returned to JSC. JSC transfers it back to JS as the call result.All Rust host functions must check for pending exceptions before using any JSC value obtained from user code (e.g., calling .toString(), .toNumber(), accessing properties via getters). The BUN_JSC_validateExceptionChecks=1 environment variable activates runtime verification of this invariant.
Diagram: Host function call path
Sources: CLAUDE.md263-274 src/runtime/api/BunObject.rs130-213
Beyond the core Bun object, Zig::GlobalObject conditionally installs additional globals depending on runtime flags:
| Flag | Effect | C++ function |
|---|---|---|
-e / --eval | Exposes require, module, __filename, etc. | Bun__ExposeNodeModuleGlobals |
--expose-gc | Adds gc() function to globalThis | JSC__JSGlobalObject__addGc |
These are applied from Run::addConditionalGlobals() in src/bun.js.zig, called inside the API lock just before the entry point is loaded.
Sources: src/bun.js.zig562-572
JSC's garbage collector and heap are not thread-safe relative to JS execution. All JS heap operations (object creation, property access, function calls) must occur on the JS thread while holding the API lock.
vm.global.vm().holdAPILock(ctx, callback) — acquires the lock and invokes callback. All JS code runs within this callback.JSValue references across calls use bun_jsc::Strong (a GC root handle), which must be created and dropped on the JS thread.For the event loop mechanism that drives tick-to-tick execution within the API lock, see 2.2.
Sources: src/bun.js.zig113 src/bun.js.zig302-303 src/CLAUDE.md274-305
Refresh this wiki