Skip to content

使用内置的 bun:ffi 模块高效地从 JavaScript 调用原生库。它适用于支持 C ABI 的语言(Zig、Rust、C/C++、C#、Nim、Kotlin 等)。


dlopen 用法(bun:ffi

要打印 sqlite3 的版本号:

ts
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-API 的 Node.js FFI 快 2-6 倍。

Bun 生成并即时编译 C 绑定,高效地在 JavaScript 类型和原生类型之间转换值。为了编译 C,Bun 嵌入了 TinyCC,一个小型快速的 C 编译器。


用法

Zig

zig
pub export fn add(a: i32, b: i32) i32 {
  return a + b;
}

编译:

bash
zig build-lib add.zig -dynamic -OReleaseFast

将共享库的路径和符号映射传递给 dlopen 导入:

ts
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

rust
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

编译:

bash
rustc --crate-type cdylib add.rs

C++

c
#include <cstdint>

extern "C" int32_t add(int32_t a, int32_t b) {
    return a + b;
}

编译:

bash
zig build-lib add.cpp -dynamic -lc -lc++

FFI 类型

支持以下 FFIType 值。

FFITypeC 类型别名
bufferchar*
cstringchar*
function(void*)(*)()fn, callback
ptrvoid*pointer, void*, char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t, int
i64int64_tint64_t
i64_fastint64_t
u8uint8_tuint8_t
u16uint16_tuint16_t
u32uint32_tuint32_t
u64uint64_tuint64_t
u64_fastuint64_t
f32floatfloat
f64doubledouble
boolbool
charchar
napi_envnapi_env
napi_valuenapi_value

注意:buffer 参数必须是 TypedArrayDataView


字符串

JavaScript 字符串和 C 风格字符串不同,这使得使用原生库处理字符串变得复杂。

JavaScript 字符串和 C 字符串有何不同?

JavaScript 字符串:

  • UTF16(每个字母 2 字节)或可能是 latin1,取决于 JavaScript 引擎和使用的字符
  • length 单独存储
  • 不可变

C 字符串:

  • UTF8(每个字母 1 字节),通常
  • 长度不存储。相反,字符串以 null 结尾,这意味着长度是它找到的第一个 \0 的索引
  • 可变

为了解决这个问题,bun:ffi 导出 CString,它扩展了 JavaScript 的内置 String 以支持 null 结尾的字符串并添加一些额外功能:

ts
class CString extends String {
  /**
   * 给定 `ptr`,这将自动搜索结束 `\0` 字符并在需要时从 UTF-8 转码为 UTF-16。
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * C 字符串的 ptr
   *
   * 此 `CString` 实例是字符串的克隆,所以它是
   * 安全的,在 `ptr` 被释放后继续使用此实例。
   */
  ptr: number;
  byteOffset?: number;
  byteLength?: number;
}

要将 null 结尾的字符串指针转换为 JavaScript 字符串:

ts
const myString = new CString(ptr);

要将已知长度的指针转换为 JavaScript 字符串:

ts
const myString = new CString(ptr, 0, byteLength);

new CString() 构造函数克隆 C 字符串,所以在 ptr 被释放后继续使用 myString 是安全的。

ts
my_library_free(myString.ptr);

// 这是安全的,因为 myString 是克隆
console.log(myString);

当在 returns 中使用时,FFIType.cstring 将指针强制转换为 JavaScript string。当在 args 中使用时,FFIType.cstringptr 相同。


函数指针

NOTE

异步函数尚不支持

要从 JavaScript 调用函数指针,使用 CFunction。如果你在 Bun 中使用 Node-API (napi) 并且已经加载了一些符号,这很有用。

ts
import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* 以某种方式,你获得了这个指针 */

const getVersion = new CFunction({
  returns: "cstring",
  args: [],
  ptr: myNativeLibraryGetVersion,
});
getVersion();

如果你有多个函数指针,可以使用 linkSymbols 一次性定义它们:

ts
import { linkSymbols } from "bun:ffi";

// getVersionPtrs 在其他地方定义
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // 与 dlopen() 不同,这里的名称可以是任何你想要的
  getMajor: {
    returns: "cstring",
    args: [],

    // 由于这不使用 dlsym(),你必须提供有效的 ptr
    // 该 ptr 可以是数字或 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 代码时很有用。

ts
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 不知道的新线程)。

ts
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
  returns: "bool",
  args: ["ptr", "usize"],
  threadsafe: true, // 可选。默认为 `false`
});

NOTE

**⚡️ 性能提示** — 为了轻微的性能提升,直接传递 `JSCallback.prototype.ptr` 而不是 `JSCallback` 对象:
ts
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

要将 TypedArray 转换为指针:

ts
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

要将指针转换为 ArrayBuffer

ts
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

ts
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

ts
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,但它通常更快,因为它不需要创建 DataViewArrayBuffer

FFITyperead 函数
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.f64

内存管理

bun:ffi 不为你管理内存。你必须在完成后释放内存。

从 JavaScript

如果你想从 JavaScript 跟踪 TypedArray 何时不再使用,你可以使用 FinalizationRegistry

从 C、Rust、Zig 等

如果你想从 C 或 FFI 跟踪 TypedArray 何时不再使用,你可以将回调和可选的上下文指针传递给 toArrayBuffertoBuffer。此函数在稍后的某个时间点调用,一旦垃圾回收器释放底层的 ArrayBuffer JavaScript 对象。

预期的签名与 JavaScriptCore 的 C API 中相同:

c
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
ts
import { toArrayBuffer } from "bun:ffi";

// 带有 deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // 这是指向回调的可选指针
  deallocatorContext,

  // 这是指向函数的指针
  jsTypedArrayBytesDeallocator,
);

// 不带 deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // 这是指向函数的指针
  jsTypedArrayBytesDeallocator,
);

内存安全

强烈不建议在 FFI 之外使用原始指针。未来版本的 Bun 可能会添加 CLI 标志来禁用 bun:ffi

指针对齐

如果 API 期望指针大小不是 charu8,请确保 TypedArray 也是该大小。由于对齐,u64*[8]u8* 不完全相同。

传递指针

在 FFI 函数期望指针的地方,传递等效大小的 TypedArray

ts
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 的指针:

ts
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,
);

读取指针

ts
const out = encode_png(
  // pixels 将作为指针传递
  pixels,

  // 尺寸:
  128,
  128,
);

// 假设它是 0 结尾的,可以这样读取:
let png = new Uint8Array(toArrayBuffer(out));

// 保存到磁盘:
await Bun.write("out.png", png);

Bun学习网由www.bunjs.com.cn整理维护