Skip to content

Usa el módulo incorporado bun:ffi para llamar eficientemente a bibliotecas nativas desde JavaScript. Funciona con lenguajes que soportan el ABI de C (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).


Uso de dlopen (bun:ffi)

Para imprimir el número de versión de sqlite3:

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

// `suffix` es "dylib", "so", o "dll" dependiendo de la plataforma
// no tienes que usar "suffix", está ahí por conveniencia
const path = `libsqlite3.${suffix}`;

const {
  symbols: {
    sqlite3_libversion, // la función a llamar
  },
} = dlopen(
  path, // un nombre de biblioteca o ruta de archivo
  {
    sqlite3_libversion: {
      // sin argumentos, devuelve una cadena
      args: [],
      returns: FFIType.cstring,
    },
  },
);

console.log(`Versión de SQLite 3: ${sqlite3_libversion()}`);

Rendimiento

Según nuestro benchmark, bun:ffi es aproximadamente 2-6 veces más rápido que el FFI de Node.js mediante Node-API.

Bun genera y compila justo a tiempo enlaces C que convierten eficientemente valores entre tipos de JavaScript y tipos nativos. Para compilar C, Bun incorpora TinyCC, un compilador de C pequeño y rápido.


Uso

Zig

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

Para compilar:

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

Pasa una ruta a la biblioteca compartida y un mapa de símbolos a importar en 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
}

Para compilar:

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

Para compilar:

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

Tipos FFI

Los siguientes valores FFIType son soportados.

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: Los argumentos buffer deben ser un TypedArray o DataView.


Cadenas

Las cadenas de JavaScript y las cadenas tipo C son diferentes, y eso complica el uso de cadenas con bibliotecas nativas.

¿En qué se diferencian las cadenas de JavaScript y las cadenas C?

Cadenas de JavaScript:

  • UTF16 (2 bytes por letra) o potencialmente latin1, dependiendo del motor de JavaScript y qué caracteres se usan
  • length almacenado por separado
  • Inmutables

Cadenas C:

  • UTF8 (1 byte por letra), usualmente
  • La longitud no se almacena. En cambio, la cadena termina en null lo que significa que la longitud es el índice del primer \0 que encuentra
  • Mutables

Para resolver esto, bun:ffi exporta CString que extiende el String incorporado de JavaScript para soportar cadenas terminadas en null y agregar algunas características adicionales:

ts
class CString extends String {
  /**
   * Dado un `ptr`, esto buscará automáticamente el carácter `\0` de cierre y transcodificará de UTF-8 a UTF-16 si es necesario.
   */
  constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

  /**
   * El ptr a la cadena C
   *
   * Esta instancia `CString` es un clon de la cadena, por lo que
   * es seguro continuar usando esta instancia después de que el `ptr` haya sido
   * liberado.
   */
  ptr: number;
  byteOffset?: number;
  byteLength?: number;
}

Para convertir de un puntero de cadena terminada en null a una cadena de JavaScript:

ts
const myString = new CString(ptr);

Para convertir de un puntero con longitud conocida a una cadena de JavaScript:

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

El constructor new CString() clona la cadena C, por lo que es seguro continuar usando myString después de que ptr haya sido liberado.

ts
my_library_free(myString.ptr);

// esto es seguro porque myString es un clon
console.log(myString);

Cuando se usa en returns, FFIType.cstring convierte el puntero a una string de JavaScript. Cuando se usa en args, FFIType.cstring es idéntico a ptr.


Punteros a funciones

NOTE

Las funciones asíncronas aún no son soportadas

Para llamar a un puntero a función desde JavaScript, usa CFunction. Esto es útil si usas Node-API (napi) con Bun, y ya has cargado algunos símbolos.

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

let myNativeLibraryGetVersion = /* de alguna manera, obtuviste este puntero */

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

Si tienes múltiples punteros a funciones, puedes definirlos todos a la vez con linkSymbols:

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

// getVersionPtrs definido en otro lugar
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();

