Skip to content

使用內置的 bun:ffi 模塊高效地從 JavaScript 調用原生庫。它適用於支持 C ABI 的語言(Zig、Rust、C/C++、C#、Nim、Kotlin 等)。


dlopen 用法(bun:ffi) ​

要打印 sqlite3 的版本號:

ts
import { dlopen, FFIType, suffix } from "bun:ffi";

// `suffix` 根據平台是 "dylib"、"so" 或 "dll"
// 你不必使用 "suffix",它只是為了方便
const path = `libsqlite3.${suffix}`;

const {
  symbols: {
    sqlite3_libversion, // 要調用的函數
  },
} = dlopen(
  path, // 庫名稱或文件路徑
  {
    sqlite3_libversion: {
      // 無參數,返回字符串
      args: [],
      returns: FFIType.cstring,
    },
  },
);

console.log(`SQLite 3 版本:${sqlite3_libversion()}`);

性能 ​

根據 我們的基准測試,bun:ffi 大約比通過 Node-API 的 Node.js FFI 快 2-6 倍。

Bun 生成並即時編譯 C 綁定,高效地在 JavaScript 類型和原生類型之間轉換值。為了編譯 C,Bun 嵌入了 TinyCC,一個小型快速的 C 編譯器。


用法 ​

Zig ​

zig
pub export fn add(a: i32, b: i32) i32 {
  return a + b;
}

編譯:

bash
zig build-lib add.zig -dynamic -OReleaseFast

將共享庫的路徑和符號映射傳遞給 dlopen 導入:

ts
import { dlopen, FFIType, suffix } from "bun:ffi";
const { i32 } = FFIType;

const path = `libadd.${suffix}`;

const lib = dlopen(path, {
  add: {
    args: [i32, i32],
    returns: i32,
  },
});

console.log(lib.symbols.add(1, 2));

Rust ​

rust
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

編譯:

bash
rustc --crate-type cdylib add.rs

C++ ​

c
#include <cstdint>

extern "C" int32_t add(int32_t a, int32_t b) {
    return a + b;
}

編譯:

bash
zig build-lib add.cpp -dynamic -lc -lc++

FFI 類型 ​

支持以下 FFIType 值。

FFITypeC 類型別名
bufferchar*
cstringchar*
function(void*)(*)()fn, callback
ptrvoid*pointer, void*, char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t, int
i64int64_tint64_t
i64_fastint64_t
u8uint8_tuint8_t
u16uint16_tuint16_t
u32uint32_tuint32_t
u64uint64_tuint64_t
u64_fastuint64_t
f32floatfloat
f64doubledouble
boolbool
charchar
napi_envnapi_env
napi_valuenapi_value

注意:buffer 參數必須是 TypedArray 或 DataView。


字符串 ​

JavaScript 字符串和 C 風格字符串不同,這使得使用原生庫處理字符串變得復雜。

JavaScript 字符串和 C 字符串有何不同? ​

JavaScript 字符串:

  • UTF16(每個字母 2 字節)或可能是 latin1,取決於 JavaScript 引擎和使用的字符
  • length 單獨存儲
  • 不可變

C 字符串:

  • UTF8(每個字母 1 字節),通常
  • 長度不存儲。相反,字符串以 null 結尾,這意味著長度是它找到的第一個 \0 的索引
  • 可變

為了解決這個問題,bun:ffi 導出 CString,它擴展了 JavaScript 的內置 String 以支持 null 結尾的字符串並添加一些額外功能:

ts
class CString extends String {
  /**
   * 給定 `ptr`,這將自動搜索結束 `\0` 字符並在需要時從 UTF-8 轉碼為 UTF-16。
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * C 字符串的 ptr
   *
   * 此 `CString` 實例是字符串的克隆,所以它是
   * 安全的,在 `ptr` 被釋放後繼續使用此實例。
   */
  ptr: number;
  byteOffset?: number;
  byteLength?: number;
}

要將 null 結尾的字符串指針轉換為 JavaScript 字符串:

ts
const myString = new CString(ptr);

要將已知長度的指針轉換為 JavaScript 字符串:

ts
const myString = new CString(ptr, 0, byteLength);

new CString() 構造函數克隆 C 字符串,所以在 ptr 被釋放後繼續使用 myString 是安全的。

ts
my_library_free(myString.ptr);

// 這是安全的,因為 myString 是克隆
console.log(myString);

當在 returns 中使用時,FFIType.cstring 將指針強制轉換為 JavaScript string。當在 args 中使用時,FFIType.cstring 與 ptr 相同。


函數指針 ​

NOTE

異步函數尚不支持

要從 JavaScript 調用函數指針,使用 CFunction。如果你在 Bun 中使用 Node-API (napi) 並且已經加載了一些符號,這很有用。

ts
import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* 以某種方式,你獲得了這個指針 */

const getVersion = new CFunction({
  returns: "cstring",
  args: [],
  ptr: myNativeLibraryGetVersion,
});
getVersion();

如果你有多個函數指針,可以使用 linkSymbols 一次性定義它們:

ts
import { linkSymbols } from "bun:ffi";

// getVersionPtrs 在其他地方定義
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // 與 dlopen() 不同,這裡的名稱可以是任何你想要的
  getMajor: {
    returns: "cstring",
    args: [],

    // 由於這不使用 dlsym(),你必須提供有效的 ptr
    // 該 ptr 可以是數字或 bigint
    // 無效的指針將使程序崩潰。
    ptr: majorPtr,
  },
  getMinor: {
    returns: "cstring",
    args: [],
    ptr: minorPtr,
  },
  getPatch: {
    returns: "cstring",
    args: [],
    ptr: patchPtr,
  },
});

