mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
510 lines
16 KiB
Plaintext
510 lines
16 KiB
Plaintext
---
|
|
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 {
|
|
// Expose generated bindings as `js` namespace with trait conversion methods
|
|
pub const js = JSC.Codegen.JSTextDecoder;
|
|
pub const toJS = js.toJS;
|
|
pub const fromJS = js.fromJS;
|
|
pub const fromJSDirect = js.fromJSDirect;
|
|
|
|
// Internal state
|
|
encoding: []const u8,
|
|
fatal: bool,
|
|
ignoreBOM: bool,
|
|
|
|
// Constructor implementation - note use of globalObject
|
|
pub fn constructor(
|
|
globalObject: *JSGlobalObject,
|
|
callFrame: *JSC.CallFrame,
|
|
) bun.JSError!*TextDecoder {
|
|
// Implementation
|
|
|
|
return bun.new(TextDecoder, .{
|
|
// Fields
|
|
});
|
|
}
|
|
|
|
// 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
|
|
fn deinit(this: *TextDecoder) void {
|
|
// Release any retained resources
|
|
// Free the pointer at the end.
|
|
bun.destroy(this);
|
|
}
|
|
|
|
// Finalize - called by JS garbage collector. This should call deinit, or deref if reference counted.
|
|
pub fn finalize(this: *TextDecoder) void {
|
|
this.deinit();
|
|
}
|
|
};
|
|
```
|
|
|
|
Key components in the Zig file:
|
|
|
|
- The struct containing native state
|
|
- `pub const js = JSC.Codegen.JS<ClassName>` to include generated code
|
|
- 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()`
|
|
- Update `src/bun.js/bindings/generated_classes_list.zig` to include the new class
|
|
|
|
## 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<Zig::GlobalObject*>(lexicalGlobalObject);
|
|
auto throwScope = DECLARE_THROW_SCOPE(vm);
|
|
JSTextDecoder* thisObject = jsCast<JSTextDecoder*>(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<JSTextDecoder*>(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 {
|
|
// Generated bindings
|
|
pub const js = JSC.Codegen.JSMyClass;
|
|
pub const toJS = js.toJS;
|
|
pub const fromJS = js.fromJS;
|
|
pub const fromJSDirect = js.fromJSDirect;
|
|
|
|
// State
|
|
value: []const u8,
|
|
|
|
pub const new = bun.TrivialNew(@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.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<Class>**: Main wrapper that holds a pointer to the Zig object (`JSTextDecoder`)
|
|
- **JS<Class>Prototype**: Contains methods and properties (`JSTextDecoderPrototype`)
|
|
- **JS<Class>Constructor**: 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.
|