Skip to main content
本文档面向 Bun 的维护者和贡献者,描述内部实现细节。
这个在 2024 年 12 月引入代码库的新绑定生成器,会扫描 *.bind.ts 文件以查找函数和类的定义,并生成 JavaScript 与原生代码之间的胶水代码以实现互操作。 目前还有其他代码生成器和系统实现类似功能,以下这些将最终被完全淘汰并由此生成器取代:
  • “Classes generator”,转换 *.classes.ts 用于自定义类。
  • “JS2Native”,允许从 src/js 到原生代码的临时调用。

在 Zig 中创建 JS 函数

定义一个简单函数实现文件,比如 add
src/bun.js/math.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("整数相加时溢出", .{});
    };
}

const gen = bun.gen.math; // "math" 为此文件的基本名

const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;
然后用 .bind.ts 文件描述 API schema。绑定文件位置与 Zig 文件并列。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79src/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.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 可以指定多个变体(也叫重载)。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79src/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 的可选类型。 根据类型不同,可用的属性更多。查看自动补全中的类型定义以获取更多详情。注意上述三个属性只能选一个,且必须写在末尾。

整数属性

整型类型允许用 clampenforceRange 自定义溢出行为
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.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