Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
70336ef
bindgen: move every Rust function that takes a `CallFrame` to schemas…
dylan-conway Oct 22, 2026
b2343d6
Keep `ERR_MISSING_ARGS` for `URL.revokeObjectURL()` and test `Bun.res…
dylan-conway Oct 22, 2026
acd9aa8
Name the schema `BunTest.bind.ts` so that a release build has it, and…
dylan-conway Oct 23, 2026
d343a1c
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 23, 2026
dac641d
bindgen: add `b.value`, make functions through one helper, and name s…
dylan-conway Oct 23, 2026
13b5a30
Merge remote-tracking branch 'origin/claude/bindgen-functions' into c…
dylan-conway Oct 23, 2026
4c5877f
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 23, 2026
432b725
Write `isValue` as a plain field so that Node runs the generator, and…
dylan-conway Oct 23, 2026
2355e41
Take a `BuildArtifact` in `URL.createObjectURL` as `main` does, free …
dylan-conway Oct 23, 2026
417eda0
Type the arguments and results of the moved functions in their schema…
dylan-conway Oct 23, 2026
7de0761
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 23, 2026
28d4d75
Add `.otherAsNull` for what is ignored unless it is of the type, name…
dylan-conway Oct 23, 2026
9262d0d
Make `.otherAsNull` go by the kind of a value, parse an enumeration b…
dylan-conway Oct 23, 2026
dc4c827
Borrow a dictionary argument that can be absent, skip the markdown op…
dylan-conway Oct 23, 2026
9d18931
Keep how an argument reaches the body with its type, and say what to …
dylan-conway Oct 23, 2026
52f16cf
Have a conversion say by what it returns that it threw, compare one s…
dylan-conway Oct 23, 2026
acb5a41
Refuse `b.null` outside a union, a range under a modifier in an array…
dylan-conway Oct 23, 2026
86b82ef
Escape a quote or a backslash in the switch label of an enumeration's…
dylan-conway Oct 23, 2026
5745562
Take `Array.prototype` as an array, as `Array.isArray` does
dylan-conway Oct 23, 2026
ac7f2ba
Expect `Array.prototype` as a tls option to be an empty array, as in …
dylan-conway Oct 23, 2026
3fcb01a
Add `requiredArguments`, which throws `ERR_MISSING_ARGS` for a shorte…
dylan-conway Oct 23, 2026
59f808f
Report `URL.createObjectURL()` as a wrong type, as Node does
dylan-conway Oct 23, 2026
4bd1e3a
Count `requiredArguments` in `.length` and the TypeScript types, and …
dylan-conway Oct 23, 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
19 changes: 12 additions & 7 deletions docs/project/bindgen.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ The generator lives in `src/codegen/bindgen*.ts`, the Rust it generates against
`src/jsc/bindgen.rs`, and the C++ in `src/jsc/bindings/Bindgen/` and
`src/jsc/bindings/BunIDL*.h`.

The body of a binding is always Rust. These do overlapping work:

- "JS2Native", allowing ad-hoc calls from `src/js` to native code (`$rust`, `$cpp`).
- `#[bun_jsc::host_fn]` functions, which take a `CallFrame`.
The body of a binding is always Rust. `$cpp` and `$newCppFunction` reach a function written in C++ from `src/js`.

## Functions

Expand Down Expand Up @@ -79,7 +76,15 @@ not compile.
To reach the function:

- from `src/js`: `$bindgen("bindgen_test.bind.ts", "add")`, which is typed from the schema. An argument's type is what it takes, so one that coerces (`.loose`, `.wrap`, `.enforceRange()`) is `any`. A `.nullable` result, and a `.nullable` getter without a setter, is `T | null`;
- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`.
- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`;
- from Rust, or from C++ that compares host functions: `export: "Bun__CronJob__onPromiseResolve"` gives the host function C
linkage under that name. The generated header declares it, and in Rust it is a `JSHostFn`,
`crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`.

`passThis: true` adds `this_value: JsLocal<'a>` before the arguments, and `passCallee: true` adds `callee: JsLocal<'a>`, the
function that was called, after it. Neither goes with overloads.

The caller's source location is `global.get_caller_src_loc()`: inside a host function `vm.topCallFrame` is its own frame.

### Namespaces

