ホームページ  >  記事  >  ウェブフロントエンド  >  Bun FFI を使用する方法と使用する必要があります

Bun FFI を使用する方法と使用する必要があります

Linda Hamilton
Linda Hamiltonオリジナル
2024-11-11 10:53:02770ブラウズ

How to and Should you use Bun FFI

私たちが何を達成しようとしているのか

bun で実行される JavaScript アプリケーションがあり、最適化したいボトルネックが特定されたとします。
よりパフォーマンスの高い言語で書き直すことが、必要な解決策になる可能性があります。

Bun は最新の JS ランタイムとして、C、C、Rust、Zig などの C ABI の公開をサポートする他の言語で書かれたライブラリを呼び出すための外部関数インターフェイス (FFI) をサポートしています。

この投稿では、それをどのように使用できるかを検討し、それから利益を得られるかどうかを結論付けます。

ライブラリをJavaScriptにリンクする方法

この例では Rust を使用しています。 C バインディングを使用した共有ライブラリの作成は、他の言語では見た目が異なりますが、考え方は同じです。

JS側から

Bun は bun:ffi モジュールを通じて FFI API を公開します。

エントリポイントは dlopen 関数です。ライブラリ ファイル (Linux の場合は .so、macOS の場合は .dylib、Windows の場合は .dll の拡張子が付いたビルド出力) への絶対パスまたは 現在の作業ディレクトリからの相対パス と、次のようなオブジェクトを受け取ります。インポートする関数のシグネチャ。
これは、ライブラリが必要なくなったときにライブラリを閉じるために使用できる close メソッドと、選択した関数を含むオブジェクトであるシンボル プロパティを持つオブジェクトを返します。

import {
  dlopen,
  FFIType,
  read,
  suffix,
  toArrayBuffer,
  type Pointer,
} from "bun:ffi";

// Both your script and your library don't typically change their locations
// Use `import.meta.dirname` to make your script independent from the cwd
const DLL_PATH =
  import.meta.dirname + `/../../rust-lib/target/release/library.${suffix}`;

function main() {
  // Deconstruct object to get functions
  // but collect `close` method into object
  // to avoid using `this` in a wrong scope
  const {
    symbols: { do_work },
    ...dll
  } = dlopen(DLL_PATH, {
    do_work: {
      args: [FFIType.ptr, FFIType.ptr, "usize", "usize"],
      returns: FFIType.void,
    },
  });

  /* ... */

  // It is unclear whether it is required or recommended to call `close`
  // an example says `JSCallback` instances specifically need to be closed
  // Note that using `symbols` after calling `close` is undefined behaviour
  dll.close();
}

main();

FFI境界を通過するデータの受け渡し

お気づきのとおり、bun が FFI を通じて受け入れるサポートされる型は、ポインターを含む数値に限定されています。
特に、bun バージョン 1.1.34 の時点では size_t または usesize のコードは存在しますが、サポートされる型のリストに size_t または usesize がありません。

Bun は、C 文字列よりも複雑なデータを渡すことについては何も提供しません。つまり、ポインターを自分で操作する必要があります。

JavaScript から Rust にポインタを渡す方法を見てみましょう ...

{
  reconstruct_slice: {
    args: [FFIType.ptr, "usize"],
    returns: FFIType.void,
  },
}

const array = new BigInt64Array([0, 1, 3]);
// Bun automatically converts `TypedArray`s into pointers
reconstruct_slice(array, array.length);
/// Reconstruct a `slice` that was initialized in JavaScript
unsafe fn reconstruct_slice(
    array_ptr: *const i64,
    length: libc::size_t,
) -> &[i64] {
    // Even though here it's not null, it's good practice to check
    assert!(!array_ptr.is_null());
    // Unaligned pointer can lead to undefined behaviour
    assert!(array_ptr.is_aligned());
    // Check that the array doesn't "wrap around" the address space
    assert!(length < usize::MAX / 4);
    let _: &[i64] = unsafe { slice::from_raw_parts(array_ptr, length) };
}

...Rust から JavaScript にポインタを返す方法。

{
  allocate_buffer: {
    args: [],
    returns: FFIType.ptr,
  },
  as_pointer: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
}

// Hardcoding this value for 64-bit systems
const BYTES_IN_PTR = 8;

