--- description: How Zig works with JavaScriptCore bindings generator globs: alwaysApply: false --- # Bun's JavaScriptCore Class Bindings Generator This document explains how Bun's class bindings generator works to bridge Zig and JavaScript code through JavaScriptCore (JSC). ## Architecture Overview Bun's binding system creates a seamless bridge between JavaScript and Zig, allowing Zig implementations to be exposed as JavaScript classes. The system has several key components: 1. **Zig Implementation** (.zig files) 2. **JavaScript Interface Definition** (.classes.ts files) 3. **Generated Code** (C++/Zig files that connect everything) ## Class Definition Files ### JavaScript Interface (.classes.ts) The `.classes.ts` files define the JavaScript API using a declarative approach: ```typescript // Example: encoding.classes.ts define({ name: "TextDecoder", constructor: true, JSType: "object", finalize: true, proto: { decode: { // Function definition args: 1, }, encoding: { // Getter with caching getter: true, cache: true, }, fatal: { // Read-only property getter: true, }, ignoreBOM: { // Read-only property getter: true, } } }); ``` Each class definition specifies: - The class name - Whether it has a constructor - JavaScript type (object, function, etc.) - Properties and methods in the `proto` field - Caching strategy for properties - Finalization requirements ### Zig Implementation (.zig) The Zig files implement the native functionality: ```zig // Example: TextDecoder.zig pub const TextDecoder = struct { // Internal state encoding: []const u8, fatal: bool, ignoreBOM: bool, // Use generated bindings pub usingnamespace JSC.Codegen.JSTextDecoder; pub usingnamespace bun.New(@This()); // Constructor implementation - note use of globalObject pub fn constructor( globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame, ) bun.JSError!*TextDecoder { // Implementation } // Prototype methods - note return type includes JSError pub fn decode( this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame, ) bun.JSError!JSC.JSValue { // Implementation } // Getters pub fn getEncoding(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.createStringFromUTF8(globalObject, this.encoding); } pub fn getFatal(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.jsBoolean(this.fatal); } // Cleanup - note standard pattern of using deinit/deref pub fn deinit(this: *TextDecoder) void { // Release any retained resources } pub fn finalize(this: *TextDecoder) void { this.deinit(); // Or sometimes this is used to free memory instead bun.default_allocator.destroy(this); } }; ``` Key components in the Zig file: - The struct containing native state - `usingnamespace JSC.Codegen.JS` to include generated code - `usingnamespace bun.New(@This())` for object creation helpers - Constructor and methods using `bun.JSError!JSValue` return type for proper error handling - Consistent use of `globalObject` parameter name instead of `ctx` - Methods matching the JavaScript interface - Getters/setters for properties - Proper resource cleanup pattern with `deinit()` and `finalize()` ## Code Generation System The binding generator produces C++ code that connects JavaScript and Zig: 1. **JSC Class Structure**: Creates C++ classes for the JS object, prototype, and constructor 2. **Memory Management**: Handles GC integration through JSC's WriteBarrier 3. **Method Binding**: Connects JS function calls to Zig implementations 4. **Type Conversion**: Converts between JS values and Zig types 5. **Property Caching**: Implements the caching system for properties The generated C++ code includes: - A JSC wrapper class (`JSTextDecoder`) - A prototype class (`JSTextDecoderPrototype`) - A constructor function (`JSTextDecoderConstructor`) - Function bindings (`TextDecoderPrototype__decodeCallback`) - Property getters/setters (`TextDecoderPrototype__encodingGetterWrap`) ## CallFrame Access The `CallFrame` object provides access to JavaScript execution context: ```zig pub fn decode( this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame ) bun.JSError!JSC.JSValue { // Get arguments const input = callFrame.argument(0); const options = callFrame.argument(1); // Get this value const thisValue = callFrame.thisValue(); // Implementation with error handling if (input.isUndefinedOrNull()) { return globalObject.throw("Input cannot be null or undefined", .{}); } // Return value or throw error return JSC.JSValue.jsString(globalObject, "result"); } ``` CallFrame methods include: - `argument(i)`: Get the i-th argument - `argumentCount()`: Get the number of arguments - `thisValue()`: Get the `this` value - `callee()`: Get the function being called ## Property Caching and GC-Owned Values The `cache: true` option in property definitions enables JSC's WriteBarrier to efficiently store values: ```typescript encoding: { getter: true, cache: true, // Enable caching } ``` ### C++ Implementation In the generated C++ code, caching uses JSC's WriteBarrier: ```cpp JSC_DEFINE_CUSTOM_GETTER(TextDecoderPrototype__encodingGetterWrap, (...)) { auto& vm = JSC::getVM(lexicalGlobalObject); Zig::GlobalObject *globalObject = reinterpret_cast(lexicalGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); JSTextDecoder* thisObject = jsCast(JSValue::decode(encodedThisValue)); JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); // Check for cached value and return if present if (JSValue cachedValue = thisObject->m_encoding.get()) return JSValue::encode(cachedValue); // Get value from Zig implementation JSC::JSValue result = JSC::JSValue::decode( TextDecoderPrototype__getEncoding(thisObject->wrapped(), globalObject) ); RETURN_IF_EXCEPTION(throwScope, {}); // Store in cache for future access thisObject->m_encoding.set(vm, thisObject, result); RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); } ``` ### Zig Accessor Functions For each cached property, the generator creates Zig accessor functions that allow Zig code to work with these GC-owned values: ```zig // External function declarations extern fn TextDecoderPrototype__encodingSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(JSC.conv) void; extern fn TextDecoderPrototype__encodingGetCachedValue(JSC.JSValue) callconv(JSC.conv) JSC.JSValue; /// `TextDecoder.encoding` setter /// This value will be visited by the garbage collector. pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { JSC.markBinding(@src()); TextDecoderPrototype__encodingSetCachedValue(thisValue, globalObject, value); } /// `TextDecoder.encoding` getter /// This value will be visited by the garbage collector. pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { JSC.markBinding(@src()); const result = TextDecoderPrototype__encodingGetCachedValue(thisValue); if (result == .zero) return null; return result; } ``` ### Benefits of GC-Owned Values This system provides several key benefits: 1. **Automatic Memory Management**: The JavaScriptCore GC tracks and manages these values 2. **Proper Garbage Collection**: The WriteBarrier ensures values are properly visited during GC 3. **Consistent Access**: Zig code can easily get/set these cached JS values 4. **Performance**: Cached values avoid repeated computation or serialization ### Use Cases GC-owned cached values are particularly useful for: 1. **Computed Properties**: Store expensive computation results 2. **Lazily Created Objects**: Create objects only when needed, then cache them 3. **References to Other Objects**: Store references to other JS objects that need GC tracking 4. **Memoization**: Cache results based on input parameters The WriteBarrier mechanism ensures that any JS values stored in this way are properly tracked by the garbage collector. ## Memory Management and Finalization The binding system handles memory management across the JavaScript/Zig boundary: 1. **Object Creation**: JavaScript `new TextDecoder()` creates both a JS wrapper and a Zig struct 2. **Reference Tracking**: JSC's GC tracks all JS references to the object 3. **Finalization**: When the JS object is collected, the finalizer releases Zig resources Bun uses a consistent pattern for resource cleanup: ```zig // Resource cleanup method - separate from finalization pub fn deinit(this: *TextDecoder) void { // Release resources like strings this._encoding.deref(); // String deref pattern // Free any buffers if (this.buffer) |buffer| { bun.default_allocator.free(buffer); } } // Called by the GC when object is collected pub fn finalize(this: *TextDecoder) void { JSC.markBinding(@src()); // For debugging this.deinit(); // Clean up resources bun.default_allocator.destroy(this); // Free the object itself } ``` Some objects that hold references to other JS objects use `.deref()` instead: ```zig pub fn finalize(this: *SocketAddress) void { JSC.markBinding(@src()); this._presentation.deref(); // Release references this.destroy(); } ``` ## Error Handling with JSError Bun uses `bun.JSError!JSValue` return type for proper error handling: ```zig pub fn decode( this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame ) bun.JSError!JSC.JSValue { // Throwing an error if (callFrame.argumentCount() < 1) { return globalObject.throw("Missing required argument", .{}); } // Or returning a success value return JSC.JSValue.jsString(globalObject, "Success!"); } ``` This pattern allows Zig functions to: 1. Return JavaScript values on success 2. Throw JavaScript exceptions on error 3. Propagate errors automatically through the call stack ## Type Safety and Error Handling The binding system includes robust error handling: ```cpp // Example of type checking in generated code JSTextDecoder* thisObject = jsDynamicCast(callFrame->thisValue()); if (UNLIKELY(!thisObject)) { scope.throwException(lexicalGlobalObject, Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "TextDecoder"_s)); return {}; } ``` ## Prototypal Inheritance The binding system creates proper JavaScript prototype chains: 1. **Constructor**: JSTextDecoderConstructor with standard .prototype property 2. **Prototype**: JSTextDecoderPrototype with methods and properties 3. **Instances**: Each JSTextDecoder instance with __proto__ pointing to prototype This ensures JavaScript inheritance works as expected: ```cpp // From generated code void JSTextDecoderConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSTextDecoderPrototype* prototype) { Base::finishCreation(vm, 0, "TextDecoder"_s, PropertyAdditionMode::WithoutStructureTransition); // Set up the prototype chain putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); ASSERT(inherits(info())); } ``` ## Performance Considerations The binding system is optimized for performance: 1. **Direct Pointer Access**: JavaScript objects maintain a direct pointer to Zig objects 2. **Property Caching**: WriteBarrier caching avoids repeated native calls for stable properties 3. **Memory Management**: JSC garbage collection integrated with Zig memory management 4. **Type Conversion**: Fast paths for common JavaScript/Zig type conversions ## Creating a New Class Binding To create a new class binding in Bun: 1. **Define the class interface** in a `.classes.ts` file: ```typescript define({ name: "MyClass", constructor: true, finalize: true, proto: { myMethod: { args: 1, }, myProperty: { getter: true, cache: true, } } }); ``` 2. **Implement the native functionality** in a `.zig` file: ```zig pub const MyClass = struct { // State value: []const u8, // Generated bindings pub usingnamespace JSC.Codegen.JSMyClass; pub usingnamespace bun.New(@This()); // Constructor pub fn constructor( globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame, ) bun.JSError!*MyClass { const arg = callFrame.argument(0); // Implementation } // Method pub fn myMethod( this: *MyClass, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame, ) bun.JSError!JSC.JSValue { // Implementation } // Getter pub fn getMyProperty(this: *MyClass, globalObject: *JSGlobalObject) JSC.JSValue { return JSC.JSValue.jsString(globalObject, this.value); } // Resource cleanup pub fn deinit(this: *MyClass) void { // Clean up resources } pub fn finalize(this: *MyClass) void { this.deinit(); bun.default_allocator.destroy(this); } }; ``` 3. **The binding generator** creates all necessary C++ and Zig glue code to connect JavaScript and Zig, including: - C++ class definitions - Method and property bindings - Memory management utilities - GC integration code ## Generated Code Structure The binding generator produces several components: ### 1. C++ Classes For each Zig class, the system generates: - **JS**: Main wrapper that holds a pointer to the Zig object (`JSTextDecoder`) - **JSPrototype**: Contains methods and properties (`JSTextDecoderPrototype`) - **JSConstructor**: Implementation of the JavaScript constructor (`JSTextDecoderConstructor`) ### 2. C++ Methods and Properties - **Method Callbacks**: `TextDecoderPrototype__decodeCallback` - **Property Getters/Setters**: `TextDecoderPrototype__encodingGetterWrap` - **Initialization Functions**: `finishCreation` methods for setting up the class ### 3. Zig Bindings - **External Function Declarations**: ```zig extern fn TextDecoderPrototype__decode(*TextDecoder, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSC.EncodedJSValue; ``` - **Cached Value Accessors**: ```zig pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { ... } pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { ... } ``` - **Constructor Helpers**: ```zig pub fn create(globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { ... } ``` ### 4. GC Integration - **Memory Cost Calculation**: `estimatedSize` method - **Child Visitor Methods**: `visitChildrenImpl` and `visitAdditionalChildren` - **Heap Analysis**: `analyzeHeap` for debugging memory issues This architecture makes it possible to implement high-performance native functionality in Zig while exposing a clean, idiomatic JavaScript API to users.