BunにJSCクラスをC++で追加する方法:実装パターンガイド
BunにJavaScriptクラスを追加したいけれど、JavaScriptCore(JSC)のC++ APIはボイラープレートが多く、何から書けばよいかわかりにくいと感じていませんか?
この記事では、implementing-jsc-classes-cppスキルが提供するC++バインディングのテンプレートと各パターンの使い方を解説します。
このスキルは何をしてくれるのか
JavaScriptCore上でC++を使ってJSクラスを実装する際の定型パターンを提供するリファレンスです。
- JSCクラスの3層構造テンプレート: DestructibleObject、Prototype、Constructorの3層で構成されるクラス定義パターン
- Iso Subspaceの設定方法: JSCのガベージコレクタ(GC)がC++フィールドを正しく管理するためのサブスペース登録
- プロパティ定義テーブル: HashTableValueによるGetter、Setter、メソッドの宣言的な登録
- Structure Caching: LazyClassStructureを使ったJSCオブジェクト構造のキャッシュ
- Zig-C++間のFFI連携:
extern "C"によるBunのZigコアとの接続
Bunへのコントリビューターや、JSC上でC++バインディングを書く開発者に向いています。
インストール方法
このスキルはBunリポジトリ内のガイドドキュメントです。スキル固有のインストールコマンドはありません。Bunリポジトリをクローンし、SKILL.mdを参照してください。
前提条件
- Bunのソースコードリポジトリへのアクセス
- LLVM 19(Bunのビルドに必須)
- C++およびJavaScriptCoreの基礎知識
使い方
Iso Subspaceテンプレート
C++フィールドを持つJSCクラスを作成する場合、GCのメモリ管理のためIso Subspaceを定義します。
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,
[](auto& spaces) { return spaces.m_clientSubspaceForMyClassT.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForMyClassT = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForMyClassT.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForMyClassT = std::forward<decltype(space)>(space); });
}
プロパティ定義テーブル
HashTableValueの配列でGetter、メソッドを宣言的に定義します。
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 } },
};
Getterの実装
JSC_DEFINE_CUSTOM_GETTERマクロで型チェック付きの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()));
}
Structure CachingとZig連携
LazyClassStructureでStructureをキャッシュし、extern "C"でZigに公開します。
// ZigGlobalObject.h
JSC::LazyClassStructure m_JSFooClassStructure;
// ZigGlobalObject.cpp
m_JSFooClassStructure.initLater([](LazyClassStructure::Initializer& init) {
Bun::initJSFooClassStructure(init);
});
// Zig への公開
extern "C" JSC::EncodedJSValue Bun__JSFooConstructor(Zig::GlobalObject* globalObject) {
return JSValue::encode(globalObject->m_JSFooClassStructure.constructor(globalObject));
}
知っておくべき注意点
subspaceの登録漏れ
C++フィールドを持つクラスは、DOMClientIsoSubspaces.hとDOMIsoSubspaces.hの両方にsubspaceを追加する必要があります。片方だけでは正しく動作しません。
root.hのインクルード
C++ファイルの先頭に#include "root.h"がないとコンパイルエラーになります。Bun固有の要件です。
3層構造は必須ではない
publicコンストラクタが不要なら、PrototypeとClassのみで構成できます。
LLVM 19が必須
BunのビルドにはLLVM 19が必要です。バージョン不一致はメモリ割り当て失敗の原因になります。
自動コード生成の検討
Bunにはgenerate-classes.tsによる自動コード生成パイプラインがあり、多くの場合は.classes.tsでの宣言的定義が推奨されます。手動実装が必要かどうかを事前に判断してください。
まとめ
implementing-jsc-classes-cppスキルは、BunにおけるJSCクラスのC++実装パターンをまとめたリファレンスです。Iso Subspace、プロパティ定義、Structure Caching、Zig連携のテンプレートを提供しています。
まずgenerate-classes.tsで対応できるか確認し、手動実装が必要な場合に活用してください。