Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a100b97
Add b.stringOrBuffer, b.u64.asBigInt and a namespace that can be called
dylan-conway Oct 16, 2026
edebc56
Define the sync zlib and zstd functions, Bun.hash, Bun.password and r…
dylan-conway Oct 16, 2026
65479a9
Define the TOML, JSON5, JSONC, YAML and XML namespaces with the bindg…
dylan-conway Oct 16, 2026
f64d067
Test and document b.stringOrBuffer, JsHeap::global and what the porte…
dylan-conway Oct 16, 2026
fbf6001
Put error_with_custom_code next to error_with_code
dylan-conway Oct 16, 2026
25cc2c6
Refuse an S3 file as bytes, use JSUint8Array::from_bytes, and apply t…
dylan-conway Oct 16, 2026
7cf7755
Fix what the review found: inputs of 2 GiB, YAML.parse when making th…
dylan-conway Oct 16, 2026
66abc40
zstdDecompress takes no options, as zstdDecompressSync
dylan-conway Oct 16, 2026
0124b56
Skip the 2 GiB shared buffer test where memory is short, and pin the …
dylan-conway Oct 16, 2026
156ea01
Merge branch 'main' of github.com:oven-sh/bun into claude/bindgen-byt…
dylan-conway Oct 16, 2026
0b58d36
Report the 2 GiB shared buffer test as skipped where memory is short
dylan-conway Oct 16, 2026
f5a6edf
Bun.hash refuses undefined and null
dylan-conway Oct 17, 2026
c0236e5
Merge branch 'main' of github.com:oven-sh/bun into claude/bindgen-byt…
dylan-conway Oct 17, 2026
861dda2
b.stringOrBuffer borrows the bytes of a shared buffer
dylan-conway Oct 17, 2026
e9efef5
TOML.parse and YAML.parse copy the bytes of a shared buffer
dylan-conway Oct 17, 2026
4a58c2a
b.bytes takes named options, and YAML, XML, JSONC, async zstd and asy…
dylan-conway Oct 17, 2026
d8f6884
b.bytes is the one way to take bytes, the schema states the handle, a…
dylan-conway Oct 17, 2026
fc76b3d
A string argument that was not converted on the heap frees nothing
dylan-conway Oct 17, 2026
cb03054
Merge branch 'main' into claude/bindgen-bytes-input
dylan-conway Oct 17, 2026
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
56 changes: 54 additions & 2 deletions docs/project/bindgen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export const semver = b.namespace({
a `PropertyCallback` of a static property table, so the object is made when the property is
first read.

A namespace that can itself be called, like `Bun.hash`, names the function that a call runs:
`b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a
function named after the namespace.

### What a function may do: `capability`

What a function can do to the JS heap is a parameter it is given, so it is
Expand All @@ -113,9 +117,23 @@ The compiler holds a body to its capability as far as this API goes. The older A
other ways to a `&JSGlobalObject` (`VirtualMachine::get().global()`), and a body that
takes one of those to run JavaScript has stepped around the check.

Much of the older API takes a `&JSGlobalObject` only to allocate or to throw. A `"heap"`
function calls it with `heap.global()`, which is `unsafe`: nothing the global object is passed
to may run JavaScript, and the compiler cannot see that. Read what is called all the way down
before writing the `SAFETY` comment. `ToString` of an object, a property that is looked up or
set and not defined, and a call to a constructor found on the global object all run JavaScript.
Throwing tells the debugger, which can run code, so after a throw the body returns without
reading what it borrowed.

```rust
// SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are
// defined and not set.
let global = unsafe { heap.global() };
```

`throws: true` makes the return type a `Result`. The error is a
`bindgen::Error<'a>`, made by `heap.error(..)`, `heap.type_error(..)`,
`heap.range_error(..)`, `heap.error_with_code(ErrorCode::INVALID_ARG_TYPE, ..)` or
`heap.range_error(..)`, `heap.error_with_code(ErrorCode::OUT_OF_RANGE, ..)`, `heap.invalid_arguments(..)` or
`bindgen::Error::from_value(value)` (`?` makes one of a `JsError` or an `AllocError`), or any type that
implements `bindgen::IntoError` (`bun_sys::Error`, `SystemError`, `JsError` and a `Box` of
one do), which lets a `capability: "none"` function fail too.
Expand Down Expand Up @@ -175,6 +193,7 @@ any other: nothing looks at how many there are.
| `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) |
| `b.bufferSource.bytes.allowShared` | `PossiblySharedBytes<'a>` | the same, and a shared one |
| `b.bufferSource.arrayBuffer` | `ExternalShared<JSCArrayBuffer>` | the same; the bytes of a view are copied |
| `b.stringOrBuffer` | `&'a [u8]` | see [A string or a buffer](#a-string-or-a-buffer) |
| `b.blob` | `ExternalShared<Blob>` | a `Blob` |
| `b.any` | `JsLocal<'a>` | anything |
| `b.null` | `()` | `null` or `undefined`; only in a union |
Expand All @@ -199,7 +218,10 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so

As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that
is `IntoIterator`. The body of a `b.fn` or of a namespace's function whose bytes do not outlive
it returns the string itself, from `heap.create_utf8_string(bytes)`.
it returns the string itself, from `heap.create_utf8_string(bytes)`. `b.u64.asBigInt` is a
result only: a `u64` that is always returned as a BigInt. `b.bufferSource` as a result is what
`heap.create_uninitialized_uint8_array(length)`, `heap.create_uint8_array_from_box(bytes)` or
`heap.create_buffer_from_box(bytes)` made. The last two take over the allocation.

### Numbers

Expand Down Expand Up @@ -327,6 +349,36 @@ way from it to a slice. What reads the bytes is a method of it, written for a by
differ each time it is read: `index_of_char` hands the pointer to a kernel that uses nothing
it reads as an index or a length. Such an argument is always there: it is not `.optional`.

### A string or a buffer

Many functions take "a string or a buffer" and want its bytes. `b.stringOrBuffer` is a string
or a `String` object, as UTF-8, or the bytes of a typed array, `DataView`, `ArrayBuffer` or
`SharedArrayBuffer`. The body gets `&'a [u8]`.

- `.orBlob`: the bytes of a `Blob` too. One whose bytes are not in memory, a file here or in S3, throws.
- `.loose`: anything else is `ToString`. Without it, anything else throws.
- `.loose.notNullish`: but `undefined` and `null` throw.

```ts
export const gzipSync = b.fn({
args: { data: b.stringOrBuffer, options: ZlibCompressionOptions.default({}) },
returns: b.bufferSource,
capability: "heap",
throws: true,
});
```

```rust
fn gzip_sync<'a>(heap: &JsHeap<'a>, data: &[u8], options: ZlibCompressionOptions<'a>) -> bindgen::Result<'a, JsLocal<'a, BufferSource>>
```

A string that is not ASCII is converted. Everything else is borrowed from where it is, the
bytes of a shared buffer too: unlike `.bytes` it takes one, and another thread that writes it
during the call changes what the body reads. Like `.bytes` it is for an argument of a function that cannot run JavaScript,
its kind is checked in the order of the arguments, and a buffer's pointer and length are read
after every argument is converted. It is always there: it is not `.optional`. `.withValue` tells
a string from a buffer, or `undefined` from `"undefined"` under `.loose`.

### Keeping the original value

A converted argument is for use now. To hand the value to something that holds
Expand Down
20 changes: 11 additions & 9 deletions src/codegen/bindgen-emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,11 @@ namespace Bun::Bindgen::Generated::${origin.namespace} {
${cppNamespaceSignature(namespace)}
{
JSC::JSGlobalObject* globalObject = owner->globalObject();
JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min<unsigned>(${namespace.functions.length}, JSC::JSFinalObject::maxInlineCapacity));
JSC::JSObject* object = ${
namespace.call
? `JSC::JSFunction::create(vm, globalObject, ${namespace.call.length}, ${asciiLiteral(namespace.name)}, ${hostFunctionName(namespace.call)}, JSC::ImplementationVisibility::Public)`
: `JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min<unsigned>(${namespace.functions.length}, JSC::JSFinalObject::maxInlineCapacity))`
};
${namespace.functions
.map(
fn =>
Expand Down Expand Up @@ -543,20 +547,18 @@ function cppDeclareConverted(type: Type, variable: string, value: string, ctx: s
/** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */
function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string {
const lines: string[] = [];
const rereads: string[] = [];
// Converting a later argument can run JavaScript that detaches or resizes a buffer.
const readLast: string[] = [];
signature.args.forEach(({ name, type }, i) => {
lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->argument(${i});`);
lines.push(`[[maybe_unused]] auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`);
// Converting a later argument can run JavaScript that detaches or resizes the buffer.
if (type.borrowsHeap && i < signature.args.length - 1)
rereads.push(
`converted${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`,
`scope.assertNoException();`,
);
readLast.push(
...type.cppReadLast(`converted${i}`, `argument${i}.value()`, `ctx${i}`, name, i === signature.args.length - 1),
);
lines.push(...cppDeclareConverted(type, `converted${i}`, `argument${i}.value()`, `ctx${i}`, name));
if (type.conversionThrows) lines.push(`RETURN_IF_EXCEPTION(scope, {});`);
});
lines.push(...rereads);
lines.push(...readLast);
const args = signature.args.map(({ type }, i) => {
return type instanceof WithValueType
? `${externType(type)} { ${toExtern(`converted${i}`)}, JSC::JSValue::encode(argument${i}.value()) }`
Expand Down
123 changes: 115 additions & 8 deletions src/codegen/bindgen-lib-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export abstract class Type {
get holdsJSValue(): boolean {
return this.dependencies.some(type => type.holdsJSValue);
}
/** Points into memory JavaScript can detach, resize or free: not for a function that can run it. */
/** Points into memory JavaScript can detach, resize or free: not for a function that can run it, and only for an argument, read once all are converted. */
get borrowsHeap(): boolean {
return this.dependencies.some(type => type.borrowsHeap);
}
Expand Down Expand Up @@ -240,6 +240,11 @@ export abstract class Type {
cppConvert(value: string, ctx: string, name: string): string {
return `Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`;
}
/** C++ lines for once every argument is converted. They read again into `target`, converted from `value`, what converting a later argument could have changed. */
cppReadLast(target: string, value: string, ctx: string, name: string, isLastArgument: boolean): string[] {
if (!this.borrowsHeap || isLastArgument) return [];
return [`${target} = ${this.cppConvert(value, ctx, name)};`, `scope.assertNoException();`];
}
/** Large, or holds something large: built where it ends up, not returned and moved there. */
get convertsInPlace(): boolean {
return false;
Expand Down Expand Up @@ -407,11 +412,33 @@ export class IntegerType extends PrimitiveType {
this.#refusesBigInt(".validateInteger()");
return new RangeType(this.info, min, max, "validateInteger");
}
/** As a result: a BigInt, which holds every `u64`. */
get asBigInt(): BigIntResultType {
assert(this.info.rust === "u64" && this.#conversion === "Strict" && !this.#allowBigInt, ".asBigInt follows b.u64");
return new BigIntResultType();
}
#refusesBigInt(modifier: string): void {
assert(!this.#allowBigInt, `${modifier} refuses a BigInt, which .allowBigInt asked for`);
}
}

export class BigIntResultType extends Type {
readonly idlType = "";
readonly tsType = "bigint";
layout(): Layout {
return { size: 8, align: 8 };
}
rustExternType(): string {
return "u64";
}
rustType(): string {
return "u64";
}
rustToJS(value: string): string {
return `bindgen::to_js::u64_as_bigint(heap, ${value})`;
}
}

export class RangeType extends IntegerType {
readonly modifier: "enforceRange" | "validateInteger";
readonly #range: readonly [number, number];
Expand Down Expand Up @@ -692,6 +719,66 @@ export class PossiblySharedBytesType extends BytesType {
}
}

/**
* A string, as UTF-8, or the bytes of a typed array, `DataView`, `ArrayBuffer` or `SharedArrayBuffer`, as `&[u8]`. A buffer's
* are where they are, a shared one's too, which another thread can write during the call.
*/
export class StringOrBufferType extends Type {
readonly conversion: "Strict" | "Loose" | "LooseNotNullish";
readonly allowBlob: boolean;
constructor(conversion: StringOrBufferType["conversion"] = "Strict", allowBlob = false) {
super();
this.conversion = conversion;
this.allowBlob = allowBlob;
}
/** `ToString` of anything else. */
get loose(): StringOrBufferType {
return new StringOrBufferType("Loose", this.allowBlob);
}
/** After `.loose`: `undefined` and `null` are refused, and not `"undefined"` and `"null"`. */
get notNullish(): StringOrBufferType {
assert(this.conversion !== "Strict", ".notNullish follows .loose: without it they are refused already");
return new StringOrBufferType("LooseNotNullish", this.allowBlob);
}
/** Also the bytes of a `Blob`. One whose bytes are not in memory, a file, throws. */
get orBlob(): StringOrBufferType {
return new StringOrBufferType(this.conversion, true);
}
get idlType(): string {
return `Bun::Bindgen::IDL${this.conversion}StringOrBuffer<Bun::Bindgen::AllowBlob::${this.allowBlob ? "Yes" : "No"}>`;
}
get tsType(): string {
return `string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView${this.allowBlob ? " | Blob" : ""}`;
}
get borrowsFrame(): boolean {
return true;
}
get borrowsHeap(): boolean {
return true;
}
get convertsInPlace(): boolean {
return true;
}
cppConvertInto(target: string, value: string, ctx: string): string {
return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`;
}
cppReadLast(target: string, value: string): string[] {
return [`${target}.read(${value});`];
}
layout(): Layout {
return { size: 16, align: 8 };
}
rustExternType(): string {
return "bindgen::extern_types::ExternBytes";
}
rustType(): string {
return "&'a [u8]";
}
rustFromExtern(expr: string): string {
return `${expr}.get(heap)`;
}
}

/** A typed array, `DataView` or `ArrayBuffer`, as a handle whose bytes are read through `with_bytes`. */
export class BufferSourceType extends Type {
readonly idlType = "Bun::Bindgen::IDLRawBufferSource";
Expand Down Expand Up @@ -779,6 +866,9 @@ export class WithValueType extends Type {
cppConvert(value: string, ctx: string, name: string): string {
return this.inner.cppConvert(value, ctx, name);
}
cppReadLast(target: string, value: string, ctx: string, name: string, isLastArgument: boolean): string[] {
return this.inner.cppReadLast(target, value, ctx, name, isLastArgument);
}
layout(): Layout {
return structLayout([this.inner.layout(), { size: 8, align: 8 }]);
}
Expand Down Expand Up @@ -896,6 +986,7 @@ export class OptionalType extends Type {
assert(!(inner instanceof WithValueType), "put .withValue last");
assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`);
assert(!(inner instanceof PossiblySharedBytesType), ".allowShared is for an argument that is always there");
assert(!(inner instanceof StringOrBufferType), "b.stringOrBuffer is for an argument that is always there");
this.inner = inner;
this.modifier = modifier;
}
Expand Down Expand Up @@ -1079,7 +1170,7 @@ export class ArrayType extends Type {
assert(!(element instanceof WithValueType), ".withValue is only for an argument");
assert(
!element.borrowsHeap,
"b.bufferSource.bytes is only for an argument: it is read after everything else is converted",
"the bytes of a buffer are only for an argument: they are read after everything else is converted",
);
assert(!(element instanceof UnitType), "an array of nothing but null");
assert(
Expand Down Expand Up @@ -1292,7 +1383,7 @@ export class DictionaryType extends NamedType {
);
assert(
!this.borrowsHeap,
"b.bufferSource.bytes is only for an argument: it is read after everything else is converted",
"the bytes of a buffer are only for an argument: they are read after everything else is converted",
);
assert(
!this.fromJS || !this.borrowsFrame,
Expand Down Expand Up @@ -1387,7 +1478,7 @@ export class UnionType extends NamedType {
);
assert(
!this.borrowsHeap,
"b.bufferSource.bytes is only for an argument: it is read after everything else is converted",
"the bytes of a buffer are only for an argument: they are read after everything else is converted",
);
for (const { name, type } of this.alternatives) {
assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`);
Expand Down Expand Up @@ -1506,11 +1597,14 @@ export class Signature {
`length: ${this.length} is not a count`,
);
for (const { name, type } of this.args) {
assert(!isMadeOf(type, BigIntResultType), `${name}: .asBigInt is a result; an argument is b.u64.allowBigInt`);
assert(
!type.borrowsHeap || this.capability !== "js",
type instanceof PossiblySharedBytesType
isMadeOf(type, PossiblySharedBytesType)
? `${name}: JavaScript can detach a buffer; .allowShared is for a function that cannot run it`
: `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`,
: isMadeOf(type, StringOrBufferType)
? `${name}: JavaScript can detach a buffer; b.stringOrBuffer is for a function that cannot run it`
: `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`,
);
}
assert(!(this.returns instanceof DefaultType), "a result has no default");
Expand All @@ -1525,6 +1619,11 @@ export class Signature {
}
}

/** `type` is a `kind`, or something it is made of is. */
function isMadeOf(type: Type, kind: new (...args: never[]) => Type): boolean {
return type.dependencies.some(dependency => isMadeOf(dependency, kind)) || type instanceof kind;
}

/** Call a signature, or look at the kind of one more argument. */
export type OverloadDispatch = Signature | { argument: number; byKind: Map<ValueKind | "other", OverloadDispatch> };

Expand Down Expand Up @@ -1802,11 +1901,17 @@ export class ClassType extends NamedType {
}
}

/** A plain object whose properties are native functions, as WebIDL's `namespace`. */
export interface NamespaceOptions {
/** The object is itself a function, named as the namespace is, that does what this one of its functions does. */
call?: string;
}

/** A plain object whose properties are native functions, as WebIDL's `namespace`. With `call`, a function that has them. */
export class NamespaceDefinition {
readonly functions: readonly FunctionDefinition[];
readonly call: FunctionDefinition | undefined;
name = "";
constructor(functions: Record<string, SignatureOptions>) {
constructor(functions: Record<string, SignatureOptions>, options: NamespaceOptions = {}) {
this.functions = Object.entries(functions).map(([name, options]) => {
assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`);
assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`);
Expand All @@ -1815,6 +1920,8 @@ export class NamespaceDefinition {
return fn;
});
assert(this.functions.length > 0, "a namespace with no functions");
this.call = this.functions.find(fn => fn.name === options.call);
assert(options.call === undefined || this.call, `call: ${options.call} is not one of its functions`);
}
}

Expand Down
Loading
Loading