Bindgen
Bun 的 Bindgen
繫結生成器會掃描 *.bind.ts 檔案,以查詢函式和類定義,並生成用於 JavaScript 與原生程式碼互操作的膠水程式碼。
還有其他程式碼生成器和系統可以實現類似功能;以下這些最終都會被逐步淘汰,轉而採用此生成器:
- “Classes generator”,轉換
*.classes.ts用於自定義類。 - “JS2Native”,允許從
src/js到原生程式碼的臨時呼叫。
在 Rust 中建立 JS 函式
給定一個實現函式的檔案,例如 add:
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 檔案旁邊。
import { fn, t } 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;
程式碼生成器會生成一個 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):
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 鍵宣告函式的多個變體(也稱為過載)。
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 函式:
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 的範例:
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 中的宣告順序:
#[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>:
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,
});
pub fn required_and_optional_arg(a: bool, b: Option<usize>, c: i32, d: Option<u8>) -> i32 {
// ...
}
根據型別的不同,還提供了更多屬性。詳情請參閱自動補全中的型別定義。你只能應用這三種屬性中的一種,並且必須將它放在最後。
整數屬性
整數型別可以使用 clamp 或 enforceRange 來自定義溢位行為:
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 進行有損轉換。
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