本文档适用于 Bun 的维护者和贡献者,并描述内部实现细节。
新的绑定生成器于 2024 年 12 月引入代码库,扫描 *.bind.ts 以查找函数和类定义,并生成 JavaScript 和原生代码之间互操作的胶水代码。
目前还有其他代码生成器和系统实现了类似的目的。以下所有内容最终都将被完全淘汰,以支持这个新的:
- “类生成器”,将
*.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("Integer overflow while adding", .{});
};
}
const gen = bun.gen.math; // "math" 是此文件的基名
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;
然后使用 .bind.ts 函数描述 API 模式。绑定文件放在 Zig 文件旁边。
src/bun.js/math.bind.tsimport { 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 对应项,并具有稍微不同的转换逻辑。绑定生成器在所有情况下都会将 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.tsimport { t, fn } from "bindgen";
export const action = fn({
variants: [
{
args: {
a: t.i32,
},
ret: t.i32,
},
{
args: {
a: t.DOMString,
},
ret: t.DOMString,
},
],
});
在 Zig 中,每个变体都会得到一个数字,基于模式定义的顺序。
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:基于代码库中现有枚举派生绑定生成器类型。
stringEnum 的示例,如在 fmt.zig / bun:internal-for-testing 中使用:
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 的完全匹配。
与来自 WebIDL 的 enforceRange 不同,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