⚠️ 警告 — bun:ffi
仍处于实验阶段,存在已知错误和限制,不应在生产环境中使用。从 Bun 与原生代码交互最稳定的方式是编写一个 Node-API 模块。
使用内置的 bun:ffi
模块可以高效地从 JavaScript 调用原生库。它支持支持 C ABI 的语言(Zig、Rust、C/C++、C#、Nim、Kotlin 等)。
打印 sqlite3
的版本号
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` is either "dylib", "so", or "dll" depending on the platform
// you don't have to use "suffix", it's just there for convenience
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // the function to call
},
} = dlopen(
path, // a library name or file path
{
sqlite3_libversion: {
// no arguments, returns a string
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`SQLite 3 version: ${sqlite3_libversion()}`);
根据 我们的基准测试,bun:ffi
的速度大约是 Node.js 通过 Node-API
进行 FFI 操作的 2-6 倍。

Bun 会生成并即时编译 C 绑定,从而高效地在 JavaScript 类型和原生类型之间转换值。为了编译 C,Bun 内嵌了 TinyCC,这是一个小巧且快速的 C 编译器。
// add.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));
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
编译
rustc --crate-type cdylib add.rs
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}
编译
zig build-lib add.cpp -dynamic -lc -lc++
支持以下 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 字节),通常
- 不存储长度。相反,字符串是空终止的,这意味着长度是它找到的第一个
\0
的索引 - 可变
为了解决这个问题,bun:ffi
导出了 CString
,它扩展了 JavaScript 内置的 String
以支持空终止字符串并添加了一些额外功能
class CString extends String {
/**
* Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
/**
* The ptr to the C string
*
* This `CString` instance is a clone of the string, so it
* is safe to continue using this instance after the `ptr` has been
* freed.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}
将空终止字符串指针转换为 JavaScript 字符串
const myString = new CString(ptr);
将已知长度的指针转换为 JavaScript 字符串
const myString = new CString(ptr, 0, byteLength);
new CString()
构造函数会克隆 C 字符串,因此在 ptr
被释放后继续使用 myString
是安全的。
my_library_free(myString.ptr);
// this is safe because myString is a clone
console.log(myString);
当用作 returns
时,FFIType.cstring
会将指针强制转换为 JavaScript string
。当用作 args
时,FFIType.cstring
与 ptr
相同。
要从 JavaScript 调用函数指针,请使用 CFunction
。如果您在使用 Bun 时使用 Node-API (napi) 并且已经加载了一些符号,这将非常有用。
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* somehow, you got this pointer */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();
如果您有多个函数指针,可以使用 linkSymbols
一次定义它们
import { linkSymbols } from "bun:ffi";
// getVersionPtrs defined elsewhere
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
// Unlike with dlopen(), the names here can be whatever you want
getMajor: {
returns: "cstring",
args: [],
// Since this doesn't use dlsym(), you have to provide a valid ptr
// That ptr could be a number or a bigint
// An invalid pointer will crash your program.
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)) {
// found a match!
}
// Sometime later:
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, // Optional. Defaults to `false`
},
);
⚡️ 性能提示 — 为了获得轻微的性能提升,请直接传递 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,
});
// This code runs slightly faster:
setOnResolve(onResolve.ptr);
// Compared to this:
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 accepts a `byteOffset` and `byteLength`
// if `byteLength` is not provided, it is assumed to be a null-terminated pointer
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
如果您想跟踪 TypedArray
何时不再被 JavaScript 使用,您可以使用 FinalizationRegistry。
从 C、Rust、Zig 等
如果您想跟踪 TypedArray
何时不再被 C 或 FFI 使用,您可以将回调函数和可选的上下文指针传递给 toArrayBuffer
或 toBuffer
。此函数将在稍后某个时间被调用,一旦垃圾回收器释放了底层的 ArrayBuffer
JavaScript 对象。
预期的签名与 JavaScriptCore 的 C API 中的签名相同。
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
import { toArrayBuffer } from "bun:ffi";
// with a deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// this is an optional pointer to a callback
deallocatorContext,
// this is a pointer to a function
jsTypedArrayBytesDeallocator,
);
// without a deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// this is a pointer to a function
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's can be specified as strings too
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 will be passed as a pointer
pixels,
128,
128,
);
自动生成的包装器会将指针转换为 TypedArray
。
如果您不想要自动转换,或者您想要指向 TypedArray
中特定字节偏移量的指针,您也可以直接获取 TypedArray
的指针
import { dlopen, FFIType, ptr } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType's can be specified as strings too
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
// this returns a number! not a BigInt!
const myPtr = ptr(pixels);
const out = encode_png(
myPtr,
// dimensions:
128,
128,
);
const out = encode_png(
// pixels will be passed as a pointer
pixels,
// dimensions:
128,
128,
);
// assuming it is 0-terminated, it can be read like this:
let png = new Uint8Array(toArrayBuffer(out));
// save it to disk:
await Bun.write("out.png", png);