A Foreign Function Interface (FFI) do Deno permite que código JavaScript e TypeScript chame funções em bibliotecas dinâmicas escritas em linguagens como C, C++ ou Rust. Isso permite integrar performance e capacidades de código nativo diretamente às suas aplicações Deno.

Documentação de referência da FFI do Deno

Introdução à FFI

FFI fornece uma ponte entre o runtime JavaScript do Deno e código nativo. Isso permite que você:

  • Use bibliotecas nativas existentes dentro das suas aplicações Deno
  • Implemente código crítico para performance em linguagens como Rust ou C
  • Acesse APIs do sistema operacional e recursos de hardware que não estão disponíveis diretamente em JavaScript

A implementação de FFI do Deno é baseada na API Deno.dlopen, que carrega bibliotecas dinâmicas e cria bindings JavaScript para as funções que elas exportam.

Considerações de segurança

FFI exige permissão explícita usando a flag --allow-ffi, pois código nativo roda fora do sandbox de segurança do Deno:

deno run --allow-ffi my_ffi_script.ts

:::info

Aviso importante de segurança: diferente de código JavaScript rodando no sandbox do Deno, bibliotecas nativas carregadas via FFI têm o mesmo nível de acesso que o próprio processo Deno. Isso significa que elas podem:

  • Acessar o sistema de arquivos
  • Fazer conexões de rede
  • Acessar variáveis de ambiente
  • Executar comandos do sistema

Sempre garanta que você confia nas bibliotecas nativas que está carregando por FFI.

:::

Uso básico

O padrão básico para usar FFI no Deno envolve:

  1. Definir a interface das funções nativas que você quer chamar
  2. Carregar a biblioteca dinâmica usando Deno.dlopen()
  3. Chamar as funções carregadas

Aqui está um exemplo simples carregando uma biblioteca C:

const dylib = Deno.dlopen("libexample.so", {
  add: { parameters: ["i32", "i32"], result: "i32" },
});

console.log(dylib.symbols.add(5, 3)); // 8

dylib.close();

Tipos aceitos

A FFI do Deno aceita uma variedade de tipos de dados para parâmetros e valores de retorno:

FFI Type Deno C Rust
i8 number char / signed char i8
u8 number unsigned char u8
i16 number short int i16
u16 number unsigned short int u16
i32 number int / signed int i32
u32 number unsigned int u32
i64 bigint long long int i64
u64 bigint unsigned long long int u64
usize bigint size_t usize
isize bigint size_t isize
f32 number float f32
f64 number double f64
void[1] undefined void ()
pointer {} | null void * *mut c_void
buffer[2] TypedArray | null uint8_t * *mut u8
function[3] {} | null void (*fun)() Option<extern "C" fn()>
{ struct: [...] }[4] TypedArray struct MyStruct MyStruct

Desde o Deno 1.25, o tipo pointer foi dividido em pointer e buffer para garantir que usuários aproveitem otimizações para Typed Arrays; e, desde o Deno 1.31, a representação JavaScript de pointer se tornou um objeto ponteiro opaco ou null para ponteiros nulos.

  • [1] O tipo void só pode ser usado como tipo de resultado.
  • [2] O tipo buffer aceita TypedArrays como parâmetro, mas sempre retorna um objeto ponteiro ou null quando usado como tipo de resultado, como o tipo pointer.
  • [3] O tipo function funciona exatamente como o tipo pointer como parâmetro e tipo de resultado.
  • [4] O tipo struct é usado para passar e retornar structs C por valor (cópia). O array struct deve enumerar o tipo de cada campo da struct em ordem. As structs recebem padding automaticamente: structs packed podem ser definidas usando uma quantidade apropriada de campos u8 para evitar padding. Apenas TypedArrays são aceitos como structs, e structs sempre são retornadas como Uint8Arrays.

Trabalhando com structs

Para passar ou retornar uma struct C por valor, descreva seu layout com { struct: [...] } — um array que lista o tipo FFI de cada campo na ordem de declaração. Valores de struct são passados como um TypedArray cujos bytes correspondem ao layout C, e structs retornadas por valor voltam como um Uint8Array do tamanho correto. O array struct na tabela de tipos acima nesta página é o formato autoritativo.

Suponha que você tenha esta pequena biblioteca C que opera em um Point 2D:

typedef struct {
  double x;
  double y;
} Point;

double distance(Point a, Point b) {
  double dx = a.x - b.x;
  double dy = a.y - b.y;
  return __builtin_sqrt(dx * dx + dy * dy);
}

Point midpoint(Point a, Point b) {
  Point m;
  m.x = (a.x + b.x) / 2.0;
  m.y = (a.y + b.y) / 2.0;
  return m;
}

Compile-a como uma biblioteca compartilhada. As flags do compilador e o nome do arquivo de saída variam por plataforma:

cc -shared -fPIC -O2 -o libpoint.so point.c
cc -dynamiclib -O2 -o libpoint.dylib point.c
cl /LD /O2 point.c /Fe:point.dll

Depois chame essa biblioteca a partir do Deno, usando o nome de arquivo da sua plataforma em Deno.dlopen. Observe que a definição struct é um array de tipos de campo na ordem de declaração, não um objeto com campos nomeados:

// `Point` mirrors the C `struct Point { double x; double y; }`.
const Point = { struct: ["f64", "f64"] } as const;

const lib = Deno.dlopen(
  "./libpoint.so",
  {
    distance: { parameters: [Point, Point], result: "f64" },
    midpoint: { parameters: [Point, Point], result: Point },
  } as const,
);

