Skip to content

Utilisez le module intégré bun:ffi pour appeler efficacement des bibliothèques natives depuis JavaScript. Cela fonctionne avec les langages qui prennent en charge l'ABI C (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).


Utilisation de dlopen (bun:ffi)

Pour afficher le numéro de version de sqlite3 :

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

// `suffix` est soit "dylib", "so", ou "dll" selon la plateforme
// vous n'êtes pas obligé d'utiliser "suffix", c'est juste pour la commodité
const path = `libsqlite3.${suffix}`;

const {
  symbols: {
    sqlite3_libversion, // la fonction à appeler
  },
} = dlopen(
  path, // un nom de bibliothèque ou un chemin de fichier
  {
    sqlite3_libversion: {
      // aucun argument, retourne une chaîne
      args: [],
      returns: FFIType.cstring,
    },
  },
);

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

Performance

Selon notre benchmark, bun:ffi est environ 2 à 6 fois plus rapide que le FFI de Node.js via Node-API.

Bun génère et compile juste-à-temps des liaisons C qui convertissent efficacement les valeurs entre les types JavaScript et les types natifs. Pour compiler du C, Bun intègre TinyCC, un petit compilateur C rapide.


Utilisation

Zig

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

Pour compiler :

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

Passez un chemin vers la bibliothèque partagée et une carte des symboles à importer dans 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
}

Pour compiler :

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

Pour compiler :

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

Types FFI

Les valeurs FFIType suivantes sont prises en charge.

FFITypeType 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

Note : Les arguments buffer doivent être un TypedArray ou DataView.


Chaînes

Les chaînes JavaScript et les chaînes de style C sont différentes, ce qui complique l'utilisation des chaînes avec les bibliothèques natives.

En quoi les chaînes JavaScript et les chaînes C sont-elles différentes ?">

Les chaînes JavaScript :

  • UTF16 (2 octets par lettre) ou potentiellement latin1, selon le moteur JavaScript et les caractères utilisés
  • length stocké séparément
  • Immuable

Les chaînes C :

  • UTF8 (1 octet par lettre), généralement
  • La longueur n'est pas stockée. Au lieu de cela, la chaîne est terminée par null, ce qui signifie que la longueur est l'index du premier \0 trouvé
  • Modifiable

Pour résoudre ce problème, bun:ffi exporte CString qui étend la classe String intégrée de JavaScript pour prendre en charge les chaînes terminées par null et ajouter quelques extras :

ts
class CString extends String {
  /**
   * Étant donné un `ptr`, cela recherchera automatiquement le caractère `\0` de fermeture et transcodera de UTF-8 vers UTF-16 si nécessaire.
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * Le ptr vers la chaîne C
   *
   * Cette instance `CString` est un clone de la chaîne, donc il
   * est sûr de continuer à utiliser cette instance après que le `ptr` a été
   * libéré.
   */
  ptr: number;
  byteOffset?: number;
  byteLength?: number;
}

Pour convertir d'un pointeur de chaîne terminée par null vers une chaîne JavaScript :

ts
const myString = new CString(ptr);

Pour convertir d'un pointeur avec une longueur connue vers une chaîne JavaScript :

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

Le constructeur new CString() clone la chaîne C, il est donc sûr de continuer à utiliser myString après que ptr a été libéré.

ts
my_library_free(myString.ptr);

// ceci est sûr car myString est un clone
console.log(myString);

Lorsqu'il est utilisé dans returns, FFIType.cstring coerce le pointeur en une string JavaScript. Lorsqu'il est utilisé dans args, FFIType.cstring est identique à ptr.


Pointeurs de fonction

NOTE

Les fonctions asynchrones ne sont pas encore prises en charge

Pour appeler un pointeur de fonction depuis JavaScript, utilisez CFunction. Ceci est utile si vous utilisez Node-API (napi) avec Bun, et que vous avez déjà chargé des symboles.

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

let myNativeLibraryGetVersion = /* d'une manière ou d'une autre, vous avez obtenu ce pointeur */

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

Si vous avez plusieurs pointeurs de fonction, vous pouvez tous les définir à la fois avec linkSymbols :

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

// getVersionPtrs défini ailleurs
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // Contrairement à dlopen(), les noms ici peuvent être ce que vous voulez
  getMajor: {
    returns: "cstring",
    args: [],

    // Puisque cela n'utilise pas dlsym(), vous devez fournir un ptr valide
    // Ce ptr pourrait être un nombre ou un bigint
    // Un pointeur invalide plantera votre programme.
    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

Utilisez JSCallback pour créer des fonctions callback JavaScript qui peuvent être passées aux fonctions C/FFI. La fonction C/FFI peut appeler le code JavaScript/TypeScript. Ceci est utile pour le code asynchrone ou chaque fois que vous voulez appeler du code JavaScript depuis 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)) {
  // trouvé une correspondance !
}

// Plus tard :
setTimeout(() => {
  searchIterator.close();
  close();
}, 5000);

Lorsque vous avez terminé avec un JSCallback, vous devez appeler close() pour libérer la mémoire.

Callbacks thread-safe expérimentaux

JSCallback prend en charge de manière expérimentale les callbacks thread-safe. Cela sera nécessaire si vous passez une fonction callback dans un thread différent de son contexte d'instanciation. Vous pouvez l'activer avec le paramètre optionnel threadsafe.

