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:
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
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}Para compilar:
zig build-lib add.zig -dynamic -OReleaseFastPasa una ruta a la biblioteca compartida y un mapa de símbolos a importar en 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
}Para compilar:
rustc --crate-type cdylib add.rsC++
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}Para compilar:
zig build-lib add.cpp -dynamic -lc -lc++Tipos FFI
Los siguientes valores FFIType son soportados.
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: 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
lengthalmacenado 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
\0que 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:
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:
const myString = new CString(ptr);Para convertir de un puntero con longitud conocida a una cadena de JavaScript:
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.
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 soportadasPara 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.
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:
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.
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).
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`: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:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);Para convertir de un puntero a un ArrayBuffer:
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:
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:
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.
FFIType | Función 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 |
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:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);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:
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:
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
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);