Por que FFI continua importante para Rust
Rust cresce em backend, WebAssembly, infraestrutura e ferramentas para devs, mas uma parte enorme do software crítico ainda vive em C e C++. Drivers, SDKs de hardware, bibliotecas de criptografia, codecs, bancos de dados, engines, módulos de visão computacional e sistemas industriais não são reescritos de uma vez. Para entrar nesses ambientes sem exigir uma migração total, Rust precisa conversar bem com código existente. É aí que Rust FFI vira uma habilidade prática, não apenas um tópico avançado.
FFI significa Foreign Function Interface: a fronteira entre Rust e outra linguagem. Na prática, quase sempre essa fronteira usa ABI C, porque C é o denominador comum entre compiladores, sistemas operacionais e runtimes. O Rust chama uma função C, C chama uma função Rust, ou os dois lados compartilham structs, ponteiros e buffers com regras muito explícitas.
Para quem busca vagas Rust em sistemas, plataforma, segurança, embarcados ou performance, FFI é um diferencial forte. Muitas empresas não começam com um produto 100% Rust. Elas começam substituindo uma parte insegura, escrevendo um módulo novo, criando uma biblioteca de alta performance ou encapsulando um SDK legado. Saber fazer isso sem transformar unsafe em um buraco de manutenção é exatamente o tipo de competência que separa protótipo de produção.
O modelo mental: fronteira pequena, camada segura
O erro comum é tratar FFI como autorização para espalhar unsafe pelo projeto. O caminho saudável é o oposto: mantenha a fronteira pequena, isole o código inseguro e exponha uma API Rust normal para o restante da aplicação. O módulo que conversa com C pode lidar com ponteiros, repr(C), nulidade, tamanho de buffers e códigos de erro. O restante do código deve trabalhar com tipos seguros, Result, slices, structs próprias e ownership claro.
Pense em três camadas. A primeira é a assinatura bruta que espelha a API C. A segunda é um wrapper interno que valida argumentos, converte erros e documenta invariantes. A terceira é a API pública Rust que o resto do time usa. Se alguém precisa ler a documentação da biblioteca C para chamar uma função comum do seu crate, a abstração provavelmente está vazando demais.
Esse desenho conversa diretamente com o guia de unsafe Rust. unsafe não significa “código perigoso por definição”; significa “o compilador não consegue provar algumas garantias sozinho”. Em FFI, você assume responsabilidade por validade de ponteiros, alinhamento, lifetime, thread-safety, ownership e compatibilidade binária. Quanto menor a superfície, menor o risco.
Chamando C a partir de Rust com bindgen
bindgen é a ferramenta mais comum quando você já tem uma biblioteca C e quer gerar bindings Rust a partir de arquivos .h. Ele lê headers com Clang e produz declarações Rust compatíveis com structs, enums, constantes e funções. Isso evita escrever manualmente centenas de assinaturas e reduz divergência quando o header muda.
Um fluxo típico começa com um wrapper.h pequeno:
#include "vendor_sdk.h"
Depois, no build.rs, você gera bindings durante o build:
fn main() {
println!("cargo:rustc-link-lib=vendor_sdk");
println!("cargo:rerun-if-changed=wrapper.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.allowlist_function("vendor_.*")
.allowlist_type("vendor_.*")
.generate()
.expect("falha ao gerar bindings");
bindings
.write_to_file(std::env::var("OUT_DIR").unwrap() + "/bindings.rs")
.expect("falha ao escrever bindings");
}
No código Rust, a recomendação é incluir os bindings em um módulo privado e escrever wrappers seguros ao redor. Evite reexportar tudo que bindgen gerou. Headers C costumam expor detalhes que não deveriam virar API estável do seu crate.
mod ffi {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
pub fn versao_do_sdk() -> Result<String, SdkError> {
let ptr = unsafe { ffi::vendor_version() };
if ptr.is_null() {
return Err(SdkError::SemVersao);
}
let texto = unsafe { std::ffi::CStr::from_ptr(ptr) };
Ok(texto.to_string_lossy().into_owned())
}
O ponto importante não é decorar a API de bindgen. É entender que bindings gerados são uma matéria-prima. A qualidade de produção vem do wrapper, dos testes, da documentação e da disciplina sobre quais tipos atravessam a fronteira.
Expondo Rust para C/C++ com cbindgen
O caminho inverso acontece quando você quer escrever a parte nova em Rust e permitir que um produto C ou C++ consuma essa biblioteca. Nesse cenário, cbindgen gera headers C/C++ a partir de tipos e funções Rust compatíveis. Ele é útil para migração gradual: você entrega uma biblioteca dinâmica ou estática, publica um header e mantém o sistema legado chamando uma API estável.
Uma função exportada precisa usar ABI C e controlar nome de símbolo:
#[repr(C)]
pub struct ResultadoProcessamento {
pub codigo: i32,
pub bytes_processados: usize,
}
#[no_mangle]
pub extern "C" fn processar_buffer(
entrada: *const u8,
tamanho: usize,
) -> ResultadoProcessamento {
if entrada.is_null() {
return ResultadoProcessamento { codigo: -1, bytes_processados: 0 };
}
let dados = unsafe { std::slice::from_raw_parts(entrada, tamanho) };
ResultadoProcessamento { codigo: 0, bytes_processados: dados.len() }
}
Esse exemplo é simples, mas já mostra decisões importantes. A função não recebe Vec<u8> nem String, porque esses tipos são específicos do Rust. Ela recebe ponteiro e tamanho, uma convenção comum em C. A struct usa #[repr(C)] para layout previsível. Erros atravessam a fronteira como código, não como panic.
Com cbindgen, o header gerado documenta a superfície pública. Isso obriga o time a pensar em estabilidade: quais structs são parte do contrato? Quem aloca memória? Quem libera? O que acontece se a versão C chama uma função de uma biblioteca Rust mais nova? Essas perguntas são menos glamourosas que performance, mas são as que evitam incidentes.
Ownership, memória e strings: onde bugs nascem
A maior parte dos bugs de FFI aparece em ownership e representação. Quem é dono de um ponteiro? Quem pode liberar a memória? O buffer continua válido depois da chamada? A string é UTF-8, Latin-1 ou bytes arbitrários? O ponteiro pode ser nulo? A função é thread-safe? O callback pode ser chamado depois que o objeto Rust foi destruído?
Uma regra prática: nunca deixe ownership implícito. Se C aloca, C libera. Se Rust aloca para C usar, forneça também uma função Rust explícita para liberar. Se C passa um buffer emprestado, documente que a função Rust não armazena o ponteiro depois de retornar. Se Rust precisa guardar algo entre chamadas, exponha um handle opaco em vez de compartilhar a struct interna.
Strings merecem cuidado especial. CString ajuda a enviar texto Rust para C sem bytes nulos internos. CStr ajuda a ler strings terminadas em \0. Mas isso não resolve encoding automaticamente. Muitas APIs legadas não são UTF-8. Em Windows, algumas APIs usam wide strings. Em SDKs antigos, documentação incompleta pode esconder convenções próprias. Teste com acentos brasileiros de verdade: ação, memória, São Paulo, configuração. Isso pega problemas que exemplos ASCII nunca mostram.
Build, CI e distribuição
FFI também é problema de build. Você pode ter um crate Rust correto e ainda falhar porque o runner de CI não tem Clang, a biblioteca C não está no LD_LIBRARY_PATH, o header mudou, o target de cross-compilation não tem toolchain C compatível ou o pacote final não inclui a .so, .dylib ou .dll necessária.
Para produção, trate a integração como parte do produto. Documente dependências nativas. Fixe versões quando possível. Rode CI em Linux e no sistema operacional que seus usuários realmente usam. Se a biblioteca roda em embarcados, teste o target certo. Se o output é um SDK, publique exemplos mínimos em C ou C++ que compilam contra o header gerado.
O conteúdo sobre release engineering em Rust vale muito aqui: checksums, artefatos reproduzíveis, smoke tests e versionamento não são luxo. Em FFI, o consumidor pode nem ser Rust. Um erro de ABI pode aparecer só no runtime de outro produto.
FFI em embarcados, indústria e migração gradual
No Brasil, FFI aparece com frequência em cenários onde Rust entra como modernização incremental. Uma empresa industrial pode ter firmware, drivers e ferramentas internas em C. Uma fintech pode usar bibliotecas nativas de criptografia ou comunicação com HSM. Uma empresa de dados pode ter um motor C++ antigo que precisa de um módulo novo mais seguro. Um time de produto pode querer expor uma biblioteca Rust para Python, Node ou mobile sem reescrever tudo.
Isso conecta FFI a Rust para embarcados, sistemas embarcados com Embassy, Rust vs C e Rust vs C++. A discussão real raramente é “Rust substitui tudo amanhã”. A discussão madura é: qual módulo novo vale escrever em Rust, qual parte legada continua em C/C++, qual fronteira é estável e como provar que a transição reduz risco?
Quem também acompanha linguagens de sistemas pode comparar esse caminho com materiais do Zig Brasil, porque Zig também é forte em interoperabilidade C. A diferença é que Rust força uma camada mais explícita de segurança e ownership, o que pode ser excelente quando o objetivo é conter riscos de memória em módulos novos.
Projeto de portfólio: wrapper seguro para uma biblioteca C
Um bom projeto público não precisa integrar um SDK secreto. Escolha uma biblioteca C pequena e real, como uma biblioteca de compressão, hash, parser, imagem ou áudio. Gere bindings com bindgen, escreva um wrapper Rust seguro, cubra casos de erro e publique exemplos.
O README deve explicar a fronteira: quais funções C são chamadas, quais invariantes o wrapper garante, como memória é alocada, quais tipos são seguros, como rodar testes e como compilar em CI. Inclua também um microbenchmark com Criterion se performance for parte da motivação, mas não faça benchmark virar a única história. Segurança e ergonomia importam tanto quanto velocidade.
Para um segundo nível, exponha uma função Rust via cbindgen e crie um exemplo C que chama essa função. Isso mostra domínio dos dois sentidos da integração. Em entrevista, esse projeto permite discutir unsafe, ABI, repr(C), ponteiros, ownership, cross-compilation, CI e documentação de contratos. É muito mais forte do que apenas dizer que você “sabe Rust”.
Checklist antes de levar FFI para produção
Antes de chamar uma integração FFI de pronta, revise estes pontos:
- a superfície
unsafeestá isolada em poucos módulos; - todo ponteiro nulo, tamanho inválido e código de erro é tratado;
- ownership de buffers, strings e handles está documentado;
- tipos que atravessam a ABI usam representação compatível, como
#[repr(C)]; panicnão atravessa a fronteira C;- headers gerados ou consumidos são versionados junto do código;
- CI compila os exemplos do lado consumidor, não só o crate Rust;
- testes incluem acentos, entradas vazias, buffers grandes e falhas simuladas;
- release inclui bibliotecas nativas, headers e instruções de linking;
- a API pública Rust esconde detalhes brutos que não precisam vazar.
Rust FFI é uma das áreas onde a linguagem mostra seu valor com mais clareza: ela não promete apagar o legado por mágica, mas permite criar uma fronteira mais segura, testável e evolutiva ao redor dele. Em 2026, essa habilidade é especialmente útil para quem quer trabalhar com sistemas reais, onde código novo e código antigo precisam conviver por muitos anos.