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:
- Definir a interface das funções nativas que você quer chamar
- Carregar a biblioteca dinâmica usando
Deno.dlopen() - 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
voidsó pode ser usado como tipo de resultado. - [2] O tipo
bufferaceita TypedArrays como parâmetro, mas sempre retorna um objeto ponteiro ounullquando usado como tipo de resultado, como o tipopointer. - [3] O tipo
functionfunciona exatamente como o tipopointercomo parâmetro e tipo de resultado. - [4] O tipo
structé usado para passar e retornar structs C por valor (cópia). O arraystructdeve 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 camposu8para evitar padding. Apenas TypedArrays são aceitos como structs, e structs sempre são retornadas comoUint8Arrays.
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.tsVocê 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
structconté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 peloTypedArrayapropriado (ou por umDataView) 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
Sempre feche recursos. Feche bibliotecas com
dylib.close()e callbacks comcallback.close()quando terminar.Prefira TypeScript. Use TypeScript para melhor checagem de tipos ao trabalhar com FFI.
Envolva chamadas FFI em blocos try/catch para lidar com erros graciosamente.
Tenha extremo cuidado ao usar FFI, pois código nativo pode contornar o sandbox de segurança do Deno.
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.rsDepois 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.commandpara 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.