bun で実行される JavaScript アプリケーションがあり、最適化したいボトルネックが特定されたとします。
よりパフォーマンスの高い言語で書き直すことが、必要な解決策になる可能性があります。
Bun は最新の JS ランタイムとして、C、C、Rust、Zig などの C ABI の公開をサポートする他の言語で書かれたライブラリを呼び出すための外部関数インターフェイス (FFI) をサポートしています。
この投稿では、それをどのように使用できるかを検討し、それから利益を得られるかどうかを結論付けます。
この例では Rust を使用しています。 C バインディングを使用した共有ライブラリの作成は、他の言語では見た目が異なりますが、考え方は同じです。
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();
お気づきのとおり、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 の両方でメモリを割り当てることは可能ですが、どちらも他のメモリを安全に管理することはできません。
メモリをどこにどのように割り当てるかを選択しましょう。
メモリのクリーンアップを JS から Rust に委任するには 3 つの方法があり、すべてに長所と短所があります。
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);
ガベージ コレクションの追跡を 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);
メモリが不要になったら、自分でメモリを削除してください。
幸いなことに、TypeScript には、これと using キーワードに非常に役立つ Disposable インターフェイスがあります。
これは、キーワードを使用した Python または C# のキーワードと同等です。
ドキュメントを参照してください
#[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);
割り当て解除が自動的に処理されるため、これははるかに簡単かつ安全です。
しかし、重大な欠点があります。
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 という部屋の象に対処する時間です。
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 サイトの他の関連記事を参照してください。