Skip to main content
bun:ffi 是实验性的,存在已知的错误和限制。请勿在生产环境中依赖它。从 Bun 与本地代码交互最稳定的方式是编写一个 Node-API 模块。
使用内置的 bun:ffi 模块从 JavaScript 高效地调用本地库。它适用于任何支持 C ABI 的语言,包括 Zig、Rust、C/C++、C#、Nim 和 Kotlin。

dlopen 用法(bun:ffi)

打印 sqlite3 的版本号:

性能

根据我们的基准测试,bun:ffi 的速度大约是通过 Node-API 实现的 Node.js FFI 的 2-6 倍。 Bun 的 JavaScript 引擎(JavaScriptCore)原生实现了 dlopen、linkSymbols、CFunction 和 JSCallback。参数转换、元数处理和结果装箱都在引擎内部完成。热点调用点会通过 DFG/FTL JIT 层编译成直接的本地调用,不需要逐参数的 JavaScript 垫片。Bun 嵌入了 TinyCC,这是一款小巧快速的 C 编译器,仅用于 cc(),它可以在运行时编译你提供的 C 源代码。

用法

Zig

add.zig
编译命令:
terminal
传入共享库路径和符号映射到 dlopen:

Rust

编译命令:

C++

编译命令:

FFI 类型

支持以下 FFIType 值。 buffer 参数必须是 TypedArray 或 DataView。 buffer_length 是 buffer 的配对长度参数:传入你为 buffer 参数传入的同一个 TypedArray/DataView,被调用函数会将该视图的字节长度作为无符号 64 位整数接收。引擎会在调用时从同一个对象读取指针和长度,因此两者始终一致。你无法通过自行传入 view.byteLength 获得这种原子快照,因为在 JavaScript 中提前读取的长度可能会因可调整大小、可增长或已转移的缓冲区而过时。buffer_length 仅可用作参数,并且与 napi 类型一样,不能在 cc() 中使用。
napi_env 和 napi_value 仅在 cc() 源代码中有效。在那里,编译后的跳板函数会用模块的环境填充 napi_env 参数;在该位置传入的 JavaScript 参数只是占位符,会被忽略。napi_value 会原样传递 JavaScript 值。在 dlopen、linkSymbols、JSCallback 或 CFunction 描述符中使用任一类型都会抛出 TypeError。

字符串

JavaScript 字符串和 C 样式字符串不同,这使得在本地库中使用字符串变得复杂。
JavaScript 字符串:
  • UTF16(每个字符 2 字节)或者可能是 latin1,具体取决于 JavaScript 引擎和使用的字符
  • length 单独存储
  • 不可变
C 字符串:
  • 通常是 UTF8(每个字母 1 字节)
  • 不存储长度。相反,字符串以 null 结尾:其长度是第一个 \0 的索引
  • 可变
为了解决这个问题,bun:ffi 导出了 CString,它会读取指针处的 UTF-8 C 字符串,并返回一个普通的 JavaScript 字符串:
从以 null 结尾的字符串指针转换为 JavaScript 字符串:
从指定长度的指针转换为 JavaScript 字符串:
new CString() 返回普通字符串(typeof myString === "string",myString === "hello" 可正常使用)。该字符串是 C 字符串的副本,因此即使 ptr 已释放,你仍然可以安全地使用它。
在 returns 中使用时,FFIType.cstring 会将指针强制转换为 JavaScript string。在 args 中使用时,FFIType.cstring 接受 ptr 所接受的一切,并且还接受直接传入的 JavaScript 字符串。引擎会将字符串转码为以 null 结尾的 UTF-8 缓冲区,该缓冲区在调用期间有效,因此你无需自行将其编码为 Buffer:
cstring 返回值的生命周期。 指针就是 C 函数返回的内容:由原生端拥有的内存(静态内存、它管理的缓冲区或它分配的堆内存)。返回时引擎不会复制任何内容,JavaScript 字符串会从该内存中克隆出来。唯一的别名情况是,C 函数返回了一个_源自你以 JavaScript 字符串形式传入的 cstring 参数_的指针。引擎会将该参数转码到仅在本次调用期间有效的缓冲区,因此应将此类返回指针视为仅在下一次 FFI 调用复用该缓冲区之前有效(这是返回输入内容的函数通常遵循的 C 规则)。请克隆它(通过返回的字符串或 new CString),不要持有原始地址。

函数指针

不支持异步函数
要从 JavaScript 调用函数指针,请使用 CFunction,例如使用从已加载的 Node-API(napi)模块中获取的指针。
要一次定义多个函数指针,请使用 linkSymbols:

回调

使用 JSCallback 创建 JavaScript 回调函数,以便将其传递给 C/FFI 函数,让原生代码能够回调 JavaScript 或 TypeScript。这对于异步代码非常有用。
使用完 JSCallback 后,调用 close() 释放内存。

实验性线程安全回调

JSCallback 实验性支持线程安全回调。如果要将回调函数传递到创建它的线程之外的其他线程,则需要使用此功能。通过可选的 threadsafe 参数启用它。 线程安全回调可从任何线程调用,包括由你的本地库生成、Bun 不会以其他方式获知的线程。引擎会在线程安全回调所在的调用线程上复制 C 参数,并将调用转发到 JavaScript 线程。在那里,引擎会像处理普通回调一样转换参数(i64/u64/usize 参数会以 BigInt 形式传入;指针会以 number 形式传入),并运行你的函数。由于从 C 的角度来看,调用是异步的,因此返回给 C 调用方的值未指定。你可以声明非 void 的 returns(下例使用 "bool"),但 C 端必须将线程安全回调视为返回 void,并忽略其返回值。
⚡️ 性能提示:如需略微提升性能,请直接传递 JSCallback.prototype.ptr,而不是传递 JSCallback 对象:

指针

Bun 在 JavaScript 中将指针表示为 number 类型。
64 位处理器最多支持 52 位可寻址空间。JavaScript 数字支持 53 位可用空间,因此还剩下 1 位额外空间。为什么不用 BigInt? BigInt 更慢。JavaScript 引擎会单独分配 BigInt,因此它无法装入常规 JavaScript 值。如果将 BigInt 传递给函数,Bun 会将其转换为 number。Windows 注意事项:Windows API 类型 HANDLE 不表示虚拟地址,将 ptr 用于 HANDLE 不会 按预期工作。请使用 u64 来安全地表示 HANDLE 值。
将 TypedArray 转换为指针:
将指针转换为 ArrayBuffer:
读取指针数据有两种选择。对于长时效指针,使用 DataView:
对于短时效指针,使用 read:
read 函数行为类似于 DataView,但通常更快,因为不需创建 DataView 或 ArrayBuffer。

内存管理

bun:ffi 不会替你管理内存。你必须在用完后释放内存。

来自 JavaScript

要跟踪 JavaScript 中不再使用的 TypedArray,请使用 FinalizationRegistry。

来自 C、Rust、Zig 等

要从 C 或 FFI 中跟踪何时不再使用 TypedArray,请向 toArrayBuffer 或 toBuffer 传递一个回调和一个可选的上下文指针。垃圾回收器释放底层的 JavaScript ArrayBuffer 对象后,Bun 会调用该回调。该回调可能会在垃圾回收线程而非 JavaScript 线程上运行,因此不得回调 JavaScript,也不得假设存在特定线程的状态。 预期的签名与 JavaScriptCore 的 C API 一致:

内存安全

不要在 FFI 之外使用原始指针。Bun 的未来版本可能会添加一个 CLI 标志来禁用 bun:ffi。

指针对齐

如果接口期望的是与 char 或 u8 不同大小的指针,确保 TypedArray 也是对应的大小。u64* 和 [8]u8* 并不完全等价,因为对齐要求不同。

传递指针

当 FFI 函数期望指针时,传入对应大小的 TypedArray:
自动生成的包装器会将 TypedArray 转换为指针。
如果不希望进行自动转换,或者希望获取 TypedArray 内特定字节偏移处的指针,请直接获取指向 TypedArray 的指针:

读取指针