const box: Pointer = allocate_buffer()!;
const ptr: number = read.ptr(box);
// Reading the value next to `ptr`
const length: number = read.ptr(box, BYTES_IN_PTR);
// Hardcoding `byteOffset` to be 0 because Rust guarantees that
// Buffer holds `i32` values which take 4 bytes
// Note how we need to call a no-op function `as_pointer` because
// `toArrayBuffer` takes a `Pointer` but `read.ptr` returns a `number`
const _buffer = toArrayBuffer(as_pointer(ptr)!, 0, length * 4);
#[no_mangle]
pub extern "C" fn allocate_buffer() -> Box<[usize; 2]> {
    let buffer: Vec<i32> = vec![0; 10];
    let memory: ManuallyDrop<Vec<i32>> = ManuallyDrop::new(buffer);
    let ptr: *const i32 = memory.as_ptr();
    let length: usize = memory.len();
    // Unlike a `Vec`, `Box` is FFI compatible and will not drop
    // its data when crossing the FFI
    // Additionally, a `Box<T>` where `T` is `Sized` will be a thin pointer
    Box::new([ptr as usize, length])
}

#[no_mangle]
pub const extern "C" fn as_pointer(ptr: usize) -> usize {
    ptr
}

Rust は JS が反対側のデータの所有権を取得していることを知らないため、ManuallyDrop を使用してヒープ上のデータを割り当て解除しないように明示的に指示する必要があります。メモリを管理する他の言語も同様のことを行う必要があります。

メモリ管理

ご覧のとおり、JS と Rust の両方でメモリを割り当てることは可能ですが、どちらも他のメモリを安全に管理することはできません。

メモリをどこにどのように割り当てるかを選択しましょう。

Rustでの割り当て

メモリのクリーンアップを JS から Rust に委任するには 3 つの方法があり、すべてに長所と短所があります。

FinalizationRegistry を使用する

FinalizationRegistry を使用して、JavaScript でオブジェクトを追跡することにより、ガベージ コレクション中にクリーンアップ コールバックを要求します。

import {
  dlopen,
  FFIType,
  read,
  suffix,
  toArrayBuffer,
  type Pointer,
} from "bun:ffi";

// Both your script and your library don't typically change their locations
// Use `import.meta.dirname` to make your script independent from the cwd
const DLL_PATH =
  import.meta.dirname + `/../../rust-lib/target/release/library.${suffix}`;

function main() {
  // Deconstruct object to get functions
  // but collect `close` method into object
  // to avoid using `this` in a wrong scope
  const {
    symbols: { do_work },
    ...dll
  } = dlopen(DLL_PATH, {
    do_work: {
      args: [FFIType.ptr, FFIType.ptr, "usize", "usize"],
      returns: FFIType.void,
    },
  });

  /* ... */

  // It is unclear whether it is required or recommended to call `close`
  // an example says `JSCallback` instances specifically need to be closed
  // Note that using `symbols` after calling `close` is undefined behaviour
  dll.close();
}

main();
{
  reconstruct_slice: {
    args: [FFIType.ptr, "usize"],
    returns: FFIType.void,
  },
}

const array = new BigInt64Array([0, 1, 3]);
// Bun automatically converts `TypedArray`s into pointers
reconstruct_slice(array, array.length);
長所
  • 簡単です
短所
  • ガベージ コレクションはエンジン固有であり、非決定的です
  • クリーンアップ コールバックが呼び出される保証はまったくありません

toArrayBuffer の FinalizationCallback パラメータを使用する

ガベージ コレクションの追跡を bun に委任して、クリーンアップ コールバックを呼び出します。
4 つのパラメータを toArrayBuffer に渡す場合、4 番目のパラメータはクリーンアップ時に呼び出される C 関数である必要があります。
ただし、5 つのパラメーターを渡す場合、5 番目のパラメーターは関数であり、4 番目のパラメーターは渡されるコンテキスト ポインターである必要があります。

/// Reconstruct a `slice` that was initialized in JavaScript
unsafe fn reconstruct_slice(
    array_ptr: *const i64,
    length: libc::size_t,
) -> &[i64] {
    // Even though here it's not null, it's good practice to check
    assert!(!array_ptr.is_null());
    // Unaligned pointer can lead to undefined behaviour
    assert!(array_ptr.is_aligned());
    // Check that the array doesn't "wrap around" the address space
    assert!(length < usize::MAX / 4);
    let _: &[i64] = unsafe { slice::from_raw_parts(array_ptr, length) };
}
{
  allocate_buffer: {
    args: [],
    returns: FFIType.ptr,
  },
  as_pointer: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
}

// Hardcoding this value for 64-bit systems
const BYTES_IN_PTR = 8;

