Skip to content

NOTE

本文档面向 Bun 的维护者和贡献者,描述了内部实现细节。

新的绑定生成器于 2024 年 12 月引入代码库,它扫描 *.bind.ts 文件以查找函数和类定义,并生成胶水代码以实现 JavaScript 和原生代码之间的互操作。

目前还有其他代码生成器和系统可以实现类似的目的。以下这些最终将完全被这个新系统取代:

  • "类生成器",将 *.classes.ts 转换为自定义类。
  • "JS2Native",允许从 src/js 临时调用原生代码。

在 Zig 中创建 JS 函数

给定一个实现简单函数(如 add)的文件:

zig
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 文件旁边。

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,
});

这个函数声明等价于:

ts
/**
 * 如果提供零个参数则抛出错误。
 * 使用模运算包装超出范围的数字。
 */
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.DOMStringt.ByteStringt.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 可以指定多个变体(也称为重载)。

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 中,每个变体根据模式定义的顺序获得一个编号。

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:从代码库中现有的枚举派生一个 bindgen 类型。

stringEnumfmt.zig / bun:internal-for-testing 中使用的示例:

ts
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 可选类型。

根据类型不同,有更多可用的属性。查看自动完成中的类型定义以了解更多细节。注意上述三个属性只能应用其中一个,并且它们必须应用在末尾。

整数属性

整数类型允许使用 clampenforceRange 自定义溢出行为

ts
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 验证函数如 validateIntegervalidateNumber 等可用。在实现 Node.js API 时使用这些,这样错误消息与 Node 的行为 1:1 匹配。

enforceRange(来自 WebIDL)不同,validate* 函数对它们接受的输入要严格得多。例如,Node 的数字验证器检查 typeof value === 'number',而 WebIDL 使用 ToNumber 进行有损转换。

ts
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

Bun学习网由www.bunjs.com.cn整理维护