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 倍。
dlopen、linkSymbols、CFunction 和 JSCallback 由 Bun 的 JavaScript 引擎(JavaScriptCore)原生实现:参数转换、参数数量处理和结果装箱都在引擎内部完成,并且热点调用点会通过 DFG/FTL JIT 层编译为直接的原生调用,不会为每个参数使用 JavaScript 补充层。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 中预先读取的长度,
对于可调整大小、可增长或已转移的缓冲区,可能会变得过时)。它仅可用作参数,并且与 napi 类型一样,
不适用于 cc()。
napi_env 和 napi_value 仅在 cc() 源代码中有效。在此环境中,
编译后的 trampoline 会将 napi_env 参数填充为模块的环境(传递到该位置的 JavaScript 参数是占位符,
会被忽略),而 napi_value 会将 JavaScript 值原样传递。 在 dlopen、linkSymbols、JSCallback
或 CFunction 描述符中使用任一类型都会抛出 TypeError。
字符串
JavaScript 字符串和 C 样式字符串不同,这使得在本地库中使用字符串变得复杂。JavaScript 字符串和 C 字符串有什么不同?
JavaScript 字符串和 C 字符串有什么不同?
JavaScript 字符串:
- UTF16(每个字符 2 字节)或者可能是 latin1,具体取决于 JavaScript 引擎和使用的字符
length单独存储- 不可变
- 通常是 UTF8(每个字母 1 字节)
- 不存储长度。相反,字符串以 null 结尾:其长度是第一个
\0的索引 - 可变
bun:ffi 导出了 CString,它会读取指针处的 UTF-8 C 字符串,并返回一个普通的 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 克隆它,而不要持有原始地址。
函数指针
不支持异步函数
CFunction,例如使用从已加载的 Node-API (napi) 模块中获取的指针。
linkSymbols:
回调
使用JSCallback 创建 JavaScript 回调函数,以便将其传递给 C/FFI 函数,让原生代码能够回调 JavaScript 或 TypeScript。这对于异步代码非常有用。
JSCallback 后,调用 close() 释放内存。
实验性线程安全回调
JSCallback 实验性支持线程安全回调。如果要将回调函数传递到创建它的线程之外的其他线程,则需要使用此功能。通过可选的 threadsafe 参数启用它。
线程安全回调可以从任意线程调用——包括由原生库创建、且 Bun 事先并不了解的线程。引擎会在线程安全回调所在线程上复制 C 参数,并将调用封送到 JavaScript 线程,在那里转换参数(64 位整数和指针会以精确的 BigInt 形式传入)并执行你的函数。由于从 C 的角度来看,调用是异步的,因此返回给 C 调用方的值是未指定的:你可以声明非 void 的 returns(下面的示例使用 "bool"),但 C 端必须将线程安全回调视为返回 void,并忽略其返回值。
⚡️ 性能提示:如需略微提升性能,请直接传递
JSCallback.prototype.ptr,而不是传递 JSCallback 对象:指针
Bun 在 JavaScript 中将指针表示为number 类型。
64 位指针如何存放在 JavaScript 数字中?
64 位指针如何存放在 JavaScript 数字中?
64 位处理器最多支持 52 位可寻址空间。JavaScript 数字支持 53 位可用空间,因此还剩下约 11 位额外空间。为什么不用
BigInt? BigInt 速度更慢。JavaScript 引擎会单独分配 BigInt,因此它们无法放入普通的 JavaScript 值中。如果将 BigInt 传递给函数,它会被转换为 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。当垃圾回收器释放底层的 ArrayBuffer JavaScript 对象后,回调会在之后被调用。
预期的签名与 JavaScriptCore 的 C API 一致:
内存安全
不要在 FFI 之外使用原始指针。Bun 的未来版本可能会添加一个 CLI 标志来禁用bun:ffi。
指针对齐
如果接口期望的是与char 或 u8 不同大小的指针,确保 TypedArray 也是对应的大小。u64* 和 [8]u8* 并不完全等价,因对齐关系不同。
传递指针
当 FFI 函数期望指针时,传入对应大小的TypedArray:
TypedArray 转换为指针。
高难度用法
高难度用法
如果不希望进行自动转换,或者希望获取
TypedArray 内特定字节偏移处的指针,请直接获取指向 TypedArray 的指针: