From c370645afc9121b3b9c8d8d32293b54b80cfe15c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Jul 2025 21:08:26 -0700 Subject: [PATCH] Update zig-javascriptcore-classes.mdc --- .cursor/rules/zig-javascriptcore-classes.mdc | 77 +++++++++++--------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/.cursor/rules/zig-javascriptcore-classes.mdc b/.cursor/rules/zig-javascriptcore-classes.mdc index 965932a988..88636c9752 100644 --- a/.cursor/rules/zig-javascriptcore-classes.mdc +++ b/.cursor/rules/zig-javascriptcore-classes.mdc @@ -1,8 +1,9 @@ --- description: How Zig works with JavaScriptCore bindings generator -globs: +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). @@ -24,7 +25,7 @@ The `.classes.ts` files define the JavaScript API using a declarative approach: ```typescript // Example: encoding.classes.ts define({ - name: "TextDecoder", + name: "TextDecoder", constructor: true, JSType: "object", finalize: true, @@ -40,17 +41,18 @@ define({ }, fatal: { // Read-only property - getter: true, + 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.) @@ -87,7 +89,7 @@ pub const TextDecoder = struct { // Fields }); } - + // Prototype methods - note return type includes JSError pub fn decode( this: *TextDecoder, @@ -96,23 +98,23 @@ pub const TextDecoder = struct { ) 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(); @@ -121,6 +123,7 @@ pub const TextDecoder = struct { ``` Key components in the Zig file: + - The struct containing native state - `pub const js = JSC.Codegen.JS` to include generated code - Constructor and methods using `bun.JSError!JSValue` return type for proper error handling @@ -128,6 +131,7 @@ Key components in the Zig file: - 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 @@ -140,6 +144,7 @@ The binding generator produces C++ code that connects JavaScript and Zig: 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`) @@ -152,28 +157,29 @@ The `CallFrame` object provides access to JavaScript execution context: ```zig pub fn decode( - this: *TextDecoder, + 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 @@ -201,17 +207,17 @@ JSC_DEFINE_CUSTOM_GETTER(TextDecoderPrototype__encodingGetterWrap, (...)) { 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)); @@ -253,7 +259,7 @@ 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 +4. **Performance**: Cached values avoid repeated computation or serialization ### Use Cases @@ -281,7 +287,7 @@ Bun uses a consistent pattern for resource cleanup: 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); @@ -312,7 +318,7 @@ Bun uses `bun.JSError!JSValue` return type for proper error handling: ```zig pub fn decode( - this: *TextDecoder, + this: *TextDecoder, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame ) bun.JSError!JSC.JSValue { @@ -320,13 +326,14 @@ pub fn decode( 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 @@ -339,7 +346,7 @@ The binding system includes robust error handling: // Example of type checking in generated code JSTextDecoder* thisObject = jsDynamicCast(callFrame->thisValue()); if (UNLIKELY(!thisObject)) { - scope.throwException(lexicalGlobalObject, + scope.throwException(lexicalGlobalObject, Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "TextDecoder"_s)); return {}; } @@ -351,7 +358,7 @@ 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 +3. **Instances**: Each JSTextDecoder instance with **proto** pointing to prototype This ensures JavaScript inheritance works as expected: @@ -360,7 +367,7 @@ This ensures JavaScript inheritance works as expected: 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())); @@ -372,7 +379,7 @@ void JSTextDecoderConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globa 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 +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 @@ -381,6 +388,7 @@ The binding system is optimized for performance: To create a new class binding in Bun: 1. **Define the class interface** in a `.classes.ts` file: + ```typescript define({ name: "MyClass", @@ -393,12 +401,13 @@ To create a new class binding in Bun: myProperty: { getter: true, cache: true, - } - } + }, + }, }); ``` 2. **Implement the native functionality** in a `.zig` file: + ```zig pub const MyClass = struct { // Generated bindings @@ -409,9 +418,9 @@ To create a new class binding in Bun: // State value: []const u8, - + pub const new = bun.TrivialNew(@This()); - + // Constructor pub fn constructor( globalObject: *JSGlobalObject, @@ -420,7 +429,7 @@ To create a new class binding in Bun: const arg = callFrame.argument(0); // Implementation } - + // Method pub fn myMethod( this: *MyClass, @@ -429,17 +438,17 @@ To create a new class binding in Bun: ) 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); @@ -474,11 +483,13 @@ For each Zig class, the system generates: ### 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 { ... }