Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/commands/upgrade-webkit.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ To do that:
- delete the webkit-changes.md file

Things to check for a successful upgrade:
- Did JSType in vendor/WebKit/Source/JavaScriptCore have any recent changes? Does the enum values align with whats present in src/jsc/bindings/JSType.zig?
- Did JSType in vendor/WebKit/Source/JavaScriptCore have any recent changes? Does the enum values align with whats present in src/jsc/bindings/JSType.rust?
- Were there any changes to the webcore code generator? If there are C++ compilation errors, check for differences in some of the generated code in like vendor/WebKit/source/WebCore/bindings/scripts/test/JS/
12 changes: 6 additions & 6 deletions .claude/hooks/post-edit-zig-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ if (!filePath) {
process.exit(0);
}

function formatZigFile() {
function formatRustFile() {
try {
// Format the Zig file
const result = spawnSync("vendor/zig/zig.exe", ["fmt", filePath], {
// Format the Rust file
const result = spawnSync("vendor/rust/rust.exe", ["fmt", filePath], {
cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
encoding: "utf-8",
});
Expand All @@ -34,7 +34,7 @@ function formatZigFile() {
}

if (result.status !== 0) {
console.error(`zig fmt failed for ${filePath}:`);
console.error(`rust fmt failed for ${filePath}:`);
if (result.stderr) {
console.error(result.stderr);
}
Expand All @@ -56,8 +56,8 @@ function formatTypeScriptFile() {
} catch (error) {}
}

if (ext === ".zig") {
formatZigFile();
if (ext === ".rust") {
formatRustFile();
} else if (
[
".cjs",
Expand Down
4 changes: 2 additions & 2 deletions .claude/hooks/pre-bash-zig-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ useSystemBun = inlineEnv.get("USE_SYSTEM_BUN") ?? useSystemBun;
// Get the executable name (argv0)
const argv0 = basename(tokens[0], extname(tokens[0]));

// Check if it's zig or zig.exe
if (argv0 === "zig") {
// Check if it's rust or rust.exe
if (argv0 === "rust") {
// Filter out flags (starting with -) to get positional arguments
const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-"));

Expand Down
4 changes: 2 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash-zig-build.js"
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash-rust-build.js"
}
]
}
Expand All @@ -17,7 +17,7 @@
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-zig-format.js"
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-rust-format.js"
}
]
}
Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/implementing-jsc-classes-cpp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ private:

## Structure Caching

Add to `ZigGlobalObject.h`:
Add to `RustGlobalObject.h`:

```cpp
JSC::LazyClassStructure m_JSFooClassStructure;
```

Initialize in `ZigGlobalObject.cpp`:
Initialize in `RustGlobalObject.cpp`:

```cpp
m_JSFooClassStructure.initLater([](LazyClassStructure::Initializer& init) {
Expand All @@ -168,14 +168,14 @@ Visit in `visitChildrenImpl`:
m_JSFooClassStructure.visit(visitor);
```

## Expose to Zig
## Expose to Rust

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

extern "C" EncodedJSValue Bun__Foo__toJS(Zig::GlobalObject* globalObject, Foo* foo) {
extern "C" EncodedJSValue Bun__Foo__toJS(Rust::GlobalObject* globalObject, Foo* foo) {
auto* structure = globalObject->m_JSFooClassStructure.get(globalObject);
return JSValue::encode(JSFoo::create(globalObject->vm(), structure, globalObject, WTFMove(foo)));
}
Expand Down
32 changes: 16 additions & 16 deletions .claude/skills/implementing-jsc-classes-zig/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
name: implementing-jsc-classes-zig
description: Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.
name: implementing-jsc-classes-rust
description: Creates JavaScript classes using Bun's Rust bindings generator (.classes.ts). Use when implementing new JS APIs in Rust with JSC integration.
---

# Bun's JavaScriptCore Class Bindings Generator

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

## Architecture

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

## Class Definition (.classes.ts)

Expand All @@ -38,9 +38,9 @@ Options:
- `proto`: Properties/methods
- `cache`: Cache property values via WriteBarrier

## Zig Implementation
## Rust Implementation

```zig
```rust
pub const TextDecoder = struct {
pub const js = JSC.Codegen.JSTextDecoder;
pub const toJS = js.toJS;
Expand Down Expand Up @@ -89,11 +89,11 @@ pub const TextDecoder = struct {
- Use `bun.JSError!JSValue` return type for error handling
- Use `globalObject` not `ctx`
- `deinit()` for cleanup, `finalize()` called by GC
- Update `src/jsc/bindings/generated_classes_list.zig`
- Update `src/jsc/bindings/generated_classes_list.rust`

## CallFrame Access

```zig
```rust
const args = callFrame.arguments();
const first_arg = args.ptr[0]; // Access as slice
const argCount = args.len;
Expand All @@ -104,7 +104,7 @@ const thisValue = callFrame.thisValue();

For `cache: true` properties, generated accessors:

```zig
```rust
// Get cached value
pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
const result = TextDecoderPrototype__encodingGetCachedValue(thisValue);
Expand All @@ -120,7 +120,7 @@ pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObje

## Error Handling

```zig
```rust
pub fn method(this: *MyClass, globalObject: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const args = callFrame.arguments();
if (args.len < 1) {
Expand All @@ -132,7 +132,7 @@ pub fn method(this: *MyClass, globalObject: *JSGlobalObject, callFrame: *JSC.Cal

## Memory Management

```zig
```rust
pub fn deinit(this: *TextDecoder) void {
this._encoding.deref();
if (this.buffer) |buffer| {
Expand Down Expand Up @@ -163,9 +163,9 @@ define({
});
```

2. Implement in `.zig`:
2. Implement in `.rust`:

```zig
```rust
pub const MyClass = struct {
pub const js = JSC.Codegen.JSMyClass;
pub const toJS = js.toJS;
Expand Down Expand Up @@ -196,11 +196,11 @@ pub const MyClass = struct {
};
```

3. Add to `src/jsc/bindings/generated_classes_list.zig`
3. Add to `src/jsc/bindings/generated_classes_list.rust`

## Generated Components

- **C++ Classes**: `JSMyClass`, `JSMyClassPrototype`, `JSMyClassConstructor`
- **Method Bindings**: `MyClassPrototype__myMethodCallback`
- **Property Accessors**: `MyClassPrototype__myPropertyGetterWrap`
- **Zig Bindings**: External function declarations, cached value accessors
- **Rust Bindings**: External function declarations, cached value accessors
24 changes: 12 additions & 12 deletions .claude/skills/javascriptcore-garbage-collector/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ if (obj->cellState <= blackThreshold) // 0 normally, bumped while GC is markin

`vendor/WebKit/Source/JavaScriptCore/heap/ConservativeRoots.cpp` walks the native stack/registers word-by-word (after `MachineThreads::tryCopyOtherThreadStacks` snapshots them). Any aligned word inside a live `MarkedBlock` cell or `PreciseAllocation` is a root.

**This means:** a `JSCell*` / `JSValue` in a C++/Zig local variable _usually_ keeps the object alive — no `Handle`/`Local` ceremony like V8.
**This means:** a `JSCell*` / `JSValue` in a C++/Rust local variable _usually_ keeps the object alive — no `Handle`/`Local` ceremony like V8.

**This does NOT mean you're always safe.** The compiler may dead-store-eliminate the local after its last visible use, or never spill it. If you extract an interior pointer (`string->characters8()`, butterfly storage, typed-array `vector()`) and then call something that can allocate, the original cell may no longer be on the stack:

Expand All @@ -106,7 +106,7 @@ JSC::EnsureStillAliveScope keepAlive(cell); // RAII: forces cell onto stack un
// ... use interior pointer, call things that allocate ...
```

or `ensureStillAliveHere(cell)`. In Zig: `value.ensureStillAlive()`.
or `ensureStillAliveHere(cell)`. In Rust: `value.ensureStillAlive()`.

## `visitChildren` — the per-cell tracing hook

Expand Down Expand Up @@ -166,7 +166,7 @@ void JSFoo::visitOutputConstraints(JSCell* cell, Visitor& visitor) {

**Why two entry points?** `visitChildren` runs once when the cell turns grey. But marking may later discover that some _other_ native object (an opaque root) is live, which retroactively makes more of _this_ cell's references live. `visitOutputConstraints` is re-invoked by `DOMGCOutputConstraint` during the constraint fixpoint to catch that.

To make a class participate, its IsoSubspace must be registered as an **output-constraint subspace** (`clientSubspaceFor*` with `outputConstraint` in `BunClientData` / generated `ZigGeneratedClasses.cpp`). The codegen does this automatically when `.classes.ts` has `hasPendingActivity`, `own` properties, or event-target semantics.
To make a class participate, its IsoSubspace must be registered as an **output-constraint subspace** (`clientSubspaceFor*` with `outputConstraint` in `BunClientData` / generated `RustGeneratedClasses.cpp`). The codegen does this automatically when `.classes.ts` has `hasPendingActivity`, `own` properties, or event-target semantics.

## Opaque roots — liveness through non-JSCell pointers

Expand Down Expand Up @@ -213,9 +213,9 @@ JSC::Weak<JSFoo> m_wrapper { jsFoo, &myOwnerSingleton, nativeThing };

`Weak<T>` is **move-only** (allocates a `WeakImpl`). Don't put it in a hot path; cache it.

## Zig: `jsc.JSRef` — the native↔wrapper reference pattern
## Rust: `jsc.JSRef` — the native↔wrapper reference pattern

In Bun's Zig code, when a native object needs to hold a reference back to its own JS wrapper, **use `jsc.JSRef`** (`src/jsc/bindings/JSRef.zig`), not `gcProtect`, not a raw `JSValue` field, and usually not `jsc.Strong` directly.
In Bun's Rust code, when a native object needs to hold a reference back to its own JS wrapper, **use `jsc.JSRef`** (`src/jsc/bindings/JSRef.rust`), not `gcProtect`, not a raw `JSValue` field, and usually not `jsc.Strong` directly.

`JSRef` is a tagged union with three states:

Expand All @@ -225,7 +225,7 @@ In Bun's Zig code, when a native object needs to hold a reference back to its ow

Pattern: **strong while busy, weak while idle.**

```zig
```rust
this_value: jsc.JSRef = .empty(),

// On construction / when work starts:
Expand Down Expand Up @@ -276,7 +276,7 @@ visitor.reportExtraMemoryVisited(thisObject->wrapped().byteSize());
- If the size changes over time, report the delta on growth (`reportExtraMemoryAllocated(cell, newSize - oldSize)`) and report the current size in `visitChildren`.
- `deprecatedReportExtraMemory` exists for callers that can't satisfy the visit-side half — avoid it.

In `.classes.ts`, `estimatedSize: true` generates the `reportExtraMemoryVisited` side; you implement `estimatedSize()` in Zig. You still call `reportExtraMemoryAllocated` (or the binding's helper) at allocation time.
In `.classes.ts`, `estimatedSize: true` generates the `reportExtraMemoryVisited` side; you implement `estimatedSize()` in Rust. You still call `reportExtraMemoryAllocated` (or the binding's helper) at allocation time.

## `HeapAnalyzer` — heap snapshots and labelling

Expand Down Expand Up @@ -309,9 +309,9 @@ If your class shows up as an opaque blob in heap snapshots, implement `analyzeHe
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| JSCell field pointing to another JSCell | `WriteBarrier<T>` member + `visitor.append(m_field)` in `visitChildren` |
| Native state inside the wrapped C++ object holds JS values | `visitAdditionalChildren` + register subspace as output-constraint |
| C++/Zig local across allocation/call | Conservative scan (free) — add `EnsureStillAliveScope` / `value.ensureStillAlive()` if extracting interior pointers or seeing release-only crashes |
| **Zig** native object holds its own JS wrapper (class has `finalize: true`) | **`jsc.JSRef`** — `upgrade()` when work starts, `downgrade()` when idle. **This is the default.** |
| **Zig** native object owns an arbitrary JS value (callback, options object) | `jsc.Strong.Optional` — `deinit()` in `finalize()`. Watch for cycles |
| C++/Rust local across allocation/call | Conservative scan (free) — add `EnsureStillAliveScope` / `value.ensureStillAlive()` if extracting interior pointers or seeing release-only crashes |
| **Rust** native object holds its own JS wrapper (class has `finalize: true`) | **`jsc.JSRef`** — `upgrade()` when work starts, `downgrade()` when idle. **This is the default.** |
| **Rust** native object owns an arbitrary JS value (callback, options object) | `jsc.Strong.Optional` — `deinit()` in `finalize()`. Watch for cycles |
| C++ non-GC object owns a JS value as a root | `JSC::Strong<T>`. **Danger:** cycle if the JS value can reach back → leak |
| Weak ref with resurrection predicate / finalize callback (C++) | `JSC::Weak<T>` + `WeakHandleOwner` |
| Wrapper kept alive by **many concurrent operations** with no single busy/idle edge | `.classes.ts` `hasPendingActivity: true` (atomic flag polled on GC thread). **Uncommon — prefer `JSRef` if you can.** |
Expand All @@ -323,7 +323,7 @@ If your class shows up as an opaque blob in heap snapshots, implement `analyzeHe
## Destruction & finalizers

- `static constexpr bool needsDestruction = true` → C++ destructor runs when the cell is swept. Sweep is **lazy** (next allocation from that block, or `IncrementalSweeper`), so destruction is delayed arbitrarily. Do not rely on it for prompt resource release — expose explicit `close()`/`dispose()`.
- In `.classes.ts`, `finalize: true` → Zig `finalize()` called from the destructor. Same laziness applies.
- In `.classes.ts`, `finalize: true` → Rust `finalize()` called from the destructor. Same laziness applies.
- `WeakHandleOwner::finalize` runs earlier (at weak-reap time) but the cell is already dead; only use it to clear caches.
- Destructors run on the mutator thread but **other JS objects may already be swept** — do not dereference `WriteBarrier` fields in a destructor.

Expand Down Expand Up @@ -363,4 +363,4 @@ If GC runs constantly with little garbage → missing `reportExtraMemoryVisited`
- `vendor/WebKit/Source/JavaScriptCore/heap/HeapAnalyzer.h`, `vendor/WebKit/Source/JavaScriptCore/heap/HeapSnapshotBuilder.cpp`, Bun: `vendor/WebKit/Source/JavaScriptCore/heap/BunV8HeapSnapshotBuilder.cpp`
- `vendor/WebKit/Source/JavaScriptCore/heap/DeferGC.h`, `vendor/WebKit/Source/JavaScriptCore/heap/Strong.h`, `vendor/WebKit/Source/JavaScriptCore/heap/HandleSet.h`
- `runtime/JSCell.h` / `JSCellInlines.h` — header layout, `visitChildren` base
- Bun: `src/jsc/bindings/BunGCOutputConstraint.cpp`, `ZigGeneratedClasses.cpp` (codegen'd `visitChildren` / `visitOutputConstraints`)
- Bun: `src/jsc/bindings/BunGCOutputConstraint.cpp`, `RustGeneratedClasses.cpp` (codegen'd `visitChildren` / `visitOutputConstraints`)
Loading