const [major, minor, patch] = [lib.symbols.getMajor(), lib.symbols.getMinor(), lib.symbols.getPatch()];

回調 ​

使用 JSCallback 創建可以傳遞給 C/FFI 函數的 JavaScript 回調函數。C/FFI 函數可以調用到 JavaScript/TypeScript 代碼中。這對於異步代碼或當你想從 C 調用 JavaScript 代碼時很有用。

ts
import { dlopen, JSCallback, ptr, CString } from "bun:ffi";

const {
  symbols: { search },
  close,
} = dlopen("libmylib", {
  search: {
    returns: "usize",
    args: ["cstring", "callback"],
  },
});

const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
  returns: "bool",
  args: ["ptr", "usize"],
});

const str = Buffer.from("wwutwutwutwutwutwutwutwutwutwutut\0", "utf8");
if (search(ptr(str), searchIterator)) {
  // 找到匹配!
}

// 稍後:
setTimeout(() => {
  searchIterator.close();
  close();
}, 5000);

當你完成使用 JSCallback 時,應該調用 close() 來釋放內存。

實驗性線程安全回調 ​

JSCallback 實驗性支持線程安全回調。如果你將回調函數傳遞到與其實例化上下文不同的線程,這將需要。你可以使用可選的 threadsafe 參數啟用它。

目前,線程安全回調在從另一個運行 JavaScript 代碼的線程運行時效果最佳,例如 Worker。未來版本的 Bun 將允許它們從任何線程調用(包括你的原生庫生成的 Bun 不知道的新線程)。

ts
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
  returns: "bool",
  args: ["ptr", "usize"],
  threadsafe: true, // 可選。默認為 `false`
});

NOTE

**⚡️ 性能提示** — 為了輕微的性能提升,直接傳遞 `JSCallback.prototype.ptr` 而不是 `JSCallback` 對象:
ts
const onResolve = new JSCallback(arg => arg === 42, {
  returns: "bool",
  args: ["i32"],
});
const setOnResolve = new CFunction({
  returns: "bool",
  args: ["function"],
  ptr: myNativeLibrarySetOnResolve,
});

// 此代碼運行稍快:
setOnResolve(onResolve.ptr);

// 與此相比:
setOnResolve(onResolve);

指針 ​

Bun 在 JavaScript 中將 指針 表示為 number。

64 位指針如何適應 JavaScript 數字? ​

64 位處理器支持多達 52 位可尋址空間。JavaScript 數字 支持 53 位可用空間,所以這給我們留下了大約 11 位的額外空間。

