> ## 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.

# FFI

> 使用 Bun 的 FFI 模块高效地从 JavaScript 调用本地库

<Warning>
  `bun:ffi` 是 **实验性质**的，存在已知的错误和限制，不应该在生产环境中依赖使用。与本地代码交互的最稳定方式是编写一个 [Node-API 模块](/runtime/node-api)。
</Warning>

使用内置的 `bun:ffi` 模块，可以高效地从 JavaScript 调用本地库。它适用于支持 C ABI 的语言（Zig、Rust、C/C++、C#、Nim、Kotlin 等）。

***

## dlopen 用法 (`bun:ffi`)

打印 `sqlite3` 的版本号：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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()}`);
```

***

## 性能

根据 [我们的性能测试](https://github.com/oven-sh/bun/tree/main/bench/ffi)，`bun:ffi` 大约比 Node.js 通过 `Node-API` 使用 FFI 快 2–6 倍。

<Image src="/images/ffi.png" height="400" />

Bun 会生成并即时编译 C 绑定，能高效地在 JavaScript 类型和本地类型之间转换值。为编译 C 代码，Bun 集成了一个小巧且快速的 C 编译器 [TinyCC](https://github.com/TinyCC/tinycc)。

***

## 用法

### Zig

```zig add.zig icon="file-code" theme={"theme":{"light":"github-light","dark":"dracula"}}
pub export fn add(a: i32, b: i32) i32 {
  return a + b;
}
```

编译命令：

```bash terminal icon="terminal" theme={"theme":{"light":"github-light","dark":"dracula"}}
zig build-lib add.zig -dynamic -OReleaseFast
```

传入共享库路径和符号映射到 `dlopen`：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 theme={"theme":{"light":"github-light","dark":"dracula"}}
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
```

编译命令：

```bash theme={"theme":{"light":"github-light","dark":"dracula"}}
rustc --crate-type cdylib add.rs
```

### C++

```c theme={"theme":{"light":"github-light","dark":"dracula"}}
#include <cstdint>

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

编译命令：

```bash theme={"theme":{"light":"github-light","dark":"dracula"}}
zig build-lib add.cpp -dynamic -lc -lc++
```

***

## FFI 类型

支持以下 `FFIType` 值。

| `FFIType`   | C 类型           | 别名                          |
| ----------- | -------------- | --------------------------- |
| buffer      | `char*`        |                             |
| cstring     | `char*`        |                             |
| function    | `(void*)(*)()` | `fn`, `callback`            |
| ptr         | `void*`        | `pointer`, `void*`, `char*` |
| i8          | `int8_t`       | `int8_t`                    |
| i16         | `int16_t`      | `int16_t`                   |
| i32         | `int32_t`      | `int32_t`, `int`            |
| i64         | `int64_t`      | `int64_t`                   |
| i64\_fast   | `int64_t`      |                             |
| u8          | `uint8_t`      | `uint8_t`                   |
| u16         | `uint16_t`     | `uint16_t`                  |
| u32         | `uint32_t`     | `uint32_t`                  |
| u64         | `uint64_t`     | `uint64_t`                  |
| u64\_fast   | `uint64_t`     |                             |
| f32         | `float`        | `float`                     |
| f64         | `double`       | `double`                    |
| bool        | `bool`         |                             |
| char        | `char`         |                             |
| napi\_env   | `napi_env`     |                             |
| napi\_value | `napi_value`   |                             |

注意：`buffer` 参数必须是 `TypedArray` 或 `DataView`。

***

## 字符串

JavaScript 字符串和 C 样式字符串不同，这使得在本地库中使用字符串变得复杂。

<Accordion title="JavaScript 字符串和 C 字符串有什么不同？">
  JavaScript 字符串：

  * UTF16（每个字符 2 字节）或者可能是 latin1，具体取决于 JavaScript 引擎和使用的字符
  * `length` 单独存储
  * 不可变

  C 字符串：

  * 通常是 UTF8（每个字符 1 字节）
  * 不存储长度，而是以 null 结尾，字符串长度即为第一个遇到的 `\0` 字符的位置
  * 可变
</Accordion>

为了解决这个问题，`bun:ffi` 导出了 `CString`，它继承自 JavaScript 内置的 `String`，支持 null 结尾字符串，并添加了若干额外功能：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 字符串：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const myString = new CString(ptr);
```

从指定长度的指针转换为 JavaScript 字符串：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const myString = new CString(ptr, 0, byteLength);
```

`new CString()` 构造函数会克隆 C 字符串，因此在释放 `ptr` 后继续使用 `myString` 是安全的。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
my_library_free(myString.ptr);

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

当用在 `returns` 中时，`FFIType.cstring` 会把指针强制转换为 JavaScript `string`。用在 `args` 中时，`FFIType.cstring` 与 `ptr` 相同。

***

## 函数指针

<Note>目前不支持异步函数</Note>

要从 JavaScript 调用函数指针，请使用 `CFunction`。这在你使用 Bun 的 Node-API (napi) 并且已经加载了一些符号时很有用。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* 你以某种方式获得的指针 */

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

如果有多个函数指针，可以通过 `linkSymbols` 一次性定义它们：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 代码时非常有用。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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`](/runtime/workers)）调用时效果最佳。未来版本的 Bun 会支持从任意线程调用（例如由本地库生成且 Bun 不知晓的新线程）。

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 theme={"theme":{"light":"github-light","dark":"dracula"}}
  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);
  ```
</Note>

***

## 指针

Bun 在 JavaScript 中将[指针](https://en.wikipedia.org/wiki/Pointer_\(computer_programming\))表示为 `number` 类型。

<Accordion title="64 位指针如何存放在 JavaScript 数字中？">
  64 位处理器支持最多约 [52 位可寻址空间](https://en.wikipedia.org/wiki/64-bit_computing#Limits_of_processors)。[JavaScript 数字](https://en.wikipedia.org/wiki/Double-precision_floating-point_format#IEEE_754_double-precision_binary_floating-point_format:_binary64)支持 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.
</Accordion>

将 `TypedArray` 转换为指针：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
```

将指针转换为 `ArrayBuffer`：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 theme={"theme":{"light":"github-light","dark":"dracula"}}
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 theme={"theme":{"light":"github-light","dark":"dracula"}}
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`。

| `FFIType` | `read` 函数  |
| --------- | ---------- |
| ptr       | `read.ptr` |
| i8        | `read.i8`  |
| i16       | `read.i16` |
| i32       | `read.i32` |
| i64       | `read.i64` |
| u8        | `read.u8`  |
| u16       | `read.u16` |
| u32       | `read.u32` |
| u64       | `read.u64` |
| f32       | `read.f32` |
| f64       | `read.f64` |

### 内存管理

`bun:ffi` 不会替你管理内存。你必须在用完后释放内存。

#### 来自 JavaScript

若想追踪 `TypedArray` 何时不再使用，可以使用 [FinalizationRegistry](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)。

#### 来自 C、Rust、Zig 等

若想从 C 或 FFI 端追踪 `TypedArray` 何时不再使用，可以给 `toArrayBuffer` 或 `toBuffer` 传递一个回调和可选上下文指针。垃圾回收释放底层的 `ArrayBuffer` JavaScript 对象时，会调用该函数。

预期的签名与 [JavaScriptCore 的 C API](https://developer.apple.com/documentation/javascriptcore/jstypedarraybytesdeallocator?language=objc) 一致：

```c theme={"theme":{"light":"github-light","dark":"dracula"}}
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
```

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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`：

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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,
);
```

[自动生成的包装器](https://github.com/oven-sh/bun/blob/6a65631cbdcae75bfa1e64323a6ad613a922cd1a/src/bun.js/ffi.exports.js#L180-L182)会把指针转换为 `TypedArray`。

<Accordion title="高难度用法">
  如果不想自动转换，或想要 `TypedArray` 内特定字节偏移的指针，也可以直接获取 `TypedArray` 的指针：

  ```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
  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,
  );
  ```
</Accordion>

### 读取指针

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const out = encode_png(
  // pixels 会作为指针传入
  pixels,

  // 尺寸：
  128,
  128,
);

// 假设这是以 0 结尾，可以这样读取：
let png = new Uint8Array(toArrayBuffer(out));

// 写入磁盘：
await Bun.write("out.png", png);
```
