本文档面向 Bun 的维护者和贡献者,描述内部实现细节。
这个在 2024 年 12 月引入代码库的新绑定生成器,会扫描 *.bind.ts 文件以查找函数和类的定义,并生成 JavaScript 与原生代码之间的胶水代码以实现互操作。
目前还有其他代码生成器和系统实现类似功能,以下这些将最终被完全淘汰并由此生成器取代:
- “Classes generator”,转换
*.classes.ts 用于自定义类。
- “JS2Native”,允许从
src/js 到原生代码的临时调用。
在 Zig 中创建 JS 函数
定义一个简单函数实现文件,比如 add
pub fn add(global: *jsc.JSGlobalObject, a: i32, b: i32) !i32 {
return std.math.add(i32, a, b) catch {
// 绑定函数可以返回 `error.OutOfMemory` 和 `error.JSError`。
// 其他如来自 `std.math.add` 的 `error.Overflow` 必须转换。
// 注意错误提示要明确。
return global.throwPretty("整数相加时溢出", .{});
};
}
const gen = bun.gen.math; // "math" 为此文件的基本名
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;
然后用 .bind.ts 文件描述 API schema。绑定文件位置与 Zig 文件并列。

src/bun.js/math.bind.ts
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(1),
},
ret: t.i32,
});
此函数声明等价于:
/**
* 如果未提供参数则抛出异常。
* 对超出范围的数字使用模运算进行环绕。
*/
declare function add(a: number, b: number = 1): number;
代码生成器会提供 bun.gen.math.jsAdd,它是原生函数实现。传递给 JavaScript 时,使用 bun.gen.math.createAddCallback(global)。src/js/ 下的 JS 文件可以用 $bindgenFn("math.bind.ts", "add") 来获取该实现句柄。
字符串
接收字符串的类型有 t.DOMString、t.ByteString 和 t.USVString,它们直接映射到各自的 WebIDL 对应类型,转换逻辑略有不同。Bindgen 会在所有情况下将 BunString 传给原生代码。
不确定时,使用 DOMString。
t.UTF8String 可以代替 t.DOMString,但是会调用 bun.String.toUTF8。原生回调将获得传入的 []const u8(WTF-8 编码数据),函数返回后释放该数据。
来自 WebIDL 规范的简要说明:
- ByteString 仅能包含有效的 latin1 字符。虽然 bun.String 很可能已是 8 位格式,但不能假设一定如此。
- USVString 不会包含无效的代理对,即文本能够正确用 UTF-8 表示。
- DOMString 最宽松,同时也是最推荐的方案。
函数变体
variants 可以指定多个变体(也叫重载)。

src/bun.js/math.bind.ts
import { t, fn } from "bindgen";
export const action = fn({
variants: [
{
args: {
a: t.i32,
},
ret: t.i32,
},
{
args: {
a: t.DOMString,
},
ret: t.DOMString,
},
],
});
在 Zig 中,每个变体会有一个编号,基于 schema 中定义的顺序。
fn action1(a: i32) i32 {
return a;
}
fn action2(a: bun.String) bun.String {
return a;
}
t.dictionary
dictionary 是 JavaScript 对象的定义,通常用于函数的输入参数。对于函数输出,通常声明一个类类型更聪明,可以添加方法和解构功能。
使用 WebIDL 的枚举类型,可以使用:
t.stringEnum:创建并代码生成一个新的枚举类型。
t.zigEnum:基于已有代码库内的 Zig 枚举派生一个 bindgen 类型。
fmt.zig / bun:internal-for-testing 中使用 stringEnum 的示例:
export const Formatter = t.stringEnum("highlight-javascript", "escape-powershell");
export const fmtString = fn({
args: {
global: t.globalObject,
code: t.UTF8String,
formatter: Formatter,
},
ret: t.DOMString,
});
WebIDL 强烈建议使用 kebab-case(连接符命名)作为枚举值,以保持与现有 Web API 的一致性。
从 Zig 代码派生枚举
TODO: zigEnum
t.oneOf
oneOf 是两个或以上类型的联合。在 Zig 中表示为 union(enum)。
TODO:
有一组属性可链式调用到 t.* 类型上。所有类型共有:
.required(仅用于字典参数)
.optional(仅用于函数参数)
.default(T)
当值为可选时,会被转换为 Zig 的可选类型。
根据类型不同,可用的属性更多。查看自动补全中的类型定义以获取更多详情。注意上述三个属性只能选一个,且必须写在末尾。
整数属性
整型类型允许用 clamp 或 enforceRange 自定义溢出行为
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// 限制在 i32 范围内
a: t.i32.enforceRange(),
// 限制在 u16 范围
b: t.u16,
// 限制在任意范围,带默认值
c: t.i32.enforceRange(0, 1000).default(5),
// 限制在任意范围,或为 null
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});
还有各种 Node.js 验证函数,如 validateInteger、validateNumber 等。实现 Node.js API 时应使用这些函数,以保证错误信息与 Node.js 完全一致。
与 WebIDL 的 enforceRange 不同, Node.js 的 validate* 函数对输入更严格。例如,Node 的数字验证会检查 typeof value === 'number',而 WebIDL 使用 ToNumber 进行有损转换。
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// 非数字则抛出
a: t.f64.validateNumber(),
// i32 范围有效
a: t.i32.validateInt32(),
// f64,且在安全整数范围内
b: t.f64.validateInteger(),
// f64,在指定范围内
c: t.f64.validateNumber(-10000, 10000),
},
ret: t.i32,
});
TODO
TODO