Heim >Web-Frontend >js-Tutorial >Wie und sollten Sie Bun FFI verwenden

Wie und sollten Sie Bun FFI verwenden

Linda Hamilton
Linda HamiltonOriginal
2024-11-11 10:53:02846Durchsuche

How to and Should you use Bun FFI

Was wollen wir erreichen?

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.

So verknüpfen Sie die Bibliothek mit JavaScript

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.

Von JS-Seite

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();

Weitergabe von Daten über die FFI-Grenze

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.

Speicherverwaltung

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.

In Rust zuordnen

Es gibt drei Methoden, die Speicherbereinigung von JS an Rust zu delegieren, und alle haben ihre Vor- und Nachteile.

Verwenden Sie FinalizationRegistry

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);
Vorteile
  • Es ist ganz einfach
Nachteile
  • Die Garbage Collection ist motorspezifisch und nicht deterministisch
  • Es ist nicht garantiert, dass der Rückruf zur Bereinigung überhaupt aufgerufen wird

Verwenden Sie den finalizationCallback-Parameter von toArrayBuffer

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);
Vorteile
  • Logik aus JavaScript delegieren
Nachteile
  • Viel Boilerplate und die Möglichkeit, dass Speicher verloren geht
  • Fehlende Typanmerkung für toArrayBuffer
  • Die Garbage Collection ist motorspezifisch und nicht deterministisch
  • Es ist nicht garantiert, dass der Rückruf zur Bereinigung überhaupt aufgerufen wird

Speicher manuell verwalten

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

  • TypeScript 5.2-Änderungsprotokoll
  • Pull-Anfrage zur Verwendung
#[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);
Vorteile
  • Die Bereinigung wird garantiert ausgeführt
  • Sie haben die Kontrolle darüber, wann Sie die Erinnerung löschen möchten
Nachteile
  • Boilerplate-Objekt für die Einwegschnittstelle
  • Das manuelle Löschen von Speicher ist langsamer als die Verwendung des Garbage Collectors
  • Wenn Sie den Besitz des Puffers abgeben möchten, müssen Sie eine Kopie erstellen und das Original löschen

In JS zuordnen

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);

Eine Randbemerkung zu Streichern

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) };
}

Was ist mit WebAssembly?

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.

Lohnt es sich tatsächlich?

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn