Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ When implementing JavaScript classes in C++:
Code generation happens automatically as part of the build process. The main scripts are:

- `src/codegen/bindgen.ts` - Generates Rust, C++ & TypeScript bindings from `*.bind.ts` files (`docs/project/bindgen.mdx`)
- `src/codegen/generate-jssink.ts` - Generates stream-related classes
- `src/codegen/bundle-modules.ts` - Bundles built-in modules like `node:fs`
- `src/codegen/bundle-functions.ts` - Bundles global functions like `ReadableStream`

Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ Several code generation scripts are used during Bun's build process. These are r

In particular, these are:

- `./src/codegen/generate-jssink.ts` -- Generates `build/debug/codegen/JSSink.cpp`, `build/debug/codegen/JSSink.h` which implement various classes for interfacing with `ReadableStream`. This is internally how `FileSink`, `ArrayBufferSink`, `"type": "direct"` streams and other code related to streams works.
- `./src/codegen/bindgen.ts` -- Generates Rust, C++ & TypeScript bindings for functions and JavaScriptCore classes implemented in Rust. `**/*.bind.ts` files declare the functions, classes, methods, getters/setters and the types of their arguments; the generator emits the C++ objects, the argument conversions and the calls into Rust. See [Bindgen](/project/bindgen).
- `./src/codegen/cppbind.ts` -- Scans the C++ bindings for functions marked with an export attribute and generates automatic Rust FFI wrappers (`cpp.rs`) for them.
- `./src/codegen/bundle-modules.ts` -- Bundles built-in modules like `node:fs`, `bun:ffi` into files we can include in the final binary. In development, these can be reloaded without rebuilding native code (you still need to run `bun run build`, but it re-reads the transpiled files from disk afterwards). In release builds, these are embedded into the binary.
Expand Down
90 changes: 86 additions & 4 deletions docs/project/bindgen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,13 @@ any other: nothing looks at how many there are, except `.ifPassed`, `requiredArg
| `b.f64` | `f64` | a number; `.loose` is `ToNumber`; see below |
| `b.string` | `&bun_core::String` | a string; `.loose` is `ToString` |
| `b.string.owned` | `bun_core::String` | as `b.string` |
| `b.string.view` | `bun_core::StringView<'a>` | as `b.string` |
| `b.string.utf8` | `&'a [u8]` | the same, and a `String` object |
| `b.string.utf8.owned` | `bun_core::Utf8Bytes<'static>` | as `b.string.utf8` |
| `b.bufferSource` | `JsLocal<'a, BufferSource>` | a typed array, `DataView` or `ArrayBuffer` that is not shared |
| `b.bufferSource.arrayBuffer` | `ExternalShared<JSCArrayBuffer>` | the same; the bytes of a view are copied |
| `b.bytes(options)` | `&'a [u8]`, and others | see [Bytes](#bytes) |
| `b.byteSpan` | `&'a [u8]` | not a JS value: [bytes from C++](#calls-between-c-and-rust) |
| `b.blob` | `ExternalShared<Blob>` | a `Blob` |
| `b.callable` | `JsLocal<'a, Callable>` | a function, or a proxy of one |
| `b.object` | `JsLocal<'a, JSObject>` | an object |
Expand Down Expand Up @@ -263,7 +265,7 @@ Only the promise calls a reaction, with the context it was given, so nothing tes
returns: `None` if the pointer was taken back before the promise settled. No argument after it has a conversion that
throws: nothing would release the pointer. A generic `T` is written as its instantiation.

These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.emptyStringAsNull` and `.ifPassed` apply to it):
These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.falsyAsNull`, `.emptyStringAsNull` and `.ifPassed` apply to it):

- `.optional`: `undefined` means absent; `Option<T>`.
- `.nullable`: `null` or `undefined` means absent; `Option<T>`.
Expand Down Expand Up @@ -329,6 +331,11 @@ it is held by value. The string belongs to the
JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending
one anywhere. `b.string.allowStringObject` also takes a `String` object, as `.utf8` does.

`b.string.view`, which comes after `.allowStringObject`, is for a body that only reads the
characters, in Latin-1 or UTF-16: a `bun_core::StringView<'a>`, and a `&bun_core::StringView<'_>` as an argument of this type alone. It takes no reference to the
storage, and `text.slice(..)` is read in place, where `b.string` would copy it into a new string. The frame holds the JS string, so it is for an argument, alone, in a union or
with a modifier, and not in an array.

A [class](#classes) is a type too: import it from its `.bind.ts` file. As an argument it is a `&'a` of the struct.
The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above except `.default()`.

Expand Down Expand Up @@ -600,13 +607,16 @@ is collected, make the class [`refCounted`](#refcounted).
| `prototypeBase` | `"Error"`: the prototype inherits from `Error.prototype`, and its `name` is the class's |
| `configurable` | `true` makes the native properties configurable |
| `enumerable` | `false` makes the native properties non-enumerable |
| `writable` | `false` makes the native functions read-only. It is not the `writable` of a cached getter |
| `memoryCost` | [`fn memory_cost(&self) -> usize`](#memory), for a heap snapshot |
| `estimatedSize` | [`fn estimated_size(&self) -> usize`](#memory), for the GC |
| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.into_js(heap)`. |
| `call` | `Name(..)` without `new`, which otherwise throws. It is `fn call(..)`, with no receiver. |
| `internalFields` | JS values the object holds |
| `proto` | properties of the prototype |
| `static` | properties of the constructor: native functions and accessors without `cache` |
| `calledFromCpp` | [member functions of `JSName`](#calls-between-c-and-rust) that are defined in Rust |
| `calledFromRust` | [member functions of `JSName`](#calls-between-c-and-rust) that hand-written C++ defines |

Code that has the global object and no capability makes one with `payload.to_js(global)`, which returns a
`JSValue`, or `payload.into_js_wrapper(global)`, which returns the `JsWrapper`.
Expand Down Expand Up @@ -639,6 +649,7 @@ A property of `proto` is one of these.
| `{ getter: T, cache: true \| "field", .. }` | an accessor that returns an [internal field](#internalfields) |
| `{ internalField: "field", setter }` | an accessor that reads the field, and with `setter` stores any value, without calling Rust |
| `{ builtin: true, length }` | the export of that name in `src/js/builtins/<class name>.ts` |
| `{ constant: 3 }` | a read-only integer, like a WebIDL `const` |
| `[{ rustName, args, .. }, ..]` | a native function with [overloads](#overloads) |

The properties are in the order of the schema, which `for-in` and `Object.keys` show. The Rust
Expand All @@ -648,8 +659,8 @@ a native function of the struct (`close` is `do_close` beside a native `close`).
also have the same `rustName`. The getter of `x` is `get_x` and its setter is `set_x`, and a leading
`_` of the property is dropped: `_idleStart` is `get_idle_start`.
`setter: true` gives the setter the getter's `capability`, and `setter: { capability: "js" }` its own. An
accessor of `static` ignores its receiver, so a subclass inherits it. `configurable` on a getter overrides the
class's.
accessor of `static` ignores its receiver, so a subclass inherits it. `configurable` and `enumerable` on a getter
or a constant override the class's.

A key that starts with `@@` is a well-known symbol: `"@@dispose"` defines `prototype[Symbol.dispose]`. The function is
writable, configurable and not enumerable, like the built-in ones, whatever the class says. Its `name` is
Expand Down Expand Up @@ -679,7 +690,7 @@ Every member takes `&self`, a setter too, and mutable state is in `Cell` and `Js
`bun_jsc::JsClass`, so `value.as_class_ref::<Glob>()` works, and `js_Glob::from_js(value)` is the struct if `value`
is an instance. `js_Glob::from_js_direct(value)` returns `None` for an instance of a subclass or one with own
properties, either of which may override a member. `wrapper.payload_ptr()` is the address of the struct, which
never changes.
never changes, and `payload.wrapper_value()` returns the object that owns the struct.

A member with `mutates: true` takes `&mut self`, and so does every setter of its class. JavaScript that ran
during another member could call it on the same object, so no member of that class has `capability: "js"`. The class
Expand All @@ -698,6 +709,20 @@ is not an argument type, has no `estimatedSize`, and does not implement `JsClass

Every field starts empty. A heap snapshot shows what it holds as a property of its name.

In C++ the field is `m_callback`, a `JSC::WriteBarrier<JSC::Unknown>`. A record gives each field a type: `b.any` is
the same field, and `b.cell(class, header)` is a field for hand-written C++ that holds only cells of that class.

```ts
internalFields: { failReason: b.any, pipeDone: b.cell("JSC::JSPromise", "JavaScriptCore/JSPromise.h") },
```

```cpp
JSC::JSPromise* done = sinkController->m_pipeDone.get(); // null if it is empty
```

`js_Name` has no functions for a `b.cell` field, and no getter or `internalField` property uses one. Rust reaches it
through a [`calledFromRust`](#calls-between-c-and-rust) member function.

A getter with `cache: true` declares a field of its own name, with the same functions, and
returns it. While the field is empty the getter calls Rust and stores the result, so emptying
`tag` makes the next read compute it again.
Expand All @@ -706,6 +731,63 @@ returns it. While the field is empty the getter calls Rust and stores the result
With `writable: true` any value assigned to the property replaces the cached one, without calling Rust.
With `setter` and `passThis`, `set_tag` replaces the cached value itself, with `tag_set_cached`.

### Calls between C++ and Rust

Hand-written C++ and the class's Rust code call each other through member functions of the generated class, so neither
side declares an `extern "C"` function for them.

```ts
export const ReadableSinkController = b.class({
proto: {
end: { returns: b.any, capability: "js", throws: true, calledFromCpp: true },
write: { args: { data: SinkChunk }, returns: b.any, capability: "js", throws: true, calledFromCpp: true },
},
calledFromCpp: {
isDetached: { returns: b.bool },
writeBytes: { args: { bytes: b.byteSpan }, returns: b.any, capability: "js", throws: true },
},
calledFromRust: {
onClose: { args: { reason: b.any, sinkClosed: b.bool }, throws: true },
setPipeDone: { args: { done: b.any } },
},
});
```

The generated class declares them:

```cpp
// Defined in Rust. Returns an empty JSValue on exception.
JSC::JSValue end(JSC::JSGlobalObject*);
JSC::JSValue write(JSC::JSGlobalObject*, JSC::JSValue data);
bool isDetached();
JSC::JSValue writeBytes(JSC::JSGlobalObject*, std::span<const uint8_t> bytes);

// Defined in hand-written C++, called from Rust.
void onClose(JSC::JSGlobalObject*, JSC::JSValue reason, bool sinkClosed);
void setPipeDone(JSC::JSValue done);
```

In both directions `globalObject` is the first parameter if the function has a `capability` or `throws`.

`calledFromCpp: true` on a function of `proto` also makes it a member function. It takes a `JSC::JSValue` for each
argument and converts it as a call from JS does, so `sinkController->write(globalObject, chunk)` is
`controller.write(chunk)` without the property lookup. It has no overloads, `.variadic` or `rejects`.

The class's `calledFromCpp` holds functions that only C++ calls and that are not properties. Nothing is converted: an
argument is `b.any`, `b.bool`, a number or `b.byteSpan`, and the result is `b.any`, `b.bool`, a number or nothing.
A function that throws returns `b.any` or nothing.

`b.byteSpan` is bytes that are not in a JS object: a `std::span<const uint8_t>` that Rust gets as `&'a [u8]`.

`calledFromRust` declares member functions that hand-written C++ defines. An argument is `b.any`, `b.bool` or a number, and
the result is `b.any` or nothing. `capability: "js"` is for one that runs JavaScript and does not throw. Rust calls them
in the module `js_ReadableSinkController`, with the object first, and with `throws: true` gets a `JsResult`.

```rust
js_ReadableSinkController::on_close(this_value, global, reason, true)?;
js_ReadableSinkController::set_pipe_done(this_value, done);
```

### `refCounted`

With `refCounted: true` the struct is a separate, refcounted heap allocation and the object holds one ref:
Expand Down
1 change: 0 additions & 1 deletion docs/project/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ A full debug build can take a few minutes when Rust or C++ has changed; cargo's

Bun's build process runs several code generation scripts automatically when certain files change:

- `./src/codegen/generate-jssink.ts` -- Generates `build/debug/codegen/JSSink.cpp`, `build/debug/codegen/JSSink.h` which implement various classes for interfacing with `ReadableStream`. This is internally how `FileSink`, `ArrayBufferSink`, `"type": "direct"` streams and other code related to streams work.
- `./src/codegen/bindgen.ts` -- Generates Rust, C++ & TypeScript bindings for functions and JavaScriptCore classes implemented in Rust. `**/*.bind.ts` files declare the functions, classes, methods, getters/setters and the types of their arguments; the generator emits the C++ objects, the argument conversions and the calls into Rust. See [Bindgen](/project/bindgen).
- `./src/codegen/cppbind.ts` -- Scans the C++ bindings for functions marked with an export attribute and generates automatic Rust FFI wrappers (`cpp.rs`) for them.
- `./src/codegen/bundle-modules.ts` -- Bundles built-in modules like `node:fs`, `bun:ffi` into files included in the final binary. In development, these can be reloaded without rebuilding native code (you still need to run `bun run build`, but it re-reads the transpiled files from disk afterwards). In release builds, these are embedded into the binary.
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
// Each Rust crate produces an rlib the link takes beside the C/C++
// objects. Rust `include!`s codegen
// `.rs` outputs (written as side effects of the bindgen /
// bundle-modules / generate-jssink edges), so the codegen output set
// bundle-modules edges), so the codegen output set
// is forwarded to order the workspace crates after it (order-only; the
// crates' dep-info then tracks exactly the files they read).
// lol-html is a direct path dep of `bun_runtime`/`bun_bundler`
Expand Down
46 changes: 2 additions & 44 deletions scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
* of the step one build late. That is why emitBindgen declares
* the per-file Generated*.h headers and not just the .cpp that gets compiled.
*
* Files nothing compiles may stay undeclared (bundle-modules' eval/ dir,
* JSSink.lut.txt consumed within its own step). The .d.ts files src/js is
* Files nothing compiles may stay undeclared (bundle-modules' eval/ dir). The .d.ts files src/js is
* typechecked against are declared, in cfg.typesDir: see `generatedTypes`. The remaining
* #included exception is BunBuiltinNames+extras.h from bundle-functions.ts,
* reached through the PCH.
Expand Down Expand Up @@ -115,9 +114,7 @@ export function registerCodegenRules(n: Ninja, cfg: CodegenFields): void {
// create-hash-table.ts) read these so a cross-compiled binary doesn't
// ship with the build host's platform baked in.
//
// restat = 1 because most scripts use writeIfNotChanged(). Scripts that
// don't (generate-jssink) always write → restat is a no-op for
// them, no harm.
// restat = 1 because the scripts use writeIfNotChanged().
const env = hostWin
? `set TARGET_PLATFORM=${platform}&& set TARGET_ARCH=${arch}&& `
: `TARGET_PLATFORM=${platform} TARGET_ARCH=${arch} `;
Expand Down Expand Up @@ -326,7 +323,6 @@ export function emitCodegen(n: Ninja, cfg: CodegenFields, sources: Sources): Cod
emitJsModules(ctx);
emitBakeCodegen(ctx);
emitBindgen(ctx);
emitJsSink(ctx);
emitObjectLuts(ctx);
emitLintDocs(ctx);
emitCompressedEmbeds(ctx);
Expand Down Expand Up @@ -986,44 +982,6 @@ function crateOf(file: string): string {
throw new BuildError(`${file} is not inside a Cargo package`);
}

function emitJsSink({ n, cfg, o, dirStamp }: Ctx): void {
const script = resolve(cfg.cwd, "src", "codegen", "generate-jssink.ts");
const hashTableScript = resolve(cfg.cwd, "src", "codegen", "create-hash-table.ts");
const perlScript = resolve(cfg.cwd, "src", "codegen", "create_hash_table");

// generate-jssink.ts writes JSSink.{cpp,h,lut.txt} + generated_jssink.rs (the
// Rust `#[no_mangle]` thunks), then calls create-hash-table.ts in-process to
// convert .lut.txt → .lut.h. So all of {cpp,h,lut.h,rs} are outputs of this
// one step (.lut.txt is really an intermediate — we don't expose it).
const jssinkRs = resolve(cfg.codegenDir, "generated_jssink.rs");
const outputs = [
resolve(cfg.codegenDir, "JSSink.cpp"),
resolve(cfg.codegenDir, "JSSink.h"),
resolve(cfg.codegenDir, "JSSink.lut.h"),
jssinkRs,
];

n.build({
outputs,
rule: "codegen",
inputs: [script, hashTableScript, perlScript],
orderOnlyInputs: [dirStamp],
vars: {
cwd: cfg.cwd,
desc: "JSSink.{cpp,h,lut.h,rs}",
args: shJoin(cfg, [script, cfg.codegenDir]),
},
});

o.all.push(...outputs);
o.cppSources.push(outputs[0]!); // .cpp
o.cppHeaders.push(outputs[1]!, outputs[2]!); // .h + .lut.h
// bun_runtime `include!`s generated_jssink.rs, so the workspace crate edges
// must order after this codegen step — `rustInputs` is the list they wait
// on (same as generated_host_exports).
o.rustInputs.push(jssinkRs);
}

/**
* LUT sources → .lut.h outputs. One build statement PER pair, because the
* script takes a single (src, out) pair.
Expand Down
2 changes: 1 addition & 1 deletion scripts/jsc-exception-lint/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function bunSourceFiles(entries: Entry[]): string[] {
);
}

// Generated C++ (Generated*.cpp, JSSink.cpp, ...). Summarized so
// Generated C++ (Generated*.cpp, ...). Summarized so
// calls into it are classified from its bodies; not analyzed, the generators
// own that code.
function generatedSourceFiles(entries: Entry[]): string[] {
Expand Down
Loading
Loading