Good AI Tools

>> .claude/skills/implementing-jsc-classes-zig

stars: 86244
forks: 3926
watches: 86244
last updated: 2026-01-13 06:38:08

Bun's JavaScriptCore Class Bindings Generator

Bridge JavaScript and Zig through .classes.ts definitions and Zig implementations.

Architecture

  1. Zig Implementation (.zig files)
  2. JavaScript Interface Definition (.classes.ts files)
  3. Generated Code (C++/Zig files connecting them)

Class Definition (.classes.ts)

define({
  name: "TextDecoder",
  constructor: true,
  JSType: "object",
  finalize: true,
  proto: {
    decode: { args: 1 },
    encoding: { getter: true, cache: true },
    fatal: { getter: true },
  },
});

Options:

  • name: Class name
  • constructor: Has public constructor
  • JSType: "object", "function", etc.
  • finalize: Needs cleanup
  • proto: Properties/methods
  • cache: Cache property values via WriteBarrier

Zig Implementation

pub const TextDecoder = struct {
    pub const js = JSC.Codegen.JSTextDecoder;
    pub const toJS = js.toJS;
    pub const fromJS = js.fromJS;
    pub const fromJSDirect = js.fromJSDirect;

    encoding: []const u8,
    fatal: bool,

    pub fn constructor(
        globalObject: *JSGlobalObject,
        callFrame: *JSC.CallFrame,
    ) bun.JSError!*TextDecoder {
        return bun.new(TextDecoder, .{ .encoding = "utf-8", .fatal = false });
    }

    pub fn decode(
        this: *TextDecoder,
        globalObject: *JSGlobalObject,
        callFrame: *JSC.CallFrame,
    ) bun.JSError!JSC.JSValue {
        const args = callFrame.arguments();
        if (args.len < 1 or args.ptr[0].isUndefinedOrNull()) {
            return globalObject.throw("Input cannot be null", .{});
        }
        return JSC.JSValue.jsString(globalObject, "result");
    }

    pub fn getEncoding(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue {
        return JSC.JSValue.createStringFromUTF8(globalObject, this.encoding);
    }

    fn deinit(this: *TextDecoder) void {
        // Release resources
    }

    pub fn finalize(this: *TextDecoder) void {
        this.deinit();
        bun.destroy(this);
    }
};

Key patterns:

  • Use bun.JSError!JSValue return type for error handling
  • Use globalObject not ctx
  • deinit() for cleanup, finalize() called by GC
  • Update src/bun.js/bindings/generated_classes_list.zig

CallFrame Access

const args = callFrame.arguments();
const first_arg = args.ptr[0];  // Access as slice
const argCount = args.len;
const thisValue = callFrame.thisValue();

Property Caching

For cache: true properties, generated accessors:

// Get cached value
pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
    const result = TextDecoderPrototype__encodingGetCachedValue(thisValue);
    if (result == .zero) return null;
    return result;
}

// Set cached value
pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
    TextDecoderPrototype__encodingSetCachedValue(thisValue, globalObject, value);
}

Error Handling

pub fn method(this: *MyClass, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue {
    const args = callFrame.arguments();
    if (args.len < 1) {
        return globalObject.throw("Missing required argument", .{});
    }
    return JSC.JSValue.jsString(globalObject, "Success!");
}

Memory Management

pub fn deinit(this: *TextDecoder) void {
    this._encoding.deref();
    if (this.buffer) |buffer| {
        bun.default_allocator.free(buffer);
    }
}

pub fn finalize(this: *TextDecoder) void {
    JSC.markBinding(@src());
    this.deinit();
    bun.default_allocator.destroy(this);
}

Creating a New Binding

  1. Define interface in .classes.ts:
define({
  name: "MyClass",
  constructor: true,
  finalize: true,
  proto: {
    myMethod: { args: 1 },
    myProperty: { getter: true, cache: true },
  },
});
  1. Implement in .zig:
pub const MyClass = struct {
    pub const js = JSC.Codegen.JSMyClass;
    pub const toJS = js.toJS;
    pub const fromJS = js.fromJS;

    value: []const u8,

    pub const new = bun.TrivialNew(@This());

    pub fn constructor(globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!*MyClass {
        return MyClass.new(.{ .value = "" });
    }

    pub fn myMethod(this: *MyClass, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue {
        return JSC.JSValue.jsUndefined();
    }

    pub fn getMyProperty(this: *MyClass, globalObject: *JSGlobalObject) JSC.JSValue {
        return JSC.JSValue.jsString(globalObject, this.value);
    }

    pub fn deinit(this: *MyClass) void {}

    pub fn finalize(this: *MyClass) void {
        this.deinit();
        bun.destroy(this);
    }
};
  1. Add to src/bun.js/bindings/generated_classes_list.zig

Generated Components

  • C++ Classes: JSMyClass, JSMyClassPrototype, JSMyClassConstructor
  • Method Bindings: MyClassPrototype__myMethodCallback
  • Property Accessors: MyClassPrototype__myPropertyGetterWrap
  • Zig Bindings: External function declarations, cached value accessors