Expand All @@ -99,7 +104,7 @@ export const semver = b.namespace({

`Bun::Bindgen::Generated::SemverObject::createSemver` makes the object. It has the signature of
a `PropertyCallback` of a static property table, so the object is made when the property is
first read.
first read. `$bindgen("SemverObject.bind.ts", "semver")` makes one for `src/js`.

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
Expand Down Expand Up @@ -316,7 +321,7 @@ The second argument of `b.dictionary` takes:
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowArray` | an array is read like any other object, not refused |
| `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default |
| `fromJS` | also generates `Name::from_js(global, value)`, for a `#[host_fn]` body; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` |
| `fromJS` | also generates `Name::from_js(global, value)`, for a body that has a `JSValue`; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` |
| `nullPrototype` | as a result, the object has no prototype, and the inline room of `Object.create(null)` |

### Enumerations
Expand Down
9 changes: 3 additions & 6 deletions scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,13 +814,10 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void {
resolve(cfg.codegenDir, "SyntheticModuleType.h"),
resolve(cfg.codegenDir, "BuiltinModuleKeys.h"),
resolve(cfg.codegenDir, "GeneratedJS2Native.h"),
// Rust sibling: include!()'d by src/runtime/generated_js2native.rs. Must be
// a declared output: bun_runtime's dep-info names it, and ninja can only
// rebuild that crate in the same build as this step if it knows the file.
resolve(cfg.codegenDir, "generated_js2native.rs"),
// Specifier → module-ID tag table: include!()'d by the
// `resolved_source_tag` module in src/jsc/lib.rs. Declared for the same
// reason as generated_js2native.rs.
// `resolved_source_tag` module in src/jsc/lib.rs. Must be a declared
// output: bun_jsc's dep-info names it, and ninja can only rebuild that
// crate in the same build as this step if it knows the file.
resolve(cfg.codegenDir, "generated_resolved_source_tag.rs"),
// Canonical builtin key -> BuiltinModuleKeys.h index: include!()'d by `builtin_module_key_index` in src/jsc/lib.rs.
resolve(cfg.codegenDir, "generated_builtin_module_key_index.rs"),
Expand Down
2 changes: 0 additions & 2 deletions src/cares_sys/c_ares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1869,8 +1869,6 @@ pub const ARES_SOCKET_BAD: ares_socket_t = usize::MAX; // INVALID_SOCKET
#[cfg(not(windows))]
pub const ARES_SOCKET_BAD: ares_socket_t = -1;

// Bun__canonicalizeIP_ host fn: see bun_runtime::dns_jsc::cares_jsc

/// Creates a sockaddr structure from an address, port.
///
/// # Parameters
Expand Down
100 changes: 70 additions & 30 deletions src/codegen/bindgen-emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,9 @@ export function cppHeader(file: SchemaFile): string {
...file.namespaces.map(cppNamespaceSignature),
...file.types.filter(type => type instanceof ClassType && type.construct).map(cppConstructorSignature),
];
const exported = file.functions.filter(fn => fn.exportName);
const declarations = [
...file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`),
...file.functions.filter(fn => !fn.exportName).map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`),
...propertyCallbacks.map(signature => `// A PropertyCallback of a static property table.\n${signature};`),
];
return `${banner(file.origin)}#pragma once
Expand All @@ -402,6 +403,8 @@ export function cppHeader(file: SchemaFile): string {
#include "JSDOMConvertDictionary.h"
#include "JSDOMConvertEnumeration.h"
${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types.map(cppHeaderForType).join("")}${
exported.length > 0 ? `\n${exported.map(fn => `BUN_DECLARE_HOST_FUNCTION(${fn.exportName});`).join("\n")}\n` : ""
}${
declarations.length > 0
? `
namespace Bun::Bindgen::Generated::${file.origin.namespace} {
Expand All @@ -412,7 +415,7 @@ ${declarations.join("\n")}
}`;
}

const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name.replace(/^_+/, ""))}`;
const hostFunctionName = (fn: FunctionDefinition) => fn.exportName ?? `js${cap(fn.name.replace(/^_+/, ""))}`;

const cppNamespaceSignature = (namespace: NamespaceDefinition) =>
`JSC::JSValue create${cap(namespace.name)}(JSC::VM& vm, JSC::JSObject* owner)`;
Expand All @@ -439,6 +442,10 @@ ${namespace.functions
return object;
}
}
extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${origin.stem}_${namespace.name}(Zig::GlobalObject* globalObject)
{
return JSC::JSValue::encode(Bun::Bindgen::Generated::${origin.namespace}::create${cap(namespace.name)}(JSC::getVM(globalObject), globalObject));
}
`;
}

Expand Down Expand Up @@ -1151,15 +1158,21 @@ const offsetOfPayload = (type: ClassType) => offsetOfInternalField(type.internal
const maxCellSize = 8000;

/** Defines the host function `name`. `takeThis` declares `thisObject`, which is then the thunks' first argument. */
/** `passThis` and `passCallee`, which come before the arguments. */
const cppLeadingOfFunction = (fn: FunctionDefinition) => [
...(fn.passThis ? ["JSC::JSValue::encode(callFrame->thisValue())"] : []),
...(fn.passCallee ? ["JSC::JSValue::encode(callFrame->jsCallee())"] : []),
];

function cppHostFunction(
name: string,
fn: FunctionDefinition,
symbol: (signature: Signature) => string,
takeThis = "",
): string {
const leading = takeThis ? ["thisObject"] : [];
const leading = takeThis ? ["thisObject"] : cppLeadingOfFunction(fn);
if (fn.signatures.length === 1) {
return `JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
return `${fn.exportName ? "BUN" : "JSC"}_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
${cppPrologue}
${takeThis}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)}
Expand Down Expand Up @@ -1199,14 +1212,15 @@ ${takeThis}${indent(cppDispatch(fn.dispatch), 4)}

function cppSourceForFunction(fn: FunctionDefinition): string {
const name = hostFunctionName(fn);
const leading = cppLeadingOfFunction(fn).map(() => "JSC::EncodedJSValue");
assert(fn.signatures.length === 1 || (leading.length === 0 && !fn.exportName), `${fn.name}: not with overloads`);
const hostFunction = cppHostFunction(name, fn, signature => thunkSymbol(fn, signature));
return `
${fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature)).join("\n")}
namespace Bun::Bindgen::Generated::${fn.origin!.namespace} {
${cppHostFunction(name, fn, signature => thunkSymbol(fn, signature))}
}
${fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature, leading)).join("\n")}
${fn.exportName ? hostFunction : `namespace Bun::Bindgen::Generated::${fn.origin!.namespace} {\n${hostFunction}\n}`}
extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject)
{
return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}, JSC::ImplementationVisibility::Public));
return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, ${fn.exportName ?? `Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}`}, JSC::ImplementationVisibility::Public));
}
`;
}
Expand Down Expand Up @@ -1306,7 +1320,7 @@ function rustLayoutAssert(rustType: string, type: Type): string {
}

/** Names the generated thunks use themselves. */
const reservedArgumentNames = new Set(["global", "this", "structure", "cx", "heap", "result"]);
const reservedArgumentNames = new Set(["global", "this", "this_value", "callee", "structure", "cx", "heap", "result"]);

function rustArgumentName(arg: { name: string }): string {
const name = snakeCase(arg.name);
Expand Down Expand Up @@ -1832,27 +1846,38 @@ ${

function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string {
const scope = file.origin;
return fn.signatures
.map(signature => {
let path = signature.rustPath;
if (!path) {
assert(fn.signatures.length === 1, `${fn.name}: each overload has a rustPath of its own`);
assert(
file.moduleDefinition,
`${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`,
);
path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`;
}
const call = `${path}(${[...capabilityArgument[signature.capability], ...rustArguments(signature)].join(", ")})`;
return rustThunk(
thunkSymbol(fn, signature),
rustExternParameters(signature, scope),
"JSValue",
`${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws, signature.returns)};
const leading = [...(fn.passThis ? ["this_value"] : []), ...(fn.passCallee ? ["callee"] : [])];
const hostFunction = fn.exportName
? `
bun_jsc::jsc_abi_extern! {
pub fn ${fn.exportName}(global: *mut JSGlobalObject, frame: *mut bun_jsc::CallFrame) -> JSValue;
}
`
: "";
return (
hostFunction +
fn.signatures
.map(signature => {
let path = signature.rustPath;
if (!path) {
assert(fn.signatures.length === 1, `${fn.name}: each overload has a rustPath of its own`);
assert(
file.moduleDefinition,
`${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`,
);
path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`;
}
const call = `${path}(${[...capabilityArgument[signature.capability], ...leading, ...rustArguments(signature)].join(", ")})`;
return rustThunk(
thunkSymbol(fn, signature),
[...leading.map(name => `${name}: JSValue`), ...rustExternParameters(signature, scope)],
"JSValue",
`${leading.map(name => `let ${name} = bindgen::JsLocal::new(&cx.heap, ${name});\n`).join("")}${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws, signature.returns)};
bindgen::host_fn_return(cx, result, |heap, value| ${signature.returns.rustToJS("value", scope)})`,
);
})
.join("");
);
})
.join("")
);
}

