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:
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
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}Zum Kompilieren:
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:
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
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}Zum Kompilieren:
rustc --crate-type cdylib add.rsC++
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}Zum Kompilieren:
zig build-lib add.cpp -dynamic -lc -lc++FFI-Typen
Die folgenden FFIType-Werte werden unterstützt.
FFIType | C-Typ | Aliases |
|---|---|---|
| buffer | char* | |
| cstring | char* | |
| function | (void*)(*)() | fn, callback |
| ptr | void* | pointer, void*, char* |
| i8 | int8_t | int8_t |
| i16 | int16_t | int16_t |
| i32 | int32_t | int32_t, int |
| i64 | int64_t | int64_t |
| i64_fast | int64_t | |
| u8 | uint8_t | uint8_t |
| u16 | uint16_t | uint16_t |
| u32 | uint32_t | uint32_t |
| u64 | uint64_t | uint64_t |
| u64_fast | uint64_t | |
| f32 | float | float |
| f64 | double | double |
| bool | bool | |
| char | char | |
| napi_env | napi_env | |
| napi_value | napi_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
lengthseparat 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
\0ist, 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:
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:
const myString = new CString(ptr);Um von einem Zeiger mit bekannter Länge in einen JavaScript-String zu konvertieren:
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.
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ütztUm 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.
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:
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.
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ß).
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: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:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);Um von einem Zeiger in ein ArrayBuffer zu konvertieren:
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:
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:
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.
FFIType | read-Funktion |
|---|---|
| ptr | read.ptr |
| i8 | read.i8 |
| i16 | read.i16 |
| i32 | read.i32 |
| i64 | read.i64 |
| u8 | read.u8 |
| u16 | read.u16 |
| u32 | read.u32 |
| u64 | read.u64 |
| f32 | read.f32 |
| f64 | read.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:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);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:
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:
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
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);