FFI em Rust: Interoperabilidade com C e Outras Linguagens
FFI (Foreign Function Interface) é o mecanismo que permite código Rust chamar funções escritas em outras linguagens, particularmente C, e vice-versa. Rust oferece suporte robusto a FFI através de palavras-chave como extern e atributos de compilação, mantendo segurança de memória mesmo ao cruzar fronteiras linguísticas. Este é um tópico crítico para integrar bibliotecas legadas, usar APIs do sistema operacional ou otimizar gargalos com código compilado de terceiros.
Por que FFI importa
A maioria dos projetos Rust em produção precisam interagir com código C em algum momento—seja libc no Linux, APIs do Windows, ou bibliotecas científicas como OpenSSL e BLAS. Dominar FFI significa poder aproveitar todo o ecossistema existente enquanto mantém a confiabilidade do Rust.
Fundamentos: Chamando C a partir de Rust
Declarações extern
Para chamar uma função C, você a declara usando extern "C". O bloco extern "C" instrui o compilador a usar a convenção de chamada C e não fazer name-mangling, essencial para compatibilidade.
// Declarando funções C do libc
extern "C" {
fn strlen(s: *const u8) -> usize;
fn printf(format: *const u8, ...) -> i32;
}
fn main() {
let text = b"Hello, FFI!\0";
unsafe {
let len = strlen(text.as_ptr());
println!("Comprimento: {}", len);
}
}
Todo acesso a código C exige bloco unsafe porque o compilador Rust não pode verificar se a função respeitará os contratos de segurança. A responsabilidade recai sobre o programador: garantir ponteiros válidos, tipos corretos e que a função não violará invariantes de memória.
Tipos nativos em FFI
Rust mapeia tipos para C naturalmente: i32 → int, u64 → uint64_t, f64 → double. Para tipos mais complexos ou opacos, use ponteiros (*const ou *mut). Strings em C são *const u8 (null-terminated). Estruturas podem ser representadas diretamente se seu layout for compatível com #[repr(C)].
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn process_point(p: Point) -> f64;
}
fn main() {
let pt = Point { x: 3.0, y: 4.0 };
unsafe {
let result = process_point(pt);
println!("Resultado: {}", result);
}
}
Exportando Rust para C: Tornando Rust uma Biblioteca
Compilando como biblioteca C
Você pode compilar Rust como arquivo objeto .o ou biblioteca dinâmica .so/.dll. Configure seu Cargo.toml com crate-type = ["cdylib"] para gerar uma biblioteca que C pode consumir.
[lib]
crate-type = ["cdylib"]
[package]
name = "minha_lib"
version = "0.1.0"
Funções exportadas devem usar extern "C" e ser públicas. Mantenha a simplicidade: evite tipos complexos de Rust, prefira tipos primitivos e ponteiros.
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn saudacao(nome: *const c_char) -> *mut c_char {
let nome_rust = unsafe {
CStr::from_ptr(nome).to_str().unwrap_or("Desconhecido")
};
let msg = format!("Olá, {}!", nome_rust);
let ptr = Box::into_raw(msg.into_boxed_str() as Box<[u8]>);
ptr as *mut c_char
}
#[no_mangle]
pub extern "C" fn liberar_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe { Box::from_raw(ptr); }
}
}
O atributo #[no_mangle] previne que o compilador renomeie a função, garantindo que C a encontre pelo nome esperado. Sempre forneça uma função para liberar memória alocada em Rust.
Gerenciamento de Segurança em FFI
Validação e bounds checking
Código C não respeita tipos Rust. Um ponteiro pode ser inválido, nulo ou apontar para memória já liberada. Antes de desreferenciar, sempre valide:
extern "C" {
fn get_buffer(size: *mut usize) -> *mut u8;
fn free_buffer(ptr: *mut u8);
}
fn safe_wrapper() -> Vec<u8> {
let mut size = 0usize;
let ptr = unsafe { get_buffer(&mut size as *mut usize) };
if ptr.is_null() || size == 0 {
return Vec::new();
}
let slice = unsafe { std::slice::from_raw_parts(ptr, size) };
let vec = slice.to_vec();
unsafe { free_buffer(ptr); }
vec
}
Garantias de thread-safety
Se sua função será chamada de múltiplas threads, use sincronização explícita. C não garante thread-safety por padrão—essa responsabilidade é sua ao exportar funções.
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref COUNTER: Mutex<i32> = Mutex::new(0);
}
#[no_mangle]
pub extern "C" fn increment() -> i32 {
let mut count = COUNTER.lock().unwrap();
*count += 1;
*count
}
Boas Práticas e Padrões
Use wrappers seguros
Sempre encapsule unsafe em funções seguras que validam precondições:
pub fn chamar_c_seguro(valor: i32) -> Result<i32, String> {
if valor < 0 {
return Err("Valor negativo não permitido".to_string());
}
unsafe {
Ok(minha_funcao_c(valor))
}
}
extern "C" {
fn minha_funcao_c(x: i32) -> i32;
}
Crates para FFI
O ecossistema oferece ferramentas valiosas: bindgen gera automaticamente bindings Rust a partir de headers C, cc compila código C junto com seu projeto, e libc fornece tipos e funções do C padrão. Use-as para reduzir erros manuais.
cargo add bindgen --build
Conclusão
FFI em Rust abre acesso a décadas de código C otimizado e testado, mas exige disciplina. Três pontos fundamentais: (1) use extern "C" e blocos unsafe conscientemente, validando sempre ponteiros e tamanhos; (2) encapsule FFI em abstrações seguras que escondem detalhes perigosos do resto do código; (3) aproveite ferramentas como bindgen para minimizar erros de tradução manual. Dominar FFI transforma Rust de uma linguagem isolada em um membro de primeira classe do ecossistema de sistemas.