Usa il modulo built-in bun:ffi per chiamare efficientemente librerie native da JavaScript. Funziona con linguaggi che supportano la C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, ecc).
Utilizzo di dlopen (bun:ffi)
Per stampare il numero di versione di sqlite3:
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` è "dylib", "so", o "dll" a seconda della piattaforma
// non devi usare "suffix", è solo lì per comodità
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // la funzione da chiamare
},
} = dlopen(
path, // un nome di libreria o percorso file
{
sqlite3_libversion: {
// nessun argomento, restituisce una stringa
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`Versione SQLite 3: ${sqlite3_libversion()}`);Prestazioni
Secondo il nostro benchmark, bun:ffi è circa 2-6x più veloce di Node.js FFI tramite Node-API.
Bun genera e compila just-in-time binding C che convertono efficientemente i valori tra tipi JavaScript e tipi nativi. Per compilare C, Bun incorpora TinyCC, un compilatore C piccolo e veloce.
Utilizzo
Zig
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}Per compilare:
zig build-lib add.zig -dynamic -OReleaseFastPassa un percorso alla libreria condivisa e una mappa di simboli da importare in dlopen:
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
}Per compilare:
rustc --crate-type cdylib add.rsC++
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}Per compilare:
zig build-lib add.cpp -dynamic -lc -lc++Tipi FFI
I seguenti valori FFIType sono supportati.
FFIType | Tipo C | Alias |
|---|---|---|
| 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 |
Nota: gli argomenti buffer devono essere un TypedArray o DataView.
Stringhe
Le stringhe JavaScript e le stringhe in stile C sono diverse, e questo complica l'uso delle stringhe con librerie native.
In che modo le stringhe JavaScript e le stringhe C sono diverse?
Stringhe JavaScript:
- UTF16 (2 byte per lettera) o potenzialmente latin1, a seconda del motore JavaScript e dei caratteri usati
lengthmemorizzato separatamente- Immutabili
Stringhe C:
- UTF8 (1 byte per lettera), di solito
- La lunghezza non è memorizzata. Invece, la stringa è terminata con null, il che significa che la lunghezza è l'indice del primo
\0che trova - Mutabili
Per risolvere questo problema, bun:ffi esporta CString che estende la String built-in di JavaScript per supportare stringhe terminate con null e aggiungere alcune funzionalità extra:
class CString extends String {
/**
* Dato un `ptr`, questo cercherà automaticamente il carattere di chiusura `\0` e tras coderà da UTF-8 a UTF-16 se necessario.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
/**
* Il ptr alla stringa C
*
* Questa istanza `CString` è un clone della stringa, quindi è
* sicuro continuare a usare questa istanza dopo che il `ptr` è stato
* liberato.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}Per convertire da un puntatore a stringa terminata con null a una stringa JavaScript:
const myString = new CString(ptr);Per convertire da un puntatore con una lunghezza nota a una stringa JavaScript:
const myString = new CString(ptr, 0, byteLength);Il costruttore new CString() clona la stringa C, quindi è sicuro continuare a usare myString dopo che ptr è stato liberato.
my_library_free(myString.ptr);
// questo è sicuro perché myString è un clone
console.log(myString);Quando usato in returns, FFIType.cstring converte il puntatore in una string JavaScript. Quando usato in args, FFIType.cstring è identico a ptr.
Puntatori a funzione
NOTE
Le funzioni async non sono ancora supportatePer chiamare un puntatore a funzione da JavaScript, usa CFunction. Questo è utile se usi Node-API (napi) con Bun e hai già caricato alcuni simboli.
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* in qualche modo, hai ottenuto questo puntatore */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();Se hai più puntatori a funzione, puoi definirli tutti in una volta con linkSymbols:
import { linkSymbols } from "bun:ffi";
// getVersionPtrs definito altrove
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
// A differenza di dlopen(), i nomi qui possono essere quelli che vuoi
getMajor: {
returns: "cstring",
args: [],
// Poiché questo non usa dlsym(), devi fornire un ptr valido
// Quel ptr potrebbe essere un numero o un bigint
// Un puntatore non valido farà crashare il tuo programma.
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()];Callback
Usa JSCallback per creare funzioni callback JavaScript che possono essere passate a funzioni C/FFI. La funzione C/FFI può chiamare il codice JavaScript/TypeScript. Questo è utile per codice asincrono o quando vuoi chiamare codice JavaScript da C.
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)) {
// trovato un match!
}
// Più tardi:
setTimeout(() => {
searchIterator.close();
close();
}, 5000);Quando hai finito con un JSCallback, dovresti chiamare close() per liberare la memoria.
Callback thread-safe sperimentali
JSCallback ha supporto sperimentale per callback thread-safe. Questo sarà necessario se passi una funzione callback in un thread diverso dal suo contesto di istanziazione. Puoi abilitarlo con il parametro opzionale threadsafe.
Attualmente, le callback thread-safe funzionano meglio quando eseguite da un altro thread che esegue codice JavaScript, cioè un Worker. Una versione futura di Bun consentirà loro di essere chiamate da qualsiasi thread (come nuovi thread generati dalla tua libreria nativa di cui Bun non è a conoscenza).
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Opzionale. Default a `false`
});NOTE
**⚡️ Suggerimento sulle prestazioni** — Per un leggero miglioramento delle prestazioni, passa direttamente `JSCallback.prototype.ptr` invece dell'oggetto `JSCallback`:const onResolve = new JSCallback(arg => arg === 42, {
returns: "bool",
args: ["i32"],
});
const setOnResolve = new CFunction({
returns: "bool",
args: ["function"],
ptr: myNativeLibrarySetOnResolve,
});
// Questo codice esegue leggermente più velocemente:
setOnResolve(onResolve.ptr);
// Rispetto a questo:
setOnResolve(onResolve);Puntatori
Bun rappresenta i puntatori come un number in JavaScript.
Come fa un puntatore a 64 bit a stare in un numero JavaScript?
I processori a 64 bit supportano fino a 52 bit di spazio indirizzabile. I numeri JavaScript supportano 53 bit di spazio utilizzabile, quindi ci rimangono circa 11 bit di spazio extra.
Perché non BigInt? BigInt è più lento. I motori JavaScript allocano un BigInt separato, il che significa che non possono stare in un valore JavaScript regolare. Se passi un BigInt a una funzione, verrà convertito in un number.
Per convertire da un TypedArray a un puntatore:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);Per convertire da un puntatore a un ArrayBuffer:
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
// toArrayBuffer accetta un `byteOffset` e `byteLength`
// se `byteLength` non è fornito, si presume che sia un puntatore terminato con null
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);Per leggere i dati da un puntatore, hai due opzioni. Per puntatori di lunga durata, usa un 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),
);Per puntatori di breve durata, usa 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),
);La funzione read si comporta in modo simile a DataView, ma è di solito più veloce perché non ha bisogno di creare un DataView o ArrayBuffer.
FFIType | Funzione read |
|---|---|
| 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 |
Gestione della memoria
bun:ffi non gestisce la memoria per te. Devi liberare la memoria quando hai finito.
Da JavaScript
Se vuoi tracciare quando un TypedArray non è più in uso da JavaScript, puoi usare un FinalizationRegistry.
Da C, Rust, Zig, ecc
Se vuoi tracciare quando un TypedArray non è più in uso da C o FFI, puoi passare una callback e un puntatore di contesto opzionale a toArrayBuffer o toBuffer. Questa funzione viene chiamata in un momento successivo, una volta che il garbage collector libera l'oggetto JavaScript ArrayBuffer sottostante.
La firma prevista è la stessa dell'API C di JavaScriptCore:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);import { toArrayBuffer } from "bun:ffi";
// con un deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// questo è un puntatore opzionale a una callback
deallocatorContext,
// questo è un puntatore a una funzione
jsTypedArrayBytesDeallocator,
);
// senza un deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// questo è un puntatore a una funzione
jsTypedArrayBytesDeallocator,
);Sicurezza della memoria
L'uso di puntatori grezzi al di fuori di FFI è estremamente sconsigliato. Una versione futura di Bun potrebbe aggiungere un flag CLI per disabilitare bun:ffi.
Allineamento dei puntatori
Se un'API si aspetta un puntatore di dimensione diversa da char o u8, assicurati che il TypedArray abbia anche quella dimensione. Un u64* non è esattamente lo stesso di [8]u8* a causa dell'allineamento.
Passare un puntatore
Dove le funzioni FFI si aspettano un puntatore, passa un TypedArray di dimensione equivalente:
import { dlopen, FFIType } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// Anche i FFIType possono essere specificati come stringhe
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 verrà passato come puntatore
pixels,
128,
128,
);Il wrapper auto-generato converte il puntatore in un TypedArray.
Modalità difficile
Se non vuoi la conversione automatica o vuoi un puntatore a un offset di byte specifico all'interno del TypedArray, puoi anche ottenere direttamente il puntatore al TypedArray:
import { dlopen, FFIType, ptr } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// Anche i FFIType possono essere specificati come stringhe
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
// questo restituisce un numero! non un BigInt!
const myPtr = ptr(pixels);
const out = encode_png(
myPtr,
// dimensioni:
128,
128,
);Leggere i puntatori
const out = encode_png(
// pixels verrà passato come puntatore
pixels,
// dimensioni:
128,
128,
);
// supponendo che sia terminato con 0, può essere letto così:
let png = new Uint8Array(toArrayBuffer(out));
// salvalo su disco:
await Bun.write("out.png", png);