Documentation Index Fetch the complete documentation index at: https://bun.zhcndoc.com/llms.txt
Use this file to discover all available pages before exploring further.
bun:ffi 是 实验性质 的,存在已知的错误和限制,不应该在生产环境中依赖使用。与本地代码交互的最稳定方式是编写一个 Node-API 模块 。
使用内置的 bun:ffi 模块,可以高效地从 JavaScript 调用本地库。它适用于支持 C ABI 的语言(Zig、Rust、C/C++、C#、Nim、Kotlin 等)。
dlopen 用法 (bun:ffi)
打印 sqlite3 的版本号:
import { dlopen, FFIType, suffix } from " bun:ffi " ;
// `suffix` 根据平台可能是 "dylib"、"so" 或 "dll"
// 你不必一定要使用 "suffix",它只是为方便准备的
const path = `libsqlite3. ${ suffix } ` ;
const {
symbols : {
sqlite3_libversion , // 要调用的函数
},
} = dlopen (
path, // 库名或文件路径
{
sqlite3_libversion : {
// 无参数,返回字符串
args : [],
returns : FFIType.cstring,
},
},
);
console. log ( `SQLite 3 版本: ${ sqlite3_libversion () } ` );
根据 我们的性能测试 ,bun:ffi 大约比 Node.js 通过 Node-API 使用 FFI 快 2–6 倍。
Bun 会生成并即时编译 C 绑定,能高效地在 JavaScript 类型和本地类型之间转换值。为编译 C 代码,Bun 集成了一个小巧且快速的 C 编译器 TinyCC 。
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 ));
Rust
// add.rs
#[no_mangle]
pub extern "C" fn add (a : i32 , b : i32 ) -> i32 {
a + b
}
编译命令:
rustc --crate-type cdylib add.rs
C++
#include < cstdint >
extern " C " int32_t add ( int32_t a , int32_t b ) {
return a + b;
}
编译命令:
zig build-lib add.cpp -dynamic -lc -lc++
FFI 类型
支持以下 FFIType 值。
FFITypeC 类型 别名 buffer char*cstring char*function (void*)(*)()fn, callbackptr void*pointer, void*, char*i8 int8_tint8_ti16 int16_tint16_ti32 int32_tint32_t, inti64 int64_tint64_ti64_fast int64_tu8 uint8_tuint8_tu16 uint16_tuint16_tu32 uint32_tuint32_tu64 uint64_tuint64_tu64_fast uint64_tf32 floatfloatf64 doubledoublebool boolchar charnapi_env napi_envnapi_value napi_value
注意:buffer 参数必须是 TypedArray 或 DataView。
字符串
JavaScript 字符串和 C 样式字符串不同,这使得在本地库中使用字符串变得复杂。
JavaScript 字符串和 C 字符串有什么不同?
JavaScript 字符串:
UTF16(每个字符 2 字节)或者可能是 latin1,具体取决于 JavaScript 引擎和使用的字符
length 单独存储
不可变
C 字符串:
通常是 UTF8(每个字符 1 字节)
不存储长度,而是以 null 结尾,字符串长度即为第一个遇到的 \0 字符的位置
可变
为了解决这个问题,bun:ffi 导出了 CString,它继承自 JavaScript 内置的 String,支持 null 结尾字符串,并添加了若干额外功能:
class CString extends String {
/**
* 给定一个 `ptr`,会自动寻找结束的 `\0` 字符,
* 必要时从 UTF-8 转码为 UTF-16。
*/
constructor ( ptr : number , byteOffset ?: number , byteLength ?: number ) : string ;
/**
* C 字符串的指针
*
* 此 `CString` 实例是字符串的克隆,
* 因此在 `ptr` 被释放后继续使用此实例是安全的。
*/
ptr : number ;
byteOffset ?: number ;
byteLength ?: number ;
}
从 null 结尾字符串指针转换为 JavaScript 字符串:
const myString = new CString (ptr);
从指定长度的指针转换为 JavaScript 字符串:
const myString = new CString (ptr, 0 , byteLength);
new CString() 构造函数会克隆 C 字符串,因此在释放 ptr 后继续使用 myString 是安全的。
my_library_free (myString.ptr);
// 这很安全,因为 myString 是克隆的
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 = /* 你以某种方式获得的指针 */
const getVersion = new CFunction ({
returns : " cstring " ,
args : [],
ptr : myNativeLibraryGetVersion,
});
getVersion ();
如果有多个函数指针,可以通过 linkSymbols 一次性定义它们:
import { linkSymbols } from " bun:ffi " ;
// getVersionPtrs 在其他地方定义
const [ majorPtr , minorPtr , patchPtr ] = getVersionPtrs ();
const lib = linkSymbols ({
// 和 dlopen() 不同,这里的名称可以随意
getMajor : {
returns : " cstring " ,
args : [],
// 由于不用 dlsym(),必须提供一个有效的指针
// 该指针可以是 number 或 bigint
// 无效指针会导致程序崩溃。
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)) {
// 找到匹配!
}
// 过一会儿:
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 , // 可选,默认为 `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,
});
// 这个调用稍快:
setOnResolve (onResolve.ptr);
// 比传入对象:
setOnResolve (onResolve);
Bun 在 JavaScript 中将指针 表示为 number 类型。
64 位指针如何存放在 JavaScript 数字中?
64 位处理器支持最多约 52 位可寻址空间 。JavaScript 数字 支持 53 位可用空间,多出大约 11 位。 为何不用 BigInt? BigInt 性能较慢。JavaScript 引擎对 BigInt 单独分配内存,无法装进普通的 JavaScript 值。如果传入 BigInt,它会被转换成 number。Windows Note : The Windows API type HANDLE does not represent a virtual address, and using ptr for it will not work as expected. Use u64 to safely represent HANDLE values.
将 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 接受 `byteOffset` 和 `byteLength`
// 如果不传 `byteLength`,默认为 null 结尾指针
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。
FFITyperead 函数ptr read.ptri8 read.i8i16 read.i16i32 read.i32i64 read.i64u8 read.u8u16 read.u16u32 read.u32u64 read.u64f32 read.f32f64 read.f64
内存管理
bun:ffi 不会替你管理内存。你必须在用完后释放内存。
来自 JavaScript
若想追踪 TypedArray 何时不再使用,可以使用 FinalizationRegistry 。
来自 C、Rust、Zig 等
若想从 C 或 FFI 端追踪 TypedArray 何时不再使用,可以给 toArrayBuffer 或 toBuffer 传递一个回调和可选上下文指针。垃圾回收释放底层的 ArrayBuffer JavaScript 对象时,会调用该函数。
预期的签名与 JavaScriptCore 的 C API 一致:
typedef void ( * JSTypedArrayBytesDeallocator)( void * bytes, void * deallocatorContext);
import { toArrayBuffer } from " bun:ffi " ;
// 传递 deallocatorContext:
toArrayBuffer (
bytes,
byteOffset,
byteLength,
// 这是一个指向上下文的可选指针
deallocatorContext,
// 这是一个指向函数的指针
jsTypedArrayBytesDeallocator,
);
// 不传 deallocatorContext:
toArrayBuffer (
bytes,
byteOffset,
byteLength,
// 这是一个指向函数的指针
jsTypedArrayBytesDeallocator,
);
内存安全
在 FFI 之外使用原始指针极不推荐。未来的 Bun 版本可能会加入 CLI 标志以禁用 bun:ffi。
指针对齐
如果接口期望的是与 char 或 u8 不同大小的指针,确保 TypedArray 也是对应的大小。u64* 和 [8]u8* 并不完全等价,因对齐关系不同。
传递指针
当 FFI 函数期望指针时,传入对应大小的 TypedArray:
import { dlopen, FFIType } from " bun:ffi " ;
const {
symbols : { encode_png },
} = dlopen (myLibraryPath, {
encode_png : {
// FFIType 也可以用字符串形式指定
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 会作为指针传入
pixels,
128 ,
128 ,
);
自动生成的包装器 会把指针转换为 TypedArray。
如果不想自动转换,或想要 TypedArray 内特定字节偏移的指针,也可以直接获取 TypedArray 的指针: import { dlopen, FFIType, ptr } from " bun:ffi " ;
const {
symbols : { encode_png },
} = dlopen (myLibraryPath, {
encode_png : {
// FFIType 也可以用字符串形式指定
args : [ " ptr " , " u32 " , " u32 " ],
returns : FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray ( 128 * 128 * 4 );
pixels. fill ( 254 );
// 这里返回的是数字!不是 BigInt!
const myPtr = ptr (pixels);
const out = encode_png (
myPtr,
// 尺寸:
128 ,
128 ,
);
读取指针
const out = encode_png (
// pixels 会作为指针传入
pixels,
// 尺寸:
128 ,
128 ,
);
// 假设这是以 0 结尾,可以这样读取:
let png = new Uint8Array ( toArrayBuffer (out));
// 写入磁盘:
await Bun. write ( " out.png " , png);