/** Once each: two classes of a file may name the same trait. */
Expand Down Expand Up @@ -1978,6 +2003,21 @@ ${files
.map(file => ` ${JSON.stringify(`${file.origin.stem}.bind.ts`)}: {\n${exported(file).join("\n")}\n };`)
.join("\n")}
}
/** The namespaces, which it returns too. */
interface Namespaces {
${files
.filter(file => file.namespaces.length > 0)
.map(file => {
const functions = `Exports[${JSON.stringify(`${file.origin.stem}.bind.ts`)}]`;
return ` ${JSON.stringify(`${file.origin.stem}.bind.ts`)}: {\n${file.namespaces
.map(
namespace =>
` ${JSON.stringify(namespace.name)}: ${namespace.call ? `${functions}[${JSON.stringify(namespace.call.name)}] & ` : ""}Pick<${functions}, ${namespace.functions.map(fn => JSON.stringify(fn.name)).join(" | ")}>;`,
)
.join("\n")}\n };`;
})
.join("\n")}
}
}
`;
}
26 changes: 23 additions & 3 deletions src/codegen/bindgen-lib-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,16 @@ export interface SignatureOptions {
expected?: Record<string, readonly string[] | string>;
}

/** What `b.fn` takes, and each function of a `b.namespace`. */
export interface FunctionOptions extends SignatureOptions {
/** Also pass `this`, `this_value: JsLocal<'a>`. */
passThis?: boolean;
/** Also pass the function that was called, `callee: JsLocal<'a>`. */
passCallee?: boolean;
/** The host function has C linkage under this name. C++ declares it with `BUN_DECLARE_HOST_FUNCTION`, and in Rust it is a `JSHostFn` of the generated module. */
export?: string;
}

