Skip to content

Verwenden Sie das eingebaute bun:ffi-Modul, um native Bibliotheken effizient aus JavaScript aufzurufen. Es funktioniert mit Sprachen, die die C ABI unterstützen (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).


dlopen-Verwendung (bun:ffi)

Um die Versionsnummer von sqlite3 auszugeben:

ts
import { dlopen, FFIType, suffix } from "bun:ffi";

// `suffix` ist entweder "dylib", "so" oder "dll" je nach Plattform
// Sie müssen "suffix" nicht verwenden, es ist nur der Einfachheit halber da
const path = `libsqlite3.${suffix}`;

const {
  symbols: {
    sqlite3_libversion, // die aufzurufende Funktion
  },
} = dlopen(
  path, // ein Bibliotheksname oder Dateipfad
  {
    sqlite3_libversion: {
      // keine Argumente, gibt einen String zurück
      args: [],
      returns: FFIType.cstring,
    },
  },
);

console.log(`SQLite 3 Version: ${sqlite3_libversion()}`);

Leistung

Laut unserem Benchmark ist bun:ffi etwa 2-6x schneller als Node.js FFI über Node-API.

Bun generiert und just-in-time-kompiliert C-Bindings, die Werte zwischen JavaScript-Typen und nativen Typen effizient konvertieren. Um C zu kompilieren, bettet Bun TinyCC ein, einen kleinen und schnellen C-Compiler.


Verwendung

Zig

zig
pub export fn add(a: i32, b: i32) i32 {
  return a + b;
}

Zum Kompilieren:

bash
zig build-lib add.zig -dynamic -OReleaseFast

Übergeben Sie einen Pfad zur gemeinsamen Bibliothek und eine Map von Symbolen, die in dlopen importiert werden sollen:

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
}

Zum Kompilieren:

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

Zum Kompilieren:

bash
zig build-lib add.cpp -dynamic -lc -lc++

FFI-Typen

Die folgenden FFIType-Werte werden unterstützt.

FFITypeC-TypAliases
bufferchar*
cstringchar*
function(void*)(*)()fn, callback
ptrvoid*pointer, void*, char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t, int
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

Hinweis: buffer-Argumente müssen ein TypedArray oder DataView sein.


Strings

JavaScript-Strings und C-ähnliche Strings sind unterschiedlich, was die Verwendung von Strings mit nativen Bibliotheken erschwert.

Wie unterscheiden sich JavaScript-Strings und C-Strings?

JavaScript-Strings:

  • UTF16 (2 Bytes pro Buchstabe) oder möglicherweise Latin1, abhängig von der JavaScript-Engine und den verwendeten Zeichen
  • length separat gespeichert
  • Unveränderlich

C-Strings:

  • UTF8 (1 Byte pro Buchstabe), normalerweise
  • Die Länge wird nicht gespeichert. Stattdessen ist der String null-terminiert, was bedeutet, dass die Länge der Index des ersten \0 ist, das gefunden wird
  • Veränderlich

Um dies zu lösen, exportiert bun:ffi CString, das JavaScripts eingebaute String erweitert, um null-terminierte Strings zu unterstützen und einige Extras hinzuzufügen:

ts
class CString extends String {
  /**
   * Bei gegebenem `ptr` sucht dies automatisch nach dem schließenden `\0`-Zeichen und transkodiert bei Bedarf von UTF-8 nach UTF-16.
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * Der Ptr zum C-String
   *
   * Diese `CString`-Instanz ist ein Klon des Strings, daher ist es
   * sicher, diese Instanz weiter zu verwenden, nachdem der `ptr`
   * freigegeben wurde.
   */
  ptr: number;
  byteOffset?: number;
  byteLength?: number;
}

Um von einem null-terminierten String-Zeiger in einen JavaScript-String zu konvertieren:

ts
const myString = new CString(ptr);

Um von einem Zeiger mit bekannter Länge in einen JavaScript-String zu konvertieren:

ts
const myString = new CString(ptr, 0, byteLength);

Der new CString()-Konstruktor klont den C-String, daher ist es sicher, myString weiter zu verwenden, nachdem ptr freigegeben wurde.

ts
my_library_free(myString.ptr);

// dies ist sicher, weil myString ein Klon ist
console.log(myString);