const box: Pointer = allocate_buffer()!;
const ptr: number = read.ptr(box);
// Reading the value next to `ptr`
const length: number = read.ptr(box, BYTES_IN_PTR);
// Hardcoding `byteOffset` to be 0 because Rust guarantees that
// Buffer holds `i32` values which take 4 bytes
// Note how we need to call a no-op function `as_pointer` because
// `toArrayBuffer` takes a `Pointer` but `read.ptr` returns a `number`
const _buffer = toArrayBuffer(as_pointer(ptr)!, 0, length * 4);
長所
  • JavaScript からロジックを委任する
短所
  • 多くの定型文とメモリリークの可能性
  • toArrayBuffer の型アノテーションがありません
  • ガベージ コレクションはエンジン固有であり、非決定的です
  • クリーンアップ コールバックが呼び出される保証はまったくありません

メモリを手動で管理する

メモリが不要になったら、自分でメモリを削除してください。
幸いなことに、TypeScript には、これと using キーワードに非常に役立つ Disposable インターフェイスがあります。
これは、キーワードを使用した Python または C# のキーワードと同等です。

ドキュメントを参照してください

  • TypeScript 5.2 変更ログ
  • 使用するためのプルリクエスト
#[no_mangle]
pub extern "C" fn allocate_buffer() -> Box<[usize; 2]> {
    let buffer: Vec<i32> = vec![0; 10];
    let memory: ManuallyDrop<Vec<i32>> = ManuallyDrop::new(buffer);
    let ptr: *const i32 = memory.as_ptr();
    let length: usize = memory.len();
    // Unlike a `Vec`, `Box` is FFI compatible and will not drop
    // its data when crossing the FFI
    // Additionally, a `Box<T>` where `T` is `Sized` will be a thin pointer
    Box::new([ptr as usize, length])
}

#[no_mangle]
pub const extern "C" fn as_pointer(ptr: usize) -> usize {
    ptr
}
{
  drop_buffer: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
}

const registry = new FinalizationRegistry((box: Pointer): void => {
  drop_buffer(box);
});
registry.register(buffer, box);
長所
  • クリーンアップは確実に実行されます
  • いつメモリを削除するかを制御できます
短所
  • 使い捨てインターフェースの定型オブジェクト
  • 手動でメモリを削除すると、ガベージ コレクターを使用するよりも時間がかかります
  • バッファの所有権を譲渡したい場合は、コピーを作成して元のバッファを削除する必要があります

JSで配置する

割り当て解除が自動的に処理されるため、これははるかに簡単かつ安全です。

しかし、重大な欠点があります。
Rust では JavaScript のメモリを管理できないため、バッファの容量を超えると割り当て解除が発生するため、それを超えることはできません。つまり、Rust に渡す前にバッファ サイズを知っておく必要があります。
必要なバッファの数が事前にわからないと、割り当てるためだけに FFI を行ったり来たりすることになり、多くのオーバーヘッドが発生します。

/// # Safety
///
/// This call assumes neither the box nor the buffer have been mutated in JS
#[no_mangle]
pub unsafe extern "C" fn drop_buffer(raw: *mut [usize; 2]) {
    let box_: Box<[usize; 2]> = unsafe { Box::from_raw(raw) };
    let ptr: *mut i32 = box_[0] as *mut i32;
    let length: usize = box_[1];
    let buffer: Vec<i32> = unsafe { Vec::from_raw_parts(ptr, length, length) };
    drop(buffer);
}
{
  box_value: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
  drop_box: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
  drop_buffer: {
    args: [FFIType.ptr, FFIType.ptr],
    returns: FFIType.void,
  },
}

// Bun expects the context to specifically be a pointer
const finalizationCtx: Pointer = box_value(length)!;

// Note that despite the presence of these extra parameters in the docs,
// they're absent from `@types/bun`
//@ts-expect-error see above
const buffer = toArrayBuffer(
  as_pointer(ptr)!,
  0,
  length * 4,
  //@ts-expect-error see above
  finalizationCtx,
  drop_buffer,
);
// Don't leak the box used to pass buffer through FFI
drop_box(box);

文字列に関する補足

ライブラリからの出力が文字列である場合、通常 JavaScript エンジンは内部で UTF-16 を使用するため、文字列ではなく u16 のベクトルを返すというマイクロ最適化を検討したかもしれません。

しかし、それは間違いです。文字列を C 文字列に変換し、bun の cstring 型を使用する方が若干高速になるからです。
これは、優れたベンチマーク ライブラリ mitata を使用して実行されたベンチマークです

