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 version: ${sqlite3_libversion()}`);

パフォーマンス

私たちのベンチマーク によると、bun:ffiNode-API を介した Node.js FFI よりも約 2〜6 倍高速です。

Bun は、JavaScript 型とネイティブ型の間で値を効率的に変換する C バインディングを生成し、ジャストインタイムコンパイルします。C をコンパイルするために、Bun は小さくて高速な C コンパイラーである TinyCC を埋め込んでいます。


使用方法

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*)(*)()fncallback
ptrvoid*pointervoid*char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_tint
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 引数は TypedArray または DataView である必要があります。


文字列

JavaScript 文字列と C 風の文字列は異なるため、ネイティブライブラリで文字列を使用するのが複雑になります。

JavaScript 文字列と C 文字列の違い

JavaScript 文字列:

  • UTF16(1 文字あたり 2 バイト)または場合によっては latin1(JavaScript エンジンと使用される文字による)
  • length は別に保存される
  • 不変

C 文字列:

  • UTF8(1 文字あたり 1 バイト)、通常
  • 長さは保存されない。代わりに、文字列は null 終端であり、長さは最初の \0 のインデックス
  • 可変

これを解決するために、bun:ffi は JavaScript の組み込みの String を拡張して null 終端文字列をサポートし、いくつかの追加機能を追加する CString をエクスポートします。

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` オブジェクトの代わりに `JSCallback.prototype.ptr` を直接渡します。
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);

ポインターからデータを読み取るには、2 つのオプションがあります。長寿命のポインターの場合は、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 がいつ使用されなくなったかを追跡したい場合は、toArrayBuffer または toBuffer にコールバックとオプションのコンテキストポインターを渡すことができます。この関数は、ガベージコレクターが基盤となる 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 は bun:ffi を無効にする CLI フラグを追加する可能性があります。

ポインターアライメント

API が char または u8 以外のサイズのポインターを期待する場合は、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 by www.bunjs.com.cn 編集