运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

Bindgen

Bun 的 Bindgen

绑定生成器会扫描 *.bind.ts 文件,以查找函数和类定义,并生成用于 JavaScript 与原生代码互操作的胶水代码。

还有其他代码生成器和系统可以实现类似功能;以下这些最终都会被逐步淘汰,转而采用此生成器:

  • “Classes generator”,转换 *.classes.ts 用于自定义类。
  • “JS2Native”,允许从 src/js 到原生代码的临时调用。

在 Rust 中创建 JS 函数

给定一个实现函数的文件,例如 add:

src/jsc/bindgen_test.rs
use crate::{JSGlobalObject, JsResult};
use crate::r#gen::bindgen_test as generated;

pub fn add(global: &JSGlobalObject, a: i32, b: i32) -> JsResult<i32> {
    match a.checked_add(b) {
        Some(v) => Ok(v),
        None => {
            // Binding functions can propagate out-of-memory and JS exceptions
            // directly; other failures (like this integer overflow) must be
            // converted into a thrown error. Remember to be descriptive.
            Err(global.throw(format_args!("Integer overflow while adding")))
        }
    }
}

然后使用 .bind.ts 文件描述 API 模式。绑定文件位于 Rust 文件旁边。

src/jsc/bindgen_test.bind.ts
import { fn, t } from "bindgen";

export const add = fn({
  args: {
    global: t.globalObject,
    a: t.i32,
    b: t.i32.default(-1),
  },
  ret: t.i32,
});

此函数声明等价于:

TypeScript
/**
 * 如果未提供参数则抛出异常。
 * 对超出范围的数字使用模运算进行环绕。
 */
declare function add(a: number, b: number = -1): number;

代码生成器会生成一个 C++ thunk,用于验证并转换 JS 参数,然后调用 Rust 实现。在 Rust 端,bindgen 不会生成任何内容;thunk 调用的分发 shim(src/runtime/hw_exports.rs 中的 bindgen_Bindgen_test_dispatchAdd1,它会调用 add)和 src/jsc/bindings/GeneratedBindings.rs 中的 create_*_callback 模块都是手写的。该模块可通过 crate::r#gen::<basename> 访问(对于 bindgen_test.bind.ts,即 crate::r#gen::bindgen_test)。要构造一个包装原生实现的 JSFunction,请使用 generated::create_add_callback(global):

Rust
use crate::r#gen::bindgen_test as generated;

let js_fn: JSValue = generated::create_add_callback(global);

在 src/js/ 中的 JS 文件里,$bindgenFn("bindgen_test.bind.ts", "add") 会通过 src/runtime/hw_exports.rs 中手写的 js2native_bindgen_<basename>_<fn> 导出,返回一个指向该实现的句柄。

导出的 bindgen 函数在 Rust 端会转换为 snake_case(requiredAndOptionalArg → required_and_optional_arg)。手写的回调构造函数遵循相同的命名约定(create_required_and_optional_arg_callback)。

字符串

要接收字符串,请使用 t.DOMString、t.ByteString 或 t.USVString。这些类型直接对应于它们在 WebIDL 中的对应类型,并且具有略有不同的转换逻辑。在所有情况下,Bindgen 都会将 bun_core::String 传递给原生代码。

不确定时,使用 DOMString。

t.UTF8String 可以替代 t.DOMString,但会立即转换为 UTF-8。 原生回调会接收一个 &[u8] 切片(WTF-8 数据),并在函数返回后释放。

WebIDL 规范要点:

  • ByteString 只能包含有效的 latin1 字符。不能假定 bun_core::String 已经是 8 位格式,但这种情况极有可能。
  • USVString 不包含无效的代理对,因此其文本可以正确地表示为 UTF-8。
  • DOMString 的限制最少,同时也是最推荐的策略。

函数变体

variants 键声明函数的多个变体(也称为重载)。

TypeScript
import { fn, t } from "bindgen";

export const action = fn({
  variants: [
    {
      args: {
        a: t.i32,
      },
      ret: t.i32,
    },
    {
      args: {
        a: t.DOMString,
      },
      ret: t.DOMString,
    },
  ],
});

每个变体都会获得一个带编号的 Rust 函数:

Rust
pub fn action1(a: i32) -> i32 {
    a
}

pub fn action2(a: bun_core::String) -> bun_core::String {
    a
}

t.dictionary

dictionary 描述一个 JavaScript 对象,通常用作函数输入。对于函数输出,建议使用类类型,以便添加方法并支持解构。

枚举

t.stringEnum 创建一个 WebIDL 枚举,并为其生成一个新的枚举类型。

以下是 fmt_jsc.bind.ts / bun:internal-for-testing 中 stringEnum 的示例:

TypeScript
export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell");

export const fmtString = fn({
  args: {
    global: t.globalObject,
    code: t.UTF8String,
    formatter: Formatter,
  },
  ret: t.DOMString,
});

在 Rust 端,该枚举对应为一个 #[repr(u8)] 枚举。Bindgen 在生成 C++ enum class 之前,会对 t.stringEnum 的值按字母顺序进行排序,因此判别值必须与生成的头文件顺序匹配,而不是 .bind.ts 中的声明顺序:

Rust
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum Formatter {
    EscapePowershell = 0,
    HighlightJavascript = 1,
    HighlightJavascriptRedacted = 2,
}

pub fn fmt_string(
    global: &JSGlobalObject,
    code: &[u8],
    formatter_id: Formatter,
) -> JsResult<bun_core::String> {
    // ...
}

WebIDL 强烈建议枚举值使用 kebab-case,以与现有 Web API 保持一致。

t.oneOf

oneOf 是两个或多个类型的联合体。它表示为一个 Rust enum,每种成员类型对应一个变体。

属性

你可以将属性链式添加到 t.* 类型上。所有类型都支持:

  • .required(仅用于字典参数)
  • .optional(仅用于函数参数)
  • .default(T)

当值为 .optional 时,它会被转换为 Rust 的 Option<T>:

TypeScript
export const requiredAndOptionalArg = fn({
  args: {
    a: t.boolean,
    b: t.usize.optional,
    c: t.i32.enforceRange(0, 100).default(42),
    d: t.u8.optional,
  },
  ret: t.i32,
});
Rust
pub fn required_and_optional_arg(a: bool, b: Option<usize>, c: i32, d: Option<u8>) -> i32 {
    // ...
}

根据类型的不同,还提供了更多属性。详情请参阅自动补全中的类型定义。你只能应用这三种属性中的一种,并且必须将它放在最后。

整数属性

整数类型可以使用 clamp 或 enforceRange 来自定义溢出行为:

TypeScript
import { fn, t } 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),
    // 限制在任意范围,或为 None
    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 进行有损转换。

TypeScript
import { fn, t } from "bindgen";

export const add = fn({
  args: {
    global: t.globalObject,
    // 非数字则抛出
    a: t.f64.validateNumber(),
    // 在 i32 范围内有效
    b: t.i32.validateInt32(),
    // 在安全整数范围内的 f64
    c: t.f64.validateInteger(),
    // 在给定范围内的 f64
    d: t.f64.validateNumber(-10000, 10000),
  },
  ret: t.i32,
});

回调

TODO。

类

TODO