import {
  dlopen,
  FFIType,
  read,
  suffix,
  toArrayBuffer,
  type Pointer,
} from "bun:ffi";

// Both your script and your library don't typically change their locations
// Use `import.meta.dirname` to make your script independent from the cwd
const DLL_PATH =
  import.meta.dirname + `/../../rust-lib/target/release/library.${suffix}`;

function main() {
  // Deconstruct object to get functions
  // but collect `close` method into object
  // to avoid using `this` in a wrong scope
  const {
    symbols: { do_work },
    ...dll
  } = dlopen(DLL_PATH, {
    do_work: {
      args: [FFIType.ptr, FFIType.ptr, "usize", "usize"],
      returns: FFIType.void,
    },
  });

  /* ... */

  // It is unclear whether it is required or recommended to call `close`
  // an example says `JSCallback` instances specifically need to be closed
  // Note that using `symbols` after calling `close` is undefined behaviour
  dll.close();
}

main();
{
  reconstruct_slice: {
    args: [FFIType.ptr, "usize"],
    returns: FFIType.void,
  },
}

const array = new BigInt64Array([0, 1, 3]);
// Bun automatically converts `TypedArray`s into pointers
reconstruct_slice(array, array.length);
/// Reconstruct a `slice` that was initialized in JavaScript
unsafe fn reconstruct_slice(
    array_ptr: *const i64,
    length: libc::size_t,
) -> &[i64] {
    // Even though here it's not null, it's good practice to check
    assert!(!array_ptr.is_null());
    // Unaligned pointer can lead to undefined behaviour
    assert!(array_ptr.is_aligned());
    // Check that the array doesn't "wrap around" the address space
    assert!(length < usize::MAX / 4);
    let _: &[i64] = unsafe { slice::from_raw_parts(array_ptr, length) };
}

WebAssembly についてはどうでしょうか?

WebAssembly という部屋の象に対処する時間です。
C ABI を扱うよりも、優れた既存の WASM バインディングを選択する必要がありますか?

答えはおそらくどちらでもありません

実際にそれだけの価値があるのでしょうか?

コードベースに別の言語を導入するには、DX およびパフォーマンスの観点から価値のある単一のボトルネック以上のものが必要になります。

これは、JS、WASM、Rust の単純な範囲関数のベンチマークです。

{
  allocate_buffer: {
    args: [],
    returns: FFIType.ptr,
  },
  as_pointer: {
    args: ["usize"],
    returns: FFIType.ptr,
  },
}

// Hardcoding this value for 64-bit systems
const BYTES_IN_PTR = 8;

const box: Pointer = allocate_buffer()!;
const ptr: number = read.ptr(box);
// Reading the value next to `ptr`
const length: number = read.ptr(box, BYTES_IN_PTR);
// Hardcoding `byteOffset` to be 0 because Rust guarantees that
// Buffer holds `i32` values which take 4 bytes
// Note how we need to call a no-op function `as_pointer` because
// `toArrayBuffer` takes a `Pointer` but `read.ptr` returns a `number`
const _buffer = toArrayBuffer(as_pointer(ptr)!, 0, length * 4);
#[no_mangle]
pub extern "C" fn allocate_buffer() -> Box<[usize; 2]> {
    let buffer: Vec<i32> = vec![0; 10];
    let memory: ManuallyDrop<Vec<i32>> = ManuallyDrop::new(buffer);
    let ptr: *const i32 = memory.as_ptr();
    let length: usize = memory.len();
    // Unlike a `Vec`, `Box` is FFI compatible and will not drop
    // its data when crossing the FFI
    // Additionally, a `Box<T>` where `T` is `Sized` will be a thin pointer
    Box::new([ptr as usize, length])
}

#[no_mangle]
pub const extern "C" fn as_pointer(ptr: usize) -> usize {
    ptr
}
{
  drop_buffer: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
}

const registry = new FinalizationRegistry((box: Pointer): void => {
  drop_buffer(box);
});
registry.register(buffer, box);

ネイティブ ライブラリは WASM をかろうじて上回り、純粋な TypeScript 実装には一貫して負けています。

bun:ffi モジュールのチュートリアル/探索はこれで終わりです。願わくば、私たち全員がもう少し知識を深めてこの問題から抜け出すことができれば幸いです。
ご意見やご質問をお気軽にコメント欄で共有してください

以上がBun FFI を使用する方法と使用する必要がありますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。