Actuellement, les callbacks thread-safe fonctionnent mieux lorsqu'ils sont exécutés depuis un autre thread qui exécute du code JavaScript, c'est-à-dire un Worker. Une future version de Bun permettra de les appeler depuis n'importe quel thread (comme les nouveaux threads spawned par votre bibliothèque native dont Bun n'a pas connaissance).

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

NOTE

**⚡️ Conseil de performance** — Pour une légère amélioration des performances, passez directement `JSCallback.prototype.ptr` au lieu de l'objet `JSCallback` :
ts
const onResolve = new JSCallback(arg => arg === 42, {
  returns: "bool",
  args: ["i32"],
});
const setOnResolve = new CFunction({
  returns: "bool",
  args: ["function"],
  ptr: myNativeLibrarySetOnResolve,
});

// Ce code s'exécute légèrement plus vite :
setOnResolve(onResolve.ptr);

// Par rapport à ceci :
setOnResolve(onResolve);

Pointeurs

Bun représente les pointeurs comme un number en JavaScript.

Comment un pointeur 64 bits tient-il dans un nombre JavaScript ?">

Les processeurs 64 bits prennent en charge jusqu'à 52 bits d'espace adressable. Les nombres JavaScript prennent en charge 53 bits d'espace utilisable, ce qui nous laisse environ 11 bits d'espace supplémentaire.

Pourquoi pas BigInt ? BigInt est plus lent. Les moteurs JavaScript allouent un BigInt séparé, ce qui signifie qu'ils ne peuvent pas tenir dans une valeur JavaScript régulière. Si vous passez un BigInt à une fonction, il sera converti en number

Pour convertir d'un TypedArray vers un pointeur :

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

Pour convertir d'un pointeur vers un ArrayBuffer :

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

// toArrayBuffer accepte un `byteOffset` et `byteLength`
// si `byteLength` n'est pas fourni, il est supposé être un pointeur terminé par null
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);

Pour lire des données depuis un pointeur, vous avez deux options. Pour les pointeurs de longue durée, utilisez 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),
);

Pour les pointeurs de courte durée, utilisez 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 fonction read se comporte de manière similaire à DataView, mais elle est généralement plus rapide car elle n'a pas besoin de créer un DataView ou ArrayBuffer.

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

Gestion de la mémoire

bun:ffi ne gère pas la mémoire pour vous. Vous devez libérer la mémoire lorsque vous avez terminé.

Depuis JavaScript

Si vous voulez suivre quand un TypedArray n'est plus utilisé depuis JavaScript, vous pouvez utiliser un FinalizationRegistry.

Depuis C, Rust, Zig, etc

Si vous voulez suivre quand un TypedArray n'est plus utilisé depuis C ou FFI, vous pouvez passer un callback et un pointeur de contexte optionnel à toArrayBuffer ou toBuffer. Cette fonction est appelée à un moment donné plus tard, une fois que le garbage collector libère l'objet JavaScript ArrayBuffer sous-jacent.

La signature attendue est la même que dans l'API C de JavaScriptCore :

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

// avec un deallocatorContext :
toArrayBuffer(
  bytes,
  byteOffset,
  byteLength,
  // ceci est un pointeur optionnel vers un callback
  deallocatorContext,
  // ceci est un pointeur vers une fonction
  jsTypedArrayBytesDeallocator,
);

// sans deallocatorContext :
toArrayBuffer(
  bytes,
  byteOffset,
  byteLength,
  // ceci est un pointeur vers une fonction
  jsTypedArrayBytesDeallocator,
);

Sécurité mémoire

L'utilisation de pointeurs bruts en dehors de FFI est extrêmement déconseillée. Une future version de Bun pourrait ajouter une option CLI pour désactiver bun:ffi.

Alignement des pointeurs

Si une API attend un pointeur de taille autre que char ou u8, assurez-vous que le TypedArray a également cette taille. Un u64* n'est pas exactement la même chose que [8]u8* en raison de l'alignement.

Passer un pointeur

Lorsque les fonctions FFI attendent un pointeur, passez un TypedArray de taille équivalente :

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

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // Les FFIType peuvent également être spécifiés sous forme de chaînes
    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 sera passé comme un pointeur
  pixels,
  128,
  128,
);

Le wrapper auto-généré convertit le pointeur en un TypedArray.

Mode difficile">

Si vous ne voulez pas la conversion automatique ou si vous voulez un pointeur vers un offset d'octet spécifique dans le TypedArray, vous pouvez également obtenir directement le pointeur vers le TypedArray :

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

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // Les FFIType peuvent également être spécifiés sous forme de chaînes
    args: ["ptr", "u32", "u32"],
    returns: FFIType.ptr,
  },
});

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

// ceci retourne un nombre ! pas un BigInt !
const myPtr = ptr(pixels);

const out = encode_png(
  myPtr,
  // dimensions :
  128,
  128,
);

Lire les pointeurs

ts
const out = encode_png(
  // pixels sera passé comme un pointeur
  pixels,
  // dimensions :
  128,
  128,
);

// en supposant qu'il soit terminé par 0, il peut être lu comme ceci :
let png = new Uint8Array(toArrayBuffer(out));

// sauvegarder sur disque :
await Bun.write("out.png", png);

Bun édité par www.bunjs.com.cn