執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

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