From 46c43d954ccc56dca5045ea13e7b09322a6d7675 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 24 Mar 2025 03:10:15 -0700 Subject: [PATCH] Add cursor rule documenting zig javascriptcore bindings --- .cursor/rules/zig-javascriptcore-classes.mdc | 488 +++++++++++++++++++ 1 file changed, 488 insertions(+) create mode 100644 .cursor/rules/zig-javascriptcore-classes.mdc diff --git a/.cursor/rules/zig-javascriptcore-classes.mdc b/.cursor/rules/zig-javascriptcore-classes.mdc new file mode 100644 index 0000000000..4c99f4929a --- /dev/null +++ b/.cursor/rules/zig-javascriptcore-classes.mdc @@ -0,0 +1,488 @@ +--- +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. \ No newline at end of file