const lib = linkSymbols({
  // A diferencia de dlopen(), los nombres aquí pueden ser lo que quieras
  getMajor: {
    returns: "cstring",
    args: [],

    // Como esto no usa dlsym(), tienes que proporcionar un ptr válido
    // Ese ptr podría ser un número o un bigint
    // Un puntero inválido hará fallar tu programa.
    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

Usa JSCallback para crear funciones callback de JavaScript que pueden pasarse a funciones C/FFI. La función C/FFI puede llamar al código JavaScript/TypeScript. Esto es útil para código asíncrono o cuando quieras llamar a código JavaScript desde 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)) {
  // ¡encontró una coincidencia!
}

// Más tarde:
setTimeout(() => {
  searchIterator.close();
  close();
}, 5000);

Cuando termines con un JSCallback, debes llamar a close() para liberar la memoria.

Callbacks thread-safe experimentales

JSCallback tiene soporte experimental para callbacks thread-safe. Esto será necesario si pasas una función callback a un hilo diferente desde su contexto de instanciación. Puedes habilitarlo con el parámetro opcional threadsafe.

Actualmente, los callbacks thread-safe funcionan mejor cuando se ejecutan desde otro hilo que está ejecutando código JavaScript, es decir, un Worker. Una versión futura de Bun permitirá que se llamen desde cualquier hilo (como nuevos hilos creados por tu biblioteca nativa de los que Bun no tiene conocimiento).

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

NOTE

**⚡️ Consejo de rendimiento** — Para una ligera mejora de rendimiento, pasa directamente `JSCallback.prototype.ptr` en lugar del objeto `JSCallback`:
ts
const onResolve = new JSCallback(arg => arg === 42, {
  returns: "bool",
  args: ["i32"],
});
const setOnResolve = new CFunction({
  returns: "bool",
  args: ["function"],
  ptr: myNativeLibrarySetOnResolve,
});

// Este código se ejecuta ligeramente más rápido:
setOnResolve(onResolve.ptr);

// Comparado con esto:
setOnResolve(onResolve);

Punteros

Bun representa los punteros como un number en JavaScript.

¿Cómo cabe un puntero de 64 bits en un número de JavaScript?

Los procesadores de 64 bits soportan hasta 52 bits de espacio direccionable. Los números de JavaScript soportan 53 bits de espacio utilizable, así que eso nos deja con aproximadamente 11 bits de espacio extra.

¿Por qué no BigInt? BigInt es más lento. Los motores de JavaScript asignan un BigInt separado lo que significa que no pueden caber en un valor regular de JavaScript. Si pasas un BigInt a una función, se convertirá a un number.

Para convertir de un TypedArray a un puntero:

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

Para convertir de un puntero a un ArrayBuffer:

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

// toArrayBuffer acepta un `byteOffset` y `byteLength`
// si `byteLength` no se proporciona, se asume que es un puntero terminado en null
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);

Para leer datos de un puntero, tienes dos opciones. Para punteros de larga duración, 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),
);

Para punteros de corta duración, 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 función read se comporta de manera similar a DataView, pero usualmente es más rápida porque no necesita crear un DataView o ArrayBuffer.

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

Gestión de memoria

bun:ffi no gestiona la memoria por ti. Debes liberar la memoria cuando termines con ella.

Desde JavaScript

Si quieres rastrear cuándo un TypedArray ya no está en uso desde JavaScript, puedes usar un FinalizationRegistry.

Desde C, Rust, Zig, etc

Si quieres rastrear cuándo un TypedArray ya no está en uso desde C o FFI, puedes pasar un callback y un puntero de contexto opcional a toArrayBuffer o toBuffer. Esta función se llama en algún momento posterior, una vez que el garbage collector libera el objeto JavaScript ArrayBuffer subyacente.

La firma esperada es la misma que en la API C de JavaScriptCore:

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

// con un deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // este es un puntero opcional a un callback
  deallocatorContext,

  // este es un puntero a una función
  jsTypedArrayBytesDeallocator,
);

// sin un deallocatorContext:
toArrayBuffer(
  bytes,
  byteOffset,

  byteLength,

  // este es un puntero a una función
  jsTypedArrayBytesDeallocator,
);

Seguridad de memoria

Usar punteros crudos fuera de FFI es extremadamente no recomendado. Una versión futura de Bun puede agregar una bandera CLI para deshabilitar bun:ffi.

Alineación de punteros

Si una API espera un puntero de tamaño diferente a char o u8, asegúrate de que el TypedArray también tenga ese tamaño. Un u64* no es exactamente lo mismo que [8]u8* debido a la alineación.

Pasar un puntero

Cuando las funciones FFI esperan un puntero, pasa un TypedArray de tamaño equivalente:

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

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // Los FFIType también se pueden especificar como cadenas
    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 se pasará como un puntero
  pixels,

  128,
  128,
);

El envoltorio auto-generado convierte el puntero a un TypedArray.

Modo difícil

Si no quieres la conversión automática o quieres un puntero a un offset de byte específico dentro del TypedArray, también puedes obtener directamente el puntero al TypedArray:

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

const {
  symbols: { encode_png },
} = dlopen(myLibraryPath, {
  encode_png: {
    // Los FFIType también se pueden especificar como cadenas
    args: ["ptr", "u32", "u32"],
    returns: FFIType.ptr,
  },
});

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

// ¡esto devuelve un número, no un BigInt!
const myPtr = ptr(pixels);

const out = encode_png(
  myPtr,

  // dimensiones:
  128,
  128,
);

Leer punteros

ts
const out = encode_png(
  // pixels se pasará como un puntero
  pixels,

  // dimensiones:
  128,
  128,
);

// asumiendo que está terminado en 0, se puede leer así:
let png = new Uint8Array(toArrayBuffer(out));

// guárdalo en disco:
await Bun.write("out.png", png);

Bun por www.bunjs.com.cn editar