SuperAPI AI 工具集

bun-ffi-interop-pattern

编程与开发

查看项目

更新于 10-16 04:22

技能介绍

用途

本技能面向需要在 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 垃圾回收管理,必须显式释放。
  • 关于收费、价格、平台支持范围等更多信息,资料中暂未提供。