Bun

FFI

⚠️ 警告bun:ffi实验性的,存在已知错误和限制,不应在生产环境中使用。与 Bun 中的原生代码交互最稳定的方法是编写 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` is either "dylib", "so", or "dll" depending on the platform
// you don't have to use "suffix", it's just there for convenience
const path = `libsqlite3.${suffix}`;

const {
  symbols: {
    sqlite3_libversion, // the function to call
  },
} = dlopen(
  path, // a library name or file path
  {
    sqlite3_libversion: {
      // no arguments, returns a string
      args: [],
      returns: FFIType.cstring,
    },
  },
);

console.log(`SQLite 3 version: ${sqlite3_libversion()}`);

性能

根据我们的基准测试bun:ffi 比通过 Node-API 的 Node.js FFI 大约快 2-6 倍。

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

用法

Zig

// add.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 类型别名
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 字符串有何不同?

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

class CString extends String {
  /**
   * Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary.
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * The ptr to the C string
   *
   * This `CString` instance is a clone of the string, so it
   * is safe to continue using this instance after the `ptr` has been
   * freed.
   */
  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);

// this is safe because myString is a clone
console.log(myString);

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

函数指针

注意 — 尚不支持异步函数。

要从 JavaScript 调用函数指针,请使用 CFunction。如果您将 Node-API (napi) 与 Bun 一起使用,并且已经加载了一些符号,这将非常有用。

import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* somehow, you got this pointer */

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

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

import { linkSymbols } from "bun:ffi";

// getVersionPtrs defined elsewhere
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // Unlike with dlopen(), the names here can be whatever you want
  getMajor: {
    returns: "cstring",
    args: [],

    // Since this doesn't use dlsym(), you have to provide a valid ptr
    // That ptr could be a number or a bigint
    // An invalid pointer will crash your program.
    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)) {
  // found a match!
}

// Sometime later:
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, // Optional. Defaults to `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,
});

// This code runs slightly faster:
setOnResolve(onResolve.ptr);

// Compared to this:
setOnResolve(onResolve);

指针

Bun 将 指针 表示为 JavaScript 中的 number 类型。

64 位指针如何适应 JavaScript number 类型?

要从 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 accepts a `byteOffset` and `byteLength`
// if `byteLength` is not provided, it is assumed to be a null-terminated pointer
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,但它通常更快,因为它不需要创建 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 中的签名相同

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

// with a deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // this is an optional pointer to a callback
  deallocatorContext,

  // this is a pointer to a function
  jsTypedArrayBytesDeallocator,
);

// without a deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // this is a pointer to a function
  jsTypedArrayBytesDeallocator,
);

内存安全

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

指针对齐

如果 API 期望指针大小为 charu8 以外的其他类型,请确保 TypedArray 的大小也相同。由于对齐,u64*[8]u8* 并不完全相同。

传递指针

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

import { dlopen, FFIType } from "bun:ffi";

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // FFIType's can be specified as strings too
    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 will be passed as a pointer
  pixels,

  128,
  128,
);

自动生成的包装器 将指针转换为 TypedArray

硬核模式

读取指针

const out = encode_png(
  // pixels will be passed as a pointer
  pixels,

  // dimensions:
  128,
  128,
);

// assuming it is 0-terminated, it can be read like this:
let png = new Uint8Array(toArrayBuffer(out));

// save it to disk:
await Bun.write("out.png", png);