Use o módulo builtin bun:ffi para chamar eficientemente bibliotecas nativas de JavaScript. Funciona com linguagens que suportam a ABI C (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).
Uso do dlopen (bun:ffi)
Para imprimir o número de versão do sqlite3:
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` é "dylib", "so" ou "dll" dependendo da plataforma
// você não precisa usar "suffix", está lá apenas por conveniência
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // a função para chamar
},
} = dlopen(
path, // um nome de biblioteca ou caminho de arquivo
{
sqlite3_libversion: {
// sem argumentos, retorna uma string
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`SQLite 3 version: ${sqlite3_libversion()}`);Desempenho
De acordo com nosso benchmark, bun:ffi é aproximadamente 2-6x mais rápido que o FFI do Node.js via Node-API.
O Bun gera e compila just-in-time bindings C que convertem eficientemente valores entre tipos JavaScript e tipos nativos. Para compilar C, o Bun incorpora TinyCC, um compilador C pequeno e rápido.
Uso
Zig
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}Para compilar:
zig build-lib add.zig -dynamic -OReleaseFastPasse um caminho para a biblioteca compartilhada e um mapa de símbolos para importar no 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
Os seguintes valores FFIType são suportados.
FFIType | Tipo C | Aliases |
|---|---|---|
| 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: argumentos buffer devem ser um TypedArray ou DataView.
Strings
Strings JavaScript e strings estilo C são diferentes, e isso complica o uso de strings com bibliotecas nativas.
Como strings JavaScript e strings C são diferentes?
Strings JavaScript:
- UTF16 (2 bytes por letra) ou potencialmente latin1, dependendo do engine JavaScript e quais caracteres são usados
lengtharmazenado separadamente- Imutável
Strings C:
- UTF8 (1 byte por letra), normalmente
- O comprimento não é armazenado. Em vez disso, a string é null-terminada, o que significa que o comprimento é o índice do primeiro
\0encontrado - Mutável
Para resolver isso, bun:ffi exporta CString que estende a String builtin do JavaScript para suportar strings null-terminadas e adicionar alguns extras:
class CString extends String {
/**
* Dado um `ptr`, isso automaticamente buscará o caractere `\0` de fechamento e transcodificará de UTF-8 para UTF-16 se necessário.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
/**
* O ptr para a string C
*
* Esta instância `CString` é um clone da string, então é
* seguro continuar usando esta instância após o `ptr` ter sido
* liberado.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}Para converter de um ponteiro de string null-terminada para uma string JavaScript:
const myString = new CString(ptr);Para converter de um ponteiro com comprimento conhecido para uma string JavaScript:
const myString = new CString(ptr, 0, byteLength);O construtor new CString() clona a string C, então é seguro continuar usando myString após ptr ter sido liberado.
my_library_free(myString.ptr);
// isso é seguro porque myString é um clone
console.log(myString);Quando usado em returns, FFIType.cstring converte o ponteiro para uma string JavaScript. Quando usado em args, FFIType.cstring é idêntico a ptr.
Ponteiros de função
NOTE
Funções assíncronas ainda não são suportadasPara chamar um ponteiro de função de JavaScript, use CFunction. Isso é útil se estiver usando Node-API (napi) com Bun e você já carregou alguns símbolos.
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* de alguma forma, você obteve este ponteiro */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();Se você tiver múltiplos ponteiros de função, pode defini-los todos de uma vez com linkSymbols:
import { linkSymbols } from "bun:ffi";
// getVersionPtrs definido em outro lugar
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
// Diferente de dlopen(), os nomes aqui podem ser o que você quiser
getMajor: {
returns: "cstring",
args: [],
// Como isso não usa dlsym(), você precisa fornecer um ptr válido
// Esse ptr pode ser um número ou um bigint
// Um ponteiro inválido travará seu 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
Use JSCallback para criar funções callback JavaScript que podem ser passadas para funções C/FFI. A função C/FFI pode chamar o código JavaScript/TypeScript. Isso é útil para código assíncrono ou quando você quer chamar código JavaScript de 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)) {
// encontrou uma correspondência!
}
// Mais tarde:
setTimeout(() => {
searchIterator.close();
close();
}, 5000);Quando terminar com um JSCallback, você deve chamar close() para liberar a memória.
Callbacks thread-safe experimentais
JSCallback tem suporte experimental para callbacks thread-safe. Isso será necessário se você passar uma função callback para uma thread diferente do seu contexto de instanciação. Você pode habilitar isso com o parâmetro opcional threadsafe.
Atualmente, callbacks thread-safe funcionam melhor quando executados de outra thread que está executando código JavaScript, ou seja, um Worker. Uma versão futura do Bun permitirá que sejam chamados de qualquer thread (como novas threads spawnadas pela sua biblioteca nativa que o Bun não conhece).
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Opcional. Padrão `false`
});NOTE
**⚡️ Dica de desempenho** — Para um leve aumento de desempenho, passe diretamente `JSCallback.prototype.ptr` em vez do 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 roda ligeiramente mais rápido:
setOnResolve(onResolve.ptr);
// Comparado com isto:
setOnResolve(onResolve);Ponteiros
O Bun representa ponteiros como um number em JavaScript.
Como um ponteiro de 64 bits cabe em um número JavaScript?
Processadores de 64 bits suportam até 52 bits de espaço endereçável. Números JavaScript suportam 53 bits de espaço utilizável, então isso nos deixa com cerca de 11 bits de espaço extra.
Por que não BigInt? BigInt é mais lento. Engines JavaScript alocam um BigInt separado, o que significa que não podem caber em um valor JavaScript regular. Se você passar um BigInt para uma função, ele será convertido para number.
Para converter de um TypedArray para um ponteiro:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);Para converter de um ponteiro para um ArrayBuffer:
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
// toArrayBuffer aceita um `byteOffset` e `byteLength`
// se `byteLength` não for fornecido, assume-se que é um ponteiro null-terminado
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);Para ler dados de um ponteiro, você tem duas opções. Para ponteiros de longa duração, use um 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 ponteiros de curta duração, use 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),
);A função read se comporta de forma similar ao DataView, mas é geralmente mais rápida porque não precisa criar um DataView ou ArrayBuffer.
FFIType | Função 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 |
Gerenciamento de memória
bun:ffi não gerencia memória para você. Você deve liberar a memória quando terminar.
De JavaScript
Se você quer rastrear quando um TypedArray não está mais em uso de JavaScript, pode usar um FinalizationRegistry.
De C, Rust, Zig, etc
Se você quer rastrear quando um TypedArray não está mais em uso de C ou FFI, pode passar um callback e um ponteiro de contexto opcional para toArrayBuffer ou toBuffer. Esta função é chamada em algum momento depois, uma vez que o garbage collector libera o objeto JavaScript ArrayBuffer subjacente.
A assinatura esperada é a mesma que na API C do JavaScriptCore:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);import { toArrayBuffer } from "bun:ffi";
// com um deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// este é um ponteiro opcional para um callback
deallocatorContext,
// este é um ponteiro para uma função
jsTypedArrayBytesDeallocator,
);
// sem um deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// este é um ponteiro para uma função
jsTypedArrayBytesDeallocator,
);Segurança de memória
Usar ponteiros brutos fora do FFI é extremamente não recomendado. Uma versão futura do Bun pode adicionar uma flag CLI para desabilitar bun:ffi.
Alinhamento de ponteiro
Se uma API espera um ponteiro de tamanho diferente de char ou u8, certifique-se de que o TypedArray também tem esse tamanho. Um u64* não é exatamente o mesmo que [8]u8* devido ao alinhamento.
Passando um ponteiro
Onde funções FFI esperam um ponteiro, passe um TypedArray de tamanho equivalente:
import { dlopen, FFIType } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType's também podem ser especificados como strings
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 será passado como um ponteiro
pixels,
128,
128,
);O wrapper auto-gerado converte o ponteiro para um TypedArray.
Hardmode
Se você não quer a conversão automática ou quer um ponteiro para um offset de byte específico dentro do TypedArray, você também pode obter diretamente o ponteiro para o TypedArray:
import { dlopen, FFIType, ptr } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType's também podem ser especificados como strings
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
// isso retorna um número! não um BigInt!
const myPtr = ptr(pixels);
const out = encode_png(
myPtr,
// dimensões:
128,
128,
);Lendo ponteiros
const out = encode_png(
// pixels será passado como um ponteiro
pixels,
// dimensões:
128,
128,
);
// assumindo que é 0-terminado, pode ser lido assim:
let png = new Uint8Array(toArrayBuffer(out));
// salva em disco:
await Bun.write("out.png", png);