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整理維護