Wenn es in returns verwendet wird, zwingt FFIType.cstring den Zeiger in einen JavaScript-string. Wenn es in args verwendet wird, ist FFIType.cstring identisch mit ptr.


Funktionszeiger

NOTE

Asynchrone Funktionen werden noch nicht unterstützt

Um einen Funktionszeiger aus JavaScript aufzurufen, verwenden Sie CFunction. Dies ist nützlich, wenn Sie Node-API (napi) mit Bun verwenden und bereits einige Symbole geladen haben.

ts
import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* irgendwie haben Sie diesen Zeiger bekommen */

const getVersion = new CFunction({
  returns: "cstring",
  args: [],
  ptr: myNativeLibraryGetVersion,
});
getVersion();

Wenn Sie mehrere Funktionszeiger haben, können Sie sie alle auf einmal mit linkSymbols definieren:

ts
import { linkSymbols } from "bun:ffi";

// getVersionPtrs woanders definiert
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // Im Gegensatz zu dlopen() können die Namen hier beliebig sein
  getMajor: {
    returns: "cstring",
    args: [],

    // Da dies kein dlsym() verwendet, müssen Sie einen gültigen Ptr bereitstellen
    // Dieser Ptr könnte eine Zahl oder eine BigInt sein
    // Ein ungültiger Zeiger lässt Ihr Programm abstürzen.
    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()];

Callbacks

Verwenden Sie JSCallback, um JavaScript-Callback-Funktionen zu erstellen, die an C/FFI-Funktionen übergeben werden können. Die C/FFI-Funktion kann in den JavaScript/TypeScript-Code aufrufen. Dies ist nützlich für asynchronen Code oder wann immer Sie von C aus in JavaScript-Code aufrufen möchten.

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)) {
  // eine Übereinstimmung gefunden!
}

// Irgendwann später:
setTimeout(() => {
  searchIterator.close();
  close();
}, 5000);

Wenn Sie mit einem JSCallback fertig sind, sollten Sie close() aufrufen, um den Speicher freizugeben.

Experimentelle threadsichere Callbacks

JSCallback hat experimentelle Unterstützung für threadsichere Callbacks. Dies ist erforderlich, wenn Sie eine Callback-Funktion in einen anderen Thread als ihren Instantiierungskontext übergeben. Sie können es mit dem optionalen threadsafe-Parameter aktivieren.

Derzeit funktionieren threadsichere Callbacks am besten, wenn sie von einem anderen Thread ausgeführt werden, der JavaScript-Code ausführt, d.h. einem Worker. Eine zukünftige Version von Bun wird es ermöglichen, sie von jedem Thread aufzurufen (wie z.B. neue Threads, die von Ihrer nativen Bibliothek erzeugt werden, von denen Bun nichts weiß).

ts
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
  returns: "bool",
  args: ["ptr", "usize"],
  threadsafe: true, // Optional. Standardwert ist `false`
});

NOTE

**⚡️ Leistungstipp** — Für eine leichte Leistungssteigerung übergeben Sie direkt `JSCallback.prototype.ptr` anstelle des `JSCallback`-Objekts:
ts
const onResolve = new JSCallback(arg => arg === 42, {
  returns: "bool",
  args: ["i32"],
});
const setOnResolve = new CFunction({
  returns: "bool",
  args: ["function"],
  ptr: myNativeLibrarySetOnResolve,
});

// Dieser Code läuft etwas schneller:
setOnResolve(onResolve.ptr);

// Im Vergleich zu diesem:
setOnResolve(onResolve);

Zeiger

Bun stellt Zeiger als number in JavaScript dar.

Wie passt ein 64-Bit-Zeiger in eine JavaScript-Nummer?

64-Bit-Prozessoren unterstützen bis zu 52 Bits adressierbaren Speicherplatz. JavaScript-Nummern unterstützen 53 Bits nutzbaren Speicherplatz, was uns etwa 11 Bits zusätzlichen Speicherplatz lässt.

Warum nicht BigInt? BigInt ist langsamer. JavaScript-Engines allozieren ein separates BigInt, was bedeutet, dass sie nicht in einen regulären JavaScript-Wert passen. Wenn Sie eine BigInt an eine Funktion übergeben, wird sie in eine number konvertiert.

Um von einem TypedArray in einen Zeiger zu konvertieren:

ts
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

Um von einem Zeiger in ein ArrayBuffer zu konvertieren:

ts
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);

// toArrayBuffer akzeptiert ein `byteOffset` und `byteLength`
// wenn `byteLength` nicht angegeben wird, wird angenommen, dass es ein null-terminierter Zeiger ist
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);

Um Daten von einem Zeiger zu lesen, haben Sie zwei Optionen. Für langlebige Zeiger verwenden Sie ein 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),
);

Für kurzlebige Zeiger verwenden Sie 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),
);

Die read-Funktion verhält sich ähnlich wie DataView, ist aber normalerweise schneller, da sie kein DataView oder ArrayBuffer erstellen muss.

FFITyperead-Funktion
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.f64

Speicherverwaltung

bun:ffi verwaltet den Speicher nicht für Sie. Sie müssen den Speicher freigeben, wenn Sie fertig sind.

Von JavaScript aus

Wenn Sie verfolgen möchten, wann ein TypedArray von JavaScript aus nicht mehr verwendet wird, können Sie eine FinalizationRegistry verwenden.

Von C, Rust, Zig, etc. aus

Wenn Sie verfolgen möchten, wann ein TypedArray von C oder FFI aus nicht mehr verwendet wird, können Sie einen Callback und einen optionalen Kontextzeiger an toArrayBuffer oder toBuffer übergeben. Diese Funktion wird zu einem späteren Zeitpunkt aufgerufen, sobald der Garbage Collector das zugrunde liegende ArrayBuffer-JavaScript-Objekt freigibt.

Die erwartete Signatur ist dieselbe wie in JavaScriptCores C-API:

c
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
ts
import { toArrayBuffer } from "bun:ffi";

// mit einem deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // dies ist ein optionaler Zeiger auf einen Callback
  deallocatorContext,

  // dies ist ein Zeiger auf eine Funktion
  jsTypedArrayBytesDeallocator,
);

// ohne einen deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // dies ist ein Zeiger auf eine Funktion
  jsTypedArrayBytesDeallocator,
);

Speichersicherheit

Die Verwendung roher Zeiger außerhalb von FFI wird dringend nicht empfohlen. Eine zukünftige Version von Bun könnte ein CLI-Flag hinzufügen, um bun:ffi zu deaktivieren.

Zeigerausrichtung

Wenn eine API einen Zeiger erwartet, der eine andere Größe als char oder u8 hat, stellen Sie sicher, dass das TypedArray ebenfalls diese Größe hat. Ein u64* ist aufgrund der Ausrichtung nicht genau dasselbe wie [8]u8*.

Einen Zeiger übergeben

Wo FFI-Funktionen einen Zeiger erwarten, übergeben Sie ein TypedArray äquivalenter Größe:

ts
import { dlopen, FFIType } from "bun:ffi";

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // FFIType's können auch als Strings angegeben werden
    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 wird als Zeiger übergeben
  pixels,

  128,
  128,
);

Der automatisch generierte Wrapper konvertiert den Zeiger in ein TypedArray.

Hardmode

Wenn Sie nicht die automatische Konvertierung möchten oder einen Zeiger auf ein bestimmtes Byte-Offset innerhalb des TypedArray möchten, können Sie auch direkt den Zeiger auf das TypedArray erhalten:

ts
import { dlopen, FFIType, ptr } from "bun:ffi";

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // FFIType's können auch als Strings angegeben werden
    args: ["ptr", "u32", "u32"],
    returns: FFIType.ptr,
  },
});

const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);

// dies gibt eine Zahl zurück! Keine BigInt!
const myPtr = ptr(pixels);

const out = encode_png(
  myPtr,

  // Dimensionen:
  128,
  128,
);

Zeiger lesen

ts
const out = encode_png(
  // pixels wird als Zeiger übergeben
  pixels,

  // Dimensionen:
  128,
  128,
);

// angenommen, es ist 0-terminiert, kann es so gelesen werden:
let png = new Uint8Array(toArrayBuffer(out));

// auf die Festplatte speichern:
await Bun.write("out.png", png);

Bun von www.bunjs.com.cn bearbeitet