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 字符串和 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 线程。在那里,引擎会像处理普通回调一样转换参数(i64/u64/usize 参数会以 BigInt 形式传入;指针会以 number 形式传入),并运行你的函数。由于从 C 的角度来看,调用是异步的,因此返回给 C 调用方的值未指定。你可以声明非 void 的 returns(下例使用 "bool"),但 C 端必须将线程安全回调视为返回 void,并忽略其返回值。
⚡️ 性能提示:如需略微提升性能,请直接传递
JSCallback.prototype.ptr,而不是传递 JSCallback 对象:指针
Bun 在 JavaScript 中将指针表示为number 类型。
64 位指针如何存放在 JavaScript 数字中?
64 位指针如何存放在 JavaScript 数字中?
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 的指针: