Skip to main content
bun:ffi实验性质的,存在已知的错误和限制,不应该在生产环境中依赖使用。与本地代码交互的最稳定方式是编写一个 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 倍。 dlopenlinkSymbolsCFunctionJSCallback 由 Bun 的 JavaScript 引擎(JavaScriptCore)原生实现:参数转换、参数数量处理和结果装箱都在引擎内部完成,并且热点调用点会通过 DFG/FTL JIT 层编译为直接的原生调用,不会为每个参数使用 JavaScript 补充层。TinyCC 是一个小型且快速的 C 编译器,仅内嵌用于 cc(),用于编译你在运行时提供的 C 源代码。

用法

Zig

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

Rust

编译命令:

C++

编译命令:

FFI 类型

支持以下 FFIType 值。 buffer 参数必须是 TypedArrayDataView buffer_lengthbuffer 的长度孪生参数:传入与 buffer 参数相同的 TypedArray/DataView, 被调用方会接收到该视图的字节长度,类型为无符号 64 位整数。引擎会在调用时从同一对象读取指针和长度, 因此二者始终一致——这是自行传入 view.byteLength 无法获得的原子快照(在 JavaScript 中预先读取的长度, 对于可调整大小、可增长或已转移的缓冲区,可能会变得过时)。它仅可用作参数,并且与 napi 类型一样, 不适用于 cc()
napi_envnapi_value 仅在 cc() 源代码中有效。在此环境中, 编译后的 trampoline 会将 napi_env 参数填充为模块的环境(传递到该位置的 JavaScript 参数是占位符, 会被忽略),而 napi_value 会将 JavaScript 值原样传递。 在 dlopenlinkSymbolsJSCallbackCFunction 描述符中使用任一类型都会抛出 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 线程,在那里转换参数(64 位整数和指针会以精确的 BigInt 形式传入)并执行你的函数。由于从 C 的角度来看,调用是异步的,因此返回给 C 调用方的值是未指定的:你可以声明非 voidreturns(下面的示例使用 "bool"),但 C 端必须将线程安全回调视为返回 void,并忽略其返回值。
⚡️ 性能提示:如需略微提升性能,请直接传递 JSCallback.prototype.ptr,而不是传递 JSCallback 对象:

指针

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

内存管理

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

来自 JavaScript

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

来自 C、Rust、Zig 等

要从 C 或 FFI 跟踪 TypedArray 不再被使用的时机,请将回调和可选的上下文指针传递给 toArrayBuffertoBuffer。当垃圾回收器释放底层的 ArrayBuffer JavaScript 对象后,回调会在之后被调用。 预期的签名与 JavaScriptCore 的 C API 一致:

内存安全

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

指针对齐

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

传递指针

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

读取指针