export class Signature {
readonly args: readonly { name: string; type: Type; expected?: readonly string[] | string }[];
readonly returns: Type;
Expand Down Expand Up @@ -1865,10 +1875,20 @@ export type OverloadDispatch = Signature | { argument: number; byKind: Map<Value
export class FunctionDefinition {
readonly signatures: readonly Signature[];
readonly dispatch: OverloadDispatch;
readonly passThis: boolean;
readonly passCallee: boolean;
readonly exportName: string | undefined;
name = "";
origin: Origin | null = null;
constructor(signatures: readonly (SignatureOptions | Signature)[]) {
constructor(signatures: readonly (SignatureOptions | Signature)[], options: FunctionOptions = {}) {
assert(signatures.length > 0, "b.overloads needs at least one signature");
this.passThis = options.passThis ?? false;
this.passCallee = options.passCallee ?? false;
this.exportName = options.export;
assert(
this.exportName === undefined || /^[A-Za-z_]\w*$/.test(this.exportName),
`export: ${this.exportName} is not a C identifier`,
);
this.signatures = signatures.map(options => (options instanceof Signature ? options : new Signature(options)));
assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once");
const nodes = new Map<string, OverloadDispatch>();
Expand Down Expand Up @@ -2375,11 +2395,11 @@ export class NamespaceDefinition {
readonly functions: readonly FunctionDefinition[];
readonly call: FunctionDefinition | undefined;
name = "";
constructor(functions: Record<string, SignatureOptions>, options: NamespaceOptions = {}) {
constructor(functions: Record<string, FunctionOptions>, 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`);
const fn = new FunctionDefinition([options]);
const fn = new FunctionDefinition([options], options);
fn.name = name;
return fn;
});
Expand Down
5 changes: 3 additions & 2 deletions src/codegen/bindgen-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DoubleType,
EnumerationType,
FunctionDefinition,
type FunctionOptions,
integers,
type MemberOptions,
ModuleDefinition,
Expand Down Expand Up @@ -91,13 +92,13 @@ export { class_ as class };
/** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */
export { selfType as self } from "./bindgen-lib-internal.ts";

export const fn = (signature: SignatureOptions) => new FunctionDefinition([signature]);
export const fn = (options: FunctionOptions) => new FunctionDefinition([options], options);

/** One JS function, several signatures, told apart by the kinds of their arguments. */
export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures);

/** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. */
export const namespace = (functions: Record<string, SignatureOptions>, options?: NamespaceOptions) =>
export const namespace = (functions: Record<string, FunctionOptions>, options?: NamespaceOptions) =>
new NamespaceDefinition(functions, options);

/** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */
Expand Down
5 changes: 1 addition & 4 deletions src/codegen/bundle-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import path from "path";
import jsclasses from "./../jsc/bindings/js_classes";
import { sliceSourceCode } from "./builtin-parser";
import { createAssertClientJS, createLogClientJS } from "./client-js";
import { getJS2NativeCPP, getJS2NativeRust } from "./generate-js2native";
import { getJS2NativeCPP } from "./generate-js2native";
import { cap, checkAscii, engineStamp, sourceStamp, writeIfNotChanged, writeIfNotChangedBinary } from "./helpers";
import { createInternalModuleRegistry } from "./internal-module-registry-scanner";
import { define } from "./replacements";
Expand Down Expand Up @@ -749,9 +749,6 @@ writeIfNotChanged(

writeIfNotChanged(path.join(CODEGEN_DIR, "GeneratedJS2Native.h"), getJS2NativeCPP());

// Rust sibling: include!()'d by src/runtime/generated_js2native.rs
writeIfNotChanged(path.join(CODEGEN_DIR, "generated_js2native.rs"), getJS2NativeRust());

const generatedDTSPath = path.join(TYPES_DIR, "generated.d.ts");
writeIfNotChanged(
generatedDTSPath,
Expand Down
Loading
Loading