本文档适用于 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 {
// Binding functions can return `error.OutOfMemory` and `error.JSError`.
// Others like `error.Overflow` from `std.math.add` must be converted.
// Remember to be descriptive.
return global.throwPretty("Integer overflow while adding", .{});
};
}
const gen = bun.gen.math; // "math" being this file's basename
const std = @import("std");
const bun = @import("root").bun;
const JSC = bun.JSC;
然后使用 .bind.ts
函数描述 API 架构。绑定文件与 Zig 文件位于同一位置。
import { t, fn } from 'bindgen';
export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(1),
},
ret: t.i32,
});
这个函数声明等价于
/**
* Throws if zero arguments are provided.
* Wraps out of range numbers using modulo.
*/
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 counterparts,并且具有稍微不同的转换逻辑。在所有情况下,Bindgen 都会将 BunString 传递给原生代码。
当有疑问时,请使用 DOMString。
t.UTF8String
可以代替 t.DOMString
使用,但会调用 bun.String.toUTF8
。原生回调会获取传递给原生代码的 []const u8
(WTF-8 数据),并在函数返回后释放它。
来自 WebIDL 规范的 TLDR 总结
- ByteString 只能包含有效的 latin1 字符。假设 bun.String 已经是 8 位格式是不安全的,但这极有可能。
- USVString 不会包含无效的代理对,也就是可以在 UTF-8 中正确表示的文本。
- DOMString 是最宽松但也最推荐的策略。
函数变体
variants
可以指定多个变体(也称为重载)。
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
: 从代码库中现有的枚举派生 bindgen 类型。
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
,仅在 dictionary 参数中使用.optional
,仅在函数参数中使用.default(T)
当一个值是可选的,它会被降级为 Zig 可选类型。
根据类型,还有更多可用的属性。有关更多详细信息,请参阅自动完成中的类型定义。请注意,以上三个属性中只能应用一个,并且它们必须在最后应用。
整数属性
整数类型允许使用 clamp
或 enforceRange
自定义溢出行为。
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// enforce in i32 range
a: t.i32.enforceRange(),
// clamp to u16 range
b: t.u16,
// enforce in arbitrary range, with a default if not provided
c: t.i32.enforceRange(0, 1000).default(5),
// clamp to arbitrary range, or null
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});
各种 Node.js 验证器函数,例如 validateInteger
、validateNumber
等都是可用的。在实现 Node.js API 时使用这些函数,以便错误消息与 Node 的行为 1:1 匹配。
与取自 WebIDL 的 enforceRange
不同,validate*
函数对它们接受的输入更加严格。例如,Node 的数值验证器检查 typeof value === 'number'
,而 WebIDL 使用 ToNumber
进行有损转换。
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// throw if not given a number
a: t.f64.validateNumber(),
// valid in i32 range
a: t.i32.validateInt32(),
// f64 within safe integer range
b: t.f64.validateInteger(),
// f64 in given range
c: t.f64.validateNumber(-10000, 10000),
},
ret: t.i32,
});
回调
TODO
类
TODO