Angenommen, Sie haben eine JavaScript-Anwendung, die in Bun ausgeführt wird, und Sie haben einen Engpass identifiziert, den Sie optimieren möchten.
Das Umschreiben in eine leistungsfähigere Sprache könnte genau die Lösung sein, die Sie brauchen.
Als moderne JS-Laufzeit unterstützt Bun Foreign Function Interface (FFI), um Bibliotheken aufzurufen, die in anderen Sprachen geschrieben sind, die die Bereitstellung von C-ABIs unterstützen, wie C, C, Rust und Zig.
In diesem Beitrag gehen wir darauf ein, wie man es verwenden kann, und kommen zu dem Schluss, ob man davon profitieren kann.
In diesem Beispiel wird Rust verwendet. Das Erstellen einer gemeinsam genutzten Bibliothek mit C-Bindungen sieht in anderen Sprachen anders aus, aber die Idee bleibt dieselbe.
Bun stellt seine FFI-API über das Modul bun:ffi bereit.
Der Einstiegspunkt ist eine Dlopen-Funktion. Es benötigt einen Pfad, der entweder absolut oder relativ zum aktuellen Arbeitsverzeichnis ist, zur Bibliotheksdatei (die Build-Ausgabe mit der Erweiterung .so für Linux, .dylib für macOS oder .dll für Windows) und ein Objekt mit die Signaturen der Funktionen, die Sie importieren möchten.
Es gibt ein Objekt mit einer Close-Methode zurück, mit der Sie die Bibliothek schließen können, wenn sie nicht mehr benötigt wird, und einer Symbol-Eigenschaft, bei der es sich um ein Objekt handelt, das die von Ihnen ausgewählten Funktionen enthält.
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();
Wie Sie vielleicht bemerkt haben, sind die unterstützten Typen, die Bun über FFI akzeptiert, auf Zahlen, einschließlich Zeiger, beschränkt.
Insbesondere size_t oder usize fehlen in der Liste der unterstützten Typen, obwohl der Code dafür ab Bun-Version 1.1.34 existiert.
Bun bietet keine Hilfe bei der Übergabe komplexerer Daten als eines C-Strings. Das bedeutet, dass Sie selbst mit Zeigern arbeiten müssen.
Sehen wir uns an, wie man einen Zeiger von JavaScript an Rust übergibt ...
{ 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) }; }
... und wie man einen Zeiger von Rust auf JavaScript zurückgibt.
{ 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 weiß nicht, dass JS den Besitz der Daten auf der anderen Seite übernimmt, daher müssen Sie ihm mithilfe von ManuallyDrop explizit mitteilen, dass es die Daten auf dem Heap nicht freigeben soll. Andere Sprachen, die den Speicher verwalten, müssen etwas Ähnliches tun.
Wie wir sehen können, ist es möglich, Speicher sowohl in JS als auch in Rust zuzuweisen, und keiner von beiden kann den Speicher anderer sicher verwalten.
Lassen Sie uns entscheiden, wo und wie Sie Ihren Speicher verteilen möchten.
Es gibt drei Methoden, die Speicherbereinigung von JS an Rust zu delegieren, und alle haben ihre Vor- und Nachteile.
Verwenden Sie FinalizationRegistry, um einen Bereinigungsrückruf während der Garbage Collection anzufordern, indem Sie das Objekt in JavaScript verfolgen.
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);
Delegieren Sie die Garbage-Collection-Verfolgung an Bun, um einen Bereinigungsrückruf aufzurufen.
Wenn 4 Parameter an toArrayBuffer übergeben werden, muss der vierte Parameter eine C-Funktion sein, die bei der Bereinigung aufgerufen werden soll.
Wenn jedoch 5 Parameter übergeben werden, ist der 5. Parameter die Funktion und der 4. Parameter muss ein Kontextzeiger sein, der übergeben wird.
/// 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);
Legen Sie den Speicher einfach selbst ab, wenn Sie ihn nicht mehr benötigen.
Glücklicherweise verfügt TypeScript dafür und für das Schlüsselwort using über eine sehr hilfreiche Einwegschnittstelle.
Es ist ein Äquivalent zu Pythons with- oder C#s using-Schlüsselwörtern.
Siehe die Dokumentation dazu
#[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);
Das ist viel einfacher und sicherer, da die Aufhebung der Zuweisung für Sie erledigt wird.
Es gibt jedoch einen erheblichen Nachteil.
Da Sie den JavaScript-Speicher in Rust nicht verwalten können, können Sie die Kapazität des Puffers nicht überschreiten, da dies zu einer Freigabe führt. Das bedeutet, dass Sie die Puffergröße kennen müssen, bevor Sie sie an Rust übergeben.
Wenn Sie nicht im Voraus wissen, wie viele Puffer Sie benötigen, entsteht auch ein hoher Overhead, da Sie nur für die Zuweisung über FFI hin und her gehen müssen.
/// # 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);
Wenn die Ausgabe, die Sie von der Bibliothek erwarten, eine Zeichenfolge ist, haben Sie möglicherweise über die Mikrooptimierung nachgedacht, einen Vektor von u16 anstelle einer Zeichenfolge zurückzugeben, da JavaScript-Engines normalerweise unter der Haube UTF-16 verwenden.
Das wäre jedoch ein Fehler, denn die Umwandlung Ihres Strings in einen C-String und die Verwendung des cstring-Typs von bun wäre etwas schneller.
Hier ist ein Benchmark, der mit einer schönen Benchmark-Bibliothek mitata erstellt wurde
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) }; }
Es ist an der Zeit, den Elefanten im Raum anzusprechen, der WebAssembly ist.
Sollten Sie schöne bestehende WASM-Bindungen dem Umgang mit C ABI vorziehen?
Die Antwort ist wahrscheinlich weder.
Die Einführung einer anderen Sprache in Ihre Codebasis erfordert mehr als nur einen einzigen Engpass, um sich DX-technisch und leistungstechnisch zu lohnen.
Hier ist ein Benchmark für eine einfache Bereichsfunktion in JS, WASM und 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);
Native Bibliothek schlägt WASM knapp und verliert durchweg gegen die reine TypeScript-Implementierung.
Und das ist alles für dieses Tutorial zur Erkundung des bun:ffi-Moduls. Hoffentlich sind wir alle etwas aufgeklärter daraus hervorgegangen.
Teilen Sie Ihre Gedanken und Fragen gerne in den Kommentaren mit
Das obige ist der detaillierte Inhalt vonWie und sollten Sie Bun FFI verwenden. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!