使用內置的 bun:ffi 模塊高效地從 JavaScript 調用原生庫。它適用於支持 C ABI 的語言(Zig、Rust、C/C++、C#、Nim、Kotlin 等)。
dlopen 用法(bun:ffi)
要打印 sqlite3 的版本號:
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
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}編譯:
zig build-lib add.zig -dynamic -OReleaseFast將共享庫的路徑和符號映射傳遞給 dlopen 導入:
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
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}編譯:
rustc --crate-type cdylib add.rsC++
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}編譯:
zig build-lib add.cpp -dynamic -lc -lc++FFI 類型
支持以下 FFIType 值。
FFIType | C 類型 | 別名 |
|---|---|---|
| buffer | char* | |
| cstring | char* | |
| function | (void*)(*)() | fn, callback |
| ptr | void* | pointer, void*, char* |
| i8 | int8_t | int8_t |
| i16 | int16_t | int16_t |
| i32 | int32_t | int32_t, int |
| i64 | int64_t | int64_t |
| i64_fast | int64_t | |
| u8 | uint8_t | uint8_t |
| u16 | uint16_t | uint16_t |
| u32 | uint32_t | uint32_t |
| u64 | uint64_t | uint64_t |
| u64_fast | uint64_t | |
| f32 | float | float |
| f64 | double | double |
| bool | bool | |
| char | char | |
| napi_env | napi_env | |
| napi_value | napi_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 結尾的字符串並添加一些額外功能:
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 字符串:
const myString = new CString(ptr);要將已知長度的指針轉換為 JavaScript 字符串:
const myString = new CString(ptr, 0, byteLength);new CString() 構造函數克隆 C 字符串,所以在 ptr 被釋放後繼續使用 myString 是安全的。
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) 並且已經加載了一些符號,這很有用。
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* 以某種方式,你獲得了這個指針 */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();如果你有多個函數指針,可以使用 linkSymbols 一次性定義它們:
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 代碼時很有用。
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 不知道的新線程)。
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` 對象: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 轉換為指針:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);要將指針轉換為 ArrayBuffer:
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:
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:
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。
FFIType | read 函數 |
|---|---|
| ptr | read.ptr |
| i8 | read.i8 |
| i16 | read.i16 |
| i32 | read.i32 |
| i64 | read.i64 |
| u8 | read.u8 |
| u16 | read.u16 |
| u32 | read.u32 |
| u64 | read.u64 |
| f32 | read.f32 |
| f64 | read.f64 |
內存管理
bun:ffi 不為你管理內存。你必須在完成後釋放內存。
從 JavaScript
如果你想從 JavaScript 跟蹤 TypedArray 何時不再使用,你可以使用 FinalizationRegistry。
從 C、Rust、Zig 等
如果你想從 C 或 FFI 跟蹤 TypedArray 何時不再使用,你可以將回調和可選的上下文指針傳遞給 toArrayBuffer 或 toBuffer。此函數在稍後的某個時間點調用,一旦垃圾回收器釋放底層的 ArrayBuffer JavaScript 對象。
預期的簽名與 JavaScriptCore 的 C API 中相同:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);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:
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 的指針:
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,
);讀取指針
const out = encode_png(
// pixels 將作為指針傳遞
pixels,
// 尺寸:
128,
128,
);
// 假設它是 0 結尾的,可以這樣讀取:
let png = new Uint8Array(toArrayBuffer(out));
// 保存到磁盤:
await Bun.write("out.png", png);