Skip to content

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:

ts
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

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

Para compilar:

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

Passe um caminho para a biblioteca compartilhada e um mapa de símbolos para importar no 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

Os seguintes valores FFIType são suportados.

FFITypeTipo CAliases
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: 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
  • length armazenado 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 \0 encontrado
  • 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:

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

ts
const myString = new CString(ptr);

Para converter de um ponteiro com comprimento conhecido para uma string JavaScript:

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

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

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

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

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

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

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

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

Para converter de um ponteiro para um ArrayBuffer:

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

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 ponteiros de curta duração, use 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),
);

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.

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

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

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

ts
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

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

Bun by www.bunjs.com.cn edit