Skip to content

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:

ts
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

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

Per compilare:

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

Passa un percorso alla libreria condivisa e una mappa di simboli da importare in dlopen:

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
}

Per compilare:

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

Per compilare:

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

Tipi FFI

I seguenti valori FFIType sono supportati.

FFITypeTipo CAlias
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

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
  • length memorizzato 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 \0 che 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:

ts
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:

ts
const myString = new CString(ptr);

Per convertire da un puntatore con una lunghezza nota a una stringa JavaScript:

ts
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.

ts
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 supportate

Per chiamare un puntatore a funzione da JavaScript, usa CFunction. Questo è utile se usi Node-API (napi) con Bun e hai già caricato alcuni simboli.

ts
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:

ts
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.

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)) {
  // 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).

ts
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`:
ts
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:

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

Per convertire da un puntatore a un ArrayBuffer:

ts
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:

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

Per puntatori di breve durata, usa 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),
);

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.

FFITypeFunzione read
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.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:

c
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
ts
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:

ts
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:

ts
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

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

Bun a cura di www.bunjs.com.cn