// Build struct values as a TypedArray whose bytes match the C layout.
// Two f64 fields → two slots in a Float64Array.
const a = new Float64Array([1.0, 2.0]); // Point { x: 1.0, y: 2.0 }
const b = new Float64Array([4.0, 6.0]); // Point { x: 4.0, y: 6.0 }

// FFI reads the underlying bytes, so pass the buffer as a Uint8Array view.
const aBytes = new Uint8Array(a.buffer);
const bBytes = new Uint8Array(b.buffer);

console.log("distance =", lib.symbols.distance(aBytes, bBytes));

// A struct returned by value comes back as a Uint8Array sized to the struct.
// Wrap it in a Float64Array to read the fields back out.
const midBytes = lib.symbols.midpoint(aBytes, bBytes);
const mid = new Float64Array(midBytes.buffer);
console.log("midpoint =", { x: mid[0], y: mid[1] });

lib.close();

Execute com a permissão --allow-ffi:

deno run --allow-ffi point.ts

Você deve ver:

distance = 5
midpoint = { x: 2.5, y: 4 }

Algumas coisas para lembrar ao trabalhar com structs:

  • O layout corresponde ao compilador C. O Deno aplica padding em campos de struct da mesma forma que seu compilador C. Se você precisa de uma struct packed, faça o padding explicitamente com campos u8, como observado na tabela de tipos acima.
  • A ordem dos campos é posicional. O array struct contém apenas tipos, na ordem de declaração — não há nomes de campo no lado JavaScript. O TypedArray que você passa deve organizar os campos na mesma ordem.
  • Structs retornadas são bytes. Um resultado struct é sempre um Uint8Array; visualize-o pelo TypedArray apropriado (ou por um DataView) para ler os campos.

Trabalhando com callbacks

Você pode passar funções JavaScript como callbacks para código nativo:

const signatures = {
  setCallback: {
    parameters: ["function"],
    result: "void",
  },
  runCallback: {
    parameters: [],
    result: "void",
  },
} as const;

// Create a callback function
const callback = new Deno.UnsafeCallback(
  { parameters: ["i32"], result: "void" } as const,
  (value) => {
    console.log("Callback received:", value);
  },
);

// Pass the callback to the native library
dylib.symbols.setCallback(callback.pointer);

// Later, this will trigger our JavaScript function
dylib.symbols.runCallback();

// Always clean up when done
callback.close();

Boas práticas com FFI

  1. Sempre feche recursos. Feche bibliotecas com dylib.close() e callbacks com callback.close() quando terminar.

  2. Prefira TypeScript. Use TypeScript para melhor checagem de tipos ao trabalhar com FFI.

  3. Envolva chamadas FFI em blocos try/catch para lidar com erros graciosamente.

  4. Tenha extremo cuidado ao usar FFI, pois código nativo pode contornar o sandbox de segurança do Deno.

  5. Mantenha a interface FFI o menor possível para reduzir a superfície de ataque.

Exemplos

Usando uma biblioteca Rust

Aqui está um exemplo de criação e uso de uma biblioteca Rust com Deno:

Primeiro, crie uma biblioteca Rust:

// lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn fibonacci(n: u32) -> u32 {
  if n <= 1 {
    return n;
  }
  fibonacci(n - 1) + fibonacci(n - 2)
}

Compile-a como uma biblioteca dinâmica:

rustc --crate-type cdylib lib.rs

Depois use-a a partir do Deno:

const libName = {
  windows: "./lib.dll",
  linux: "./liblib.so",
  darwin: "./liblib.dylib",
}[Deno.build.os];

const dylib = Deno.dlopen(
  libName,
  {
    fibonacci: { parameters: ["u32"], result: "u32" },
  } as const,
);

// Calculate the 10th Fibonacci number
const result = dylib.symbols.fibonacci(10);
console.log(`Fibonacci(10) = ${result}`); // 55

dylib.close();

Exemplos

Esses repositórios mantidos pela comunidade incluem exemplos funcionais de integrações FFI com várias bibliotecas nativas em diferentes sistemas operacionais.

Abordagens relacionadas para integração com código nativo

Embora a FFI do Deno forneça uma forma direta de chamar funções nativas, existem outras abordagens para integrar código nativo:

Usando Node-API (N-API) com Deno

O Deno oferece suporte a Node-API (N-API) para compatibilidade com addons nativos do Node.js. Isso permite usar módulos nativos existentes escritos para Node.js.

Carregando diretamente um addon Node-API:

import process from "node:process";
process.dlopen(module, "./native_module.node", 0);

Usando um pacote npm que usa um addon Node-API:

import someNativeAddon from "npm:some-native-addon";
console.log(someNativeAddon.doSomething());

Como isso é diferente de FFI?

Aspecto FFI Suporte a Node-API
Setup Sem etapa de build Exige binários pré-compilados ou etapa de build
Portabilidade Ligada ao ABI da biblioteca ABI estável entre versões
Caso de uso Chamadas diretas de biblioteca Reutilizar addons Node.js

O suporte a Node-API é ideal para aproveitar módulos nativos existentes do Node.js, enquanto FFI é melhor para chamadas diretas e leves a bibliotecas nativas.

Alternativas à FFI

Antes de usar FFI, considere estas alternativas:

  • WebAssembly, para código nativo portável que roda dentro do sandbox do Deno.
  • Use Deno.command para executar binários externos e subprocessos com permissões controladas.
  • Verifique se as APIs nativas do Deno já fornecem a funcionalidade de que você precisa.

As capacidades de FFI do Deno fornecem integração poderosa com código nativo, permitindo otimizações de performance e acesso a funcionalidade de nível de sistema. No entanto, esse poder vem com considerações de segurança significativas. Sempre tenha cautela ao trabalhar com FFI e garanta que confia nas bibliotecas nativas que está usando.