CtrlK
BlogDocsLog inGet started
Tessl Logo

implementing-jsc-classes-cpp

Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.

72

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Implementing JavaScript Classes in C++

Class Structure

For publicly accessible Constructor and Prototype, create 3 classes:

  1. class Foo : public JSC::DestructibleObject - if C++ fields exist; otherwise use JSC::constructEmptyObject with putDirectOffset
  2. class FooPrototype : public JSC::JSNonFinalObject
  3. class FooConstructor : public JSC::InternalFunction

No public constructor? Only Prototype and class needed.

Iso Subspaces

Classes with C++ fields need subspaces in:

  • src/jsc/bindings/webcore/DOMClientIsoSubspaces.h
  • src/jsc/bindings/webcore/DOMIsoSubspaces.h
template<typename MyClassT, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) {
    if constexpr (mode == JSC::SubspaceAccess::Concurrently)
        return nullptr;
    return WebCore::subspaceForImpl<MyClassT, WebCore::UseCustomHeapCellType::No>(
        vm, BUN_SUBSPACE_SLOTS(m_clientSubspaceForMyClassT, m_subspaceForMyClassT));
}

subspaceForImpl only reads the slots inline. The subspaces themselves are created by subspaceForImplSlow in src/jsc/bindings/BunClientData.cpp, which is the only place that constructs an IsoSubspace (test/internal/source-lints/iso-subspace-creation.test.ts checks this).

Property Definitions

static JSC_DECLARE_HOST_FUNCTION(jsFooProtoFuncMethod);
static JSC_DECLARE_CUSTOM_GETTER(jsFooGetter_property);

static const HashTableValue JSFooPrototypeTableValues[] = {
    { "property"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsFooGetter_property, 0 } },
    { "method"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFooProtoFuncMethod, 1 } },
};

Prototype Class

class JSFooPrototype final : public JSC::JSNonFinalObject {
public:
    using Base = JSC::JSNonFinalObject;
    static constexpr unsigned StructureFlags = Base::StructureFlags;

    static JSFooPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) {
        JSFooPrototype* prototype = new (NotNull, allocateCell<JSFooPrototype>(vm)) JSFooPrototype(vm, structure);
        prototype->finishCreation(vm);
        return prototype;
    }

    template<typename, JSC::SubspaceAccess>
    static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { return &vm.plainObjectSpace(); }

    DECLARE_INFO;

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) {
        auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
        structure->setMayBePrototype(true);
        return structure;
    }

private:
    JSFooPrototype(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) {}
    void finishCreation(JSC::VM& vm);
};

void JSFooPrototype::finishCreation(VM& vm) {
    Base::finishCreation(vm);
    reifyStaticProperties(vm, JSFoo::info(), JSFooPrototypeTableValues, *this);
    JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}

Getter/Setter/Function Definitions

// Getter
JSC_DEFINE_CUSTOM_GETTER(jsFooGetter_prop, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName)) {
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSFoo* thisObject = jsDynamicCast<JSFoo*>(JSValue::decode(thisValue));
    if (UNLIKELY(!thisObject)) {
        Bun::throwThisTypeError(*globalObject, scope, "JSFoo"_s, "prop"_s);
        return {};
    }
    return JSValue::encode(jsBoolean(thisObject->value()));
}

// Function
JSC_DEFINE_HOST_FUNCTION(jsFooProtoFuncMethod, (JSGlobalObject* globalObject, CallFrame* callFrame)) {
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    auto* thisObject = jsDynamicCast<JSFoo*>(callFrame->thisValue());
    if (UNLIKELY(!thisObject)) {
        Bun::throwThisTypeError(*globalObject, scope, "Foo"_s, "method"_s);
        return {};
    }
    return JSValue::encode(thisObject->doSomething(vm, globalObject));
}

Constructor Class

class JSFooConstructor final : public JSC::InternalFunction {
public:
    using Base = JSC::InternalFunction;
    static constexpr unsigned StructureFlags = Base::StructureFlags;

    static JSFooConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSObject* prototype) {
        JSFooConstructor* constructor = new (NotNull, JSC::allocateCell<JSFooConstructor>(vm)) JSFooConstructor(vm, structure);
        constructor->finishCreation(vm, prototype);
        return constructor;
    }

    DECLARE_INFO;

    template<typename CellType, JSC::SubspaceAccess>
    static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { return &vm.internalFunctionSpace(); }

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) {
        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
    }

private:
    JSFooConstructor(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure, callFoo, constructFoo) {}

    void finishCreation(JSC::VM& vm, JSC::JSObject* prototype) {
        Base::finishCreation(vm, 0, "Foo"_s);
        putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly);
    }
};

Structure Caching

Add to ZigGlobalObject.h:

JSC::LazyClassStructure m_JSFooClassStructure;

Initialize in ZigGlobalObject.cpp:

m_JSFooClassStructure.initLater([](LazyClassStructure::Initializer& init) {
    Bun::initJSFooClassStructure(init);
});

Visit in visitChildrenImpl:

m_JSFooClassStructure.visit(visitor);

Expose to Zig

extern "C" JSC::EncodedJSValue Bun__JSFooConstructor(Zig::GlobalObject* globalObject) {
    return JSValue::encode(globalObject->m_JSFooClassStructure.constructor(globalObject));
}

extern "C" EncodedJSValue Bun__Foo__toJS(Zig::GlobalObject* globalObject, Foo* foo) {
    auto* structure = globalObject->m_JSFooClassStructure.get(globalObject);
    return JSValue::encode(JSFoo::create(globalObject->vm(), structure, globalObject, WTFMove(foo)));
}

Include #include "root.h" at the top of C++ files.

Repository
oven-sh/bun
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.