為什麼不用 BigInt? BigInt 較慢。JavaScript 引擎分配單獨的 BigInt,這意味著它們無法適應常規 JavaScript 值。如果你將 BigInt 傳遞給函數,它將被轉換為 number

要將 TypedArray 轉換為指針:

ts
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

要將指針轉換為 ArrayBuffer:

ts
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

// toArrayBuffer 接受 `byteOffset` 和 `byteLength`
// 如果未提供 `byteLength`,則假定它是 null 結尾的指針
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);

要從指針讀取數據,你有兩個選項。對於長生命周期的指針,使用 DataView:

ts
import { toArrayBuffer } from "bun:ffi";
let myDataView = new DataView(toArrayBuffer(myPtr, 0, 32));

console.log(
  myDataView.getUint8(0, true),
  myDataView.getUint8(1, true),
  myDataView.getUint8(2, true),
  myDataView.getUint8(3, true),
);

對於短生命周期的指針,使用 read:

ts
import { read } from "bun:ffi";

console.log(
  // ptr, byteOffset
  read.u8(myPtr, 0),
  read.u8(myPtr, 1),
  read.u8(myPtr, 2),
  read.u8(myPtr, 3),
);

read 函數的行為類似於 DataView,但它通常更快,因為它不需要創建 DataView 或 ArrayBuffer。

FFITyperead 函數
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.f64

內存管理 ​

bun:ffi 不為你管理內存。你必須在完成後釋放內存。

從 JavaScript ​

如果你想從 JavaScript 跟蹤 TypedArray 何時不再使用,你可以使用 FinalizationRegistry。

從 C、Rust、Zig 等 ​

如果你想從 C 或 FFI 跟蹤 TypedArray 何時不再使用,你可以將回調和可選的上下文指針傳遞給 toArrayBuffer 或 toBuffer。此函數在稍後的某個時間點調用,一旦垃圾回收器釋放底層的 ArrayBuffer JavaScript 對象。

預期的簽名與 JavaScriptCore 的 C API 中相同:

c
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
ts
import { toArrayBuffer } from "bun:ffi";

// 帶有 deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // 這是指向回調的可選指針
  deallocatorContext,

  // 這是指向函數的指針
  jsTypedArrayBytesDeallocator,
);

// 不帶 deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // 這是指向函數的指針
  jsTypedArrayBytesDeallocator,
);

內存安全 ​

強烈不建議在 FFI 之外使用原始指針。未來版本的 Bun 可能會添加 CLI 標志來禁用 bun:ffi。

指針對齊 ​

如果 API 期望指針大小不是 char 或 u8,請確保 TypedArray 也是該大小。由於對齊,u64* 與 [8]u8* 不完全相同。

傳遞指針 ​

在 FFI 函數期望指針的地方,傳遞等效大小的 TypedArray:

ts
import { dlopen, FFIType } from "bun:ffi";

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // FFIType 也可以指定為字符串
    args: ["ptr", "u32", "u32"],
    returns: FFIType.ptr,
  },
});

const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
pixels.subarray(0, 32 * 32 * 2).fill(0);

const out = encode_png(
  // pixels 將作為指針傳遞
  pixels,

  128,
  128,
);

自動生成的包裝器 將指針轉換為 TypedArray。

高級模式 ​

如果你不想要自動轉換,或者你想要指向 TypedArray 內特定字節偏移的指針,你也可以直接獲取 TypedArray 的指針:

ts
import { dlopen, FFIType, ptr } from "bun:ffi";

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // FFIType 也可以指定為字符串
    args: ["ptr", "u32", "u32"],
    returns: FFIType.ptr,
  },
});

const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);

// 這返回數字!不是 BigInt!
const myPtr = ptr(pixels);

const out = encode_png(
  myPtr,

  // 尺寸:
  128,
  128,
);

讀取指針 ​

ts
const out = encode_png(
  // pixels 將作為指針傳遞
  pixels,

  // 尺寸:
  128,
  128,
);

// 假設它是 0 結尾的,可以這樣讀取:
let png = new Uint8Array(toArrayBuffer(out));

// 保存到磁盤:
await Bun.write("out.png", png);

Bun學習網由www.bunjs.com.cn整理維護