Chapter 09

FFI 与单文件可执行

从 JS 直接调 C 动态库,并把整个 Bun 应用打成一个可分发的二进制文件。

9.1 什么是 FFI

FFI(Foreign Function Interface,外部函数接口)指"从一种语言调用另一种语言写的函数"——JavaScript 调 C 库就是典型场景。历史上 Node 靠 node-gyp 编译 C++ 扩展实现 FFI(如 better-sqlite3),开发和部署成本都高。

Bun 提供 bun:ffi 直接加载动态库(.so/.dylib/.dll),声明函数签名就能调——不用写 C++ 胶水层。

9.2 bun:ffi 基本用法

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

const libPath = `./libadd.${suffix}`;   // dylib/so/dll,suffix 跨平台

const { symbols: lib } = dlopen(libPath, {
  add: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
});

console.log(lib.add(40, 2));    // → 42

对应的 C 源:

// add.c
// 编译:cc -shared -o libadd.dylib -fPIC add.c
int add(int a, int b) {
    return a + b;
}

9.3 支持的类型

整数
i8/u8/i16/u16/i32/u32/i64/u64
浮点
f32/f64
指针
ptrcstring(C 字符串→JS string)
复合
buffer(传 Uint8Array)、function(回调函数指针)
特殊
void(无返回值)、bool

9.4 Buffer 传递(零拷贝)

// C:void hash(const uint8_t* data, size_t len, uint8_t* out)
const { symbols } = dlopen(path, {
  hash: { args: [FFIType.ptr, FFIType.u64, FFIType.ptr], returns: FFIType.void },
});

const data = new TextEncoder().encode("hello");
const out = new Uint8Array(32);
symbols.hash(data, data.length, out);

JS 的 TypedArray 直接按指针传给 C——不复制一份。

9.5 cc / tcc 即时编译

Bun 内置了 TinyCC(tcc)——可以把一段 C 源码即时编译成可调用函数,开发阶段特别方便:

import { cc } from "bun:ffi";

const { symbols } = cc({
  source: `
    int add(int a, int b) { return a + b; }
  `,
  symbols: {
    add: { args: ["int", "int"], returns: "int" },
  },
});
console.log(symbols.add(1, 2));

9.6 性能:比 N-API 快

Bun 官方 benchmark 显示:FFI 调用单次开销 < 5ns,比 Node N-API 的 ~50ns 快一个数量级。这让"JS 热路径调用 C 函数"真正可行(例如做加密、图像处理热点)。

9.7 bun build --compile:单文件可执行

把源代码 + 所有依赖 + Bun 运行时本身,一起打进一个二进制文件:

$ bun build src/cli.ts --compile --outfile mycli

$ ./mycli hello
hello

产物是一个 ~50MB 的静态链接二进制——用户不用装 Bun,不用装 Node,直接运行。

典型场景

分发内部工具、CLI、Electron-free 桌面助手、轻量 daemon。运行时启动 <20ms,比 pkg/nexe 的 Node 单文件更小更快。

9.8 交叉编译

在 macOS 上打 Linux / Windows 二进制:

$ bun build src/cli.ts --compile --target=bun-linux-x64       --outfile mycli-linux
$ bun build src/cli.ts --compile --target=bun-linux-arm64     --outfile mycli-linux-arm
$ bun build src/cli.ts --compile --target=bun-darwin-arm64    --outfile mycli-mac-arm
$ bun build src/cli.ts --compile --target=bun-windows-x64     --outfile mycli-win.exe

支持的 target:bun-linux-x64/bun-linux-arm64/bun-linux-x64-baseline(老 CPU)/bun-darwin-x64/bun-darwin-arm64/bun-windows-x64

9.9 精简体积 --minify

$ bun build src/cli.ts --compile --minify --outfile mycli

同时启用 bytecode caching 可进一步加快冷启动:

$ bun build src/cli.ts --compile --bytecode --outfile mycli

9.10 打包静态资源

单文件里捆绑资源:通过 embed-file 的形式:

// src/cli.ts
import cssText from "./styles.css" with { type: "text" };
import logo from "./logo.png";
console.log(cssText.length);
$ bun build src/cli.ts --compile --outfile mycli

文件会被嵌进二进制,运行时自动解出。

9.11 FFI 实战:SQLite 加密扩展

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

const { symbols } = dlopen("./libsqlcipher.so", {
  sqlite3_key: {
    args: [FFIType.ptr, FFIType.cstring, FFIType.i32],
    returns: FFIType.i32,
  },
});

9.12 小结