用途
本技能面向需要在 Bun 运行时中桥接 JavaScript/TypeScript 与原生 C/C++ ABI 的系统程序员。当你使用 bun:ffi 调用原生库、通过 dlopen 加载共享库,或处理 FFI 指针与 C 结构体内存布局时,应遵循其中规定的内存安全与互操作规则。
主要能力
1. 库加载与惰性初始化
关键规则:绝不在模块顶层调用 dlopen。如果库缺失或不兼容,可能导致整个 Bun 进程在启动时崩溃,因此必须使用惰性加载。示例中通过模块级变量缓存 dlopen 结果,并在加载函数中按需初始化。suffix 会自动解析平台后缀:linux 为 so,darwin 为 dylib,win32 为 dll。
import { dlopen, suffix, FFIType, ptr, toArrayBuffer, CString } from 'bun:ffi';
let _lib: ReturnType<typeof dlopen> | null = null;
export function loadMyLib(customPath?: string) {
if (_lib) return _lib;
const libPath = customPath || `libexample.${suffix}`;
_lib = dlopen(libPath, {
add: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
get_data: { args: [], returns: FFIType.ptr },
free_data: { args: [FFIType.ptr], returns: FFIType.void },
});
return _lib;
}
2. 结构体传参限制
关键限制:Bun FFI 原生不支持 C 结构体的按值传递。如果 C 函数要求按值传入结构体(例如 void process_struct(MyStruct s);),必须使用以下变通方案之一,不要尝试直接传入 JS 对象。
方案 A:指针传递(推荐)。尽可能修改 C API 使其接受指针。
// C 侧:void process_struct_ptr(MyStruct *s);
// TS 侧:
const process_struct_ptr = { args: [FFIType.ptr], returns: FFIType.void };
方案 B:参数拆分(仅适用于很小的结构体)。如果无法修改 C API,则严格依据目标架构的 ABI(例如 x86_64 的 System V ABI)将结构体字段拆分为独立参数。
// C 侧:struct Slice { long len; char *ptr; };
// void process_slice(struct Slice s);
// TS 侧(x86_64 System V ABI 将其映射到两个寄存器):
const process_slice = { args: [FFIType.i64, FFIType.ptr], returns: FFIType.void };
3. 安全的内存操作与指针
bun:ffi 中的指针表示为 number(32 位)或 bigint(64 位)。
// 1. 从 TypedArray 获取指针
const arr = new Uint8Array(64);
const arrPtr = ptr(arr); // 返回指向缓冲区的 number 或 bigint
// 2. 读取内存——从指针创建 ArrayBuffer 副本
const dataPtr = lib.symbols.get_data();
const buffer = toArrayBuffer(dataPtr, 64); // 必须指定精确的字节长度
const view = new DataView(buffer);
// 必须始终使用显式字节序(true = 小端)以保证跨平台稳定性
const field1 = view.getBigInt64(0, true); // 偏移 0
const field2 = view.getUint32(8, true); // 偏移 8
// 3. 读取以空字符结尾的 C 字符串
const str = new CString(dataPtr).toString();
4. 结构体内存布局与偏移量
关键防护规则:不要在 TypeScript 中猜测或手动计算结构体偏移量。C 编译器会应用复杂的填充与对齐规则,无法可靠预测。必须让用户使用 C 程序生成偏移量,或假定用户已提供精确的字节偏移。
// 指示用户编译并运行以下程序以获得准确偏移量:
#include <stdio.h>
#include <stddef.h>
#include "target_lib.h"
int main() {
printf("const SIZE = %zu;\n", sizeof(MyStruct));
printf("const OFFSET_FIELD1 = %zu;\n", offsetof(MyStruct, field1));
printf("const OFFSET_FIELD2 = %zu;\n", offsetof(MyStruct, field2));
return 0;
}
5. 资源管理(防止泄漏)
由 C 库分配的原生内存必须显式释放。JS 垃圾回收不管理 FFI 指针。
const dataPtr = lib.symbols.get_data();
try {
const buf = toArrayBuffer(dataPtr, 32);
const view = new DataView(buf);
const id = view.getUint32(0, true);
// 执行工作...
} finally {
// 必须始终在 finally 块中释放原生内存
lib.symbols.free_data(dataPtr);
}
6. 类型安全包装模式
在消费已解析的结构体数据时,将指针读取包装为带已知偏移量的类型化函数。
interface MyData {
id: number;
name: string;
}
// 偏移量必须来自 C 偏移量程序(第 4 节),而非手动计算
const OFFSETS = { id: 0, name: 4 } as const; // 示例:经 offsetof() 验证
const STRUCT_SIZE = 32; // 示例:经 sizeof() 验证
function parseData(dataPtr: number): MyData {
const buf = toArrayBuffer(dataPtr, STRUCT_SIZE);
const view = new DataView(buf);
return {
id: view.getUint32(OFFSETS.id, true),
name: new CString(dataPtr + OFFSETS.name).toString(),
};
}
使用方式
在编写 bun:ffi 绑定时,按上述规则组织代码:先惰性加载库,再声明符号签名,处理结构体时优先改用指针传递,读取内存时显式指定长度与字节序,偏移量由 C 程序生成,原生内存在 finally 中释放,最后用类型安全包装函数对外暴露解析结果。
适用场景
- 通过 bun:ffi 调用原生库
- 使用 dlopen 加载共享库
- 处理 FFI 指针与 C 结构体内存布局
- 编写需要跨平台(linux/darwin/win32)的 FFI 绑定
限制
- Bun FFI 不支持 C 结构体的按值传递,必须使用指针传递或参数拆分变通。
- 结构体偏移量不能手动计算,必须由 C 程序生成或由用户提供。
- 原生内存不受 JS 垃圾回收管理,必须显式释放。
- 关于收费、价格、平台支持范围等更多信息,资料中暂未提供。