FFI: interoperando com C
Nesta aula, exploramos como o Rust interopera com a linguagem C através da FFI (Foreign Function Interface). Aprendemos a usar blocos extern "C", a expor funções Rust com #[no_mangle], a gerar bindings automaticamente com bindgen e a lidar com os principais cuidados de segurança ao cruzar a fronteira entre Rust e C.
Interoperar com C é uma das capacidades mais poderosas do Rust, permitindo reutilizar bibliotecas maduras, acessar APIs de sistema operacional e expor código Rust para outras linguagens. A FFI (Foreign Function Interface) do Rust é o mecanismo que possibilita essa comunicação, baseada na ABI (Application Binary Interface) do C. Nesta aula, vamos mergulhar nos detalhes práticos de como declarar funções C em Rust, como exportar funções Rust para C, como automatizar a geração de bindings com bindgen e, crucialmente, como manter a segurança que o Rust promete mesmo ao lidar com ponteiros e memória não gerenciada.
O tema é vasto e exige atenção redobrada, pois erros de segurança nesse contexto podem levar a vulnerabilidades graves. Vamos abordar desde os conceitos fundamentais até as boas práticas, com exemplos concretos que você pode testar em seu ambiente de desenvolvimento.
extern "C"
O primeiro passo para chamar funções de uma biblioteca C a partir do Rust é declarar essas funções dentro de um bloco extern "C". Esse bloco informa ao compilador que as funções seguem a convenção de chamada da linguagem C, ou seja, a ABI do C. Isso é essencial porque o Rust, por padrão, não garante uma ABI estável, podendo alterar a forma como os argumentos são passados entre versões. Ao usar extern "C", alinhamos nosso código com a ABI do C, permitindo que o linker encontre e chame as funções corretamente.
Dentro do bloco, declaramos os protótipos das funções como se fossem funções Rust, mas sem corpo. Por exemplo, para chamar a função abs da libc, faríamos:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Valor absoluto de -5: {}", abs(-5));
}
}Note o uso de unsafe: chamar uma função externa é uma operação insegura, pois o Rust não pode garantir que a função se comporte de acordo com as regras de segurança da linguagem. A declaração em si é segura, mas a chamada exige um bloco unsafe. Além disso, é importante marcar as funções com unsafe na declaração se elas tiverem pré-condições que o chamador deve garantir, mas isso é uma prática opcional que ajuda a documentar o contrato.
Outro ponto crucial: as funções C podem usar tipos que não têm equivalente direto em Rust, como ponteiros para void (void*), que mapeamos para *mut c_void ou *const c_void. A biblioteca padrão fornece o módulo std::ffi com tipos como c_int, c_char, CStr e CString para lidar com strings e tipos primitivos de C. Sempre prefira esses tipos em vez de usar os tipos nativos do Rust, pois eles têm tamanhos e alinhamentos compatíveis com C na plataforma alvo.
#[no_mangle]
Quando queremos expor funções Rust para serem chamadas por código C (ou por outras linguagens que usam a ABI do C), precisamos garantir que o nome da função no binário final seja exatamente o que o C espera. Por padrão, o Rust aplica name mangling aos nomes das funções, adicionando informações de tipo e módulo, o que torna o nome ilegível e incompatível com C. Para desativar esse processo, usamos o atributo #[no_mangle]. Ele instrui o compilador a usar o nome literal da função na tabela de símbolos.
Além do #[no_mangle], também devemos envolver a função em um bloco extern "C" para que a convenção de chamada seja a do C. Veja um exemplo de função Rust exportada para C:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}Depois de compilar essa biblioteca (por exemplo, com cargo build --release), podemos usar a função add em código C declarando-a como:
int add(int a, int b);É importante notar que #[no_mangle] também é usado em conjunto com pub para que o símbolo seja exportado. Sem pub, a função não seria visível externamente. Além disso, para evitar conflitos de símbolos em bibliotecas compartilhadas, é comum prefixar os nomes das funções com um identificador único, como o nome do crate.
Outra consideração: se a função Rust pode entrar em pânico (panic), devemos evitar que o unwind atravesse a fronteira FFI, pois isso é comportamento indefinido em C. A prática recomendada é usar catch_unwind dentro da função exportada e converter o panic em um erro de C, ou usar extern "C" com a opção panic = "abort" no perfil de compilação.
bindgen (visão geral)
Escrever declarações FFI manualmente para bibliotecas C grandes pode ser tedioso e propenso a erros. O bindgen é uma ferramenta que gera automaticamente bindings Rust a partir de cabeçalhos C (arquivos .h). Ele analisa as declarações e produz um arquivo Rust com as declarações extern, tipos, constantes e estruturas correspondentes. Isso economiza tempo e reduz a chance de erros de tradução.
Para usar o bindgen, normalmente adicionamos um script de build (build.rs) que chama a biblioteca bindgen em tempo de compilação. O script pode especificar os cabeçalhos a serem processados e opções como a geração de documentação ou a inclusão de tipos específicos. No Cargo.toml, adicionamos a dependência de build:
[build-dependencies]
bindgen = "0.69"E no build.rs, um exemplo mínimo:
use std::env;
use std::path::PathBuf;
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}Depois, no código Rust, incluímos o arquivo gerado com include!:
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));O bindgen também pode gerar bindings para tipos que usam ponteiros e estruturas, mapeando-os para tipos Rust equivalentes, como *mut c_void e structs com #[repr(C)]. É uma ferramenta extremamente útil para integrar bibliotecas como OpenSSL, SQLite ou APIs de sistema. No entanto, é importante revisar o código gerado, pois ele pode incluir blocos unsafe e exigir tratamento manual para casos especiais.
Cuidados de segurança
A FFI é uma das áreas mais propensas a bugs de segurança em Rust, porque quebra as garantias de segurança de memória da linguagem. Ao chamar funções C ou expor funções para C, devemos tomar cuidado redobrado para evitar undefined behavior. Os principais riscos são: ponteiros nulos, ponteiros pendentes, acessos fora dos limites, data races e vazamento de memória.
Uma regra de ouro é: nunca confie em dados vindos do C sem validá-los. Por exemplo, se uma função C retorna um ponteiro, devemos verificar se não é nulo antes de usá-lo. Além disso, ao receber uma string de C, devemos usar CStr para convertê-la com segurança para &str, garantindo que a terminação nula esteja presente e que não haja bytes inválidos. Ao contrário, para passar strings para C, usamos CString, que garante a terminação nula e não contém bytes nulos internos.
Outro ponto crítico é o gerenciamento de memória. Se uma função C aloca memória que deve ser liberada pelo chamador, precisamos garantir que a liberação seja feita corretamente. O Rust não pode usar o RAII para memória alocada por C, então muitas vezes precisamos de wrappers que implementem Drop para chamar a função de liberação. Por exemplo, se temos uma função create() que retorna um ponteiro e destroy() que libera, podemos criar uma struct que implementa Drop para chamar destroy() automaticamente.
Também devemos evitar compartilhar referências entre Rust e C de forma desprotegida. Se C guarda um ponteiro para dados Rust, precisamos garantir que o Rust não mova ou invalide esses dados enquanto C estiver usando. Isso pode exigir o uso de Box::into_raw para obter um ponteiro estável e Box::from_raw para recuperar a propriedade. Em ambientes com threads, é essencial usar Send e Sync apropriadamente e evitar data races.
Por fim, ao expor funções Rust para C, devemos garantir que a função não possa causar panics que atravessem a fronteira, pois isso é undefined behavior. Use catch_unwind ou configure o perfil para abortar em pânico. Lembre-se também de que tipos como enum e struct que não são #[repr(C)] não têm representação estável, então use #[repr(C)] para qualquer tipo que cruze a fronteira.
Boas práticas e observações finais
Para concluir, listamos algumas boas práticas ao trabalhar com FFI em Rust:
- Sempre use os tipos do módulo
std::ffi(comoc_int,c_char) em vez de tipos nativos. - Documente as pré-condições e responsabilidades de memória de cada função FFI.
- Quando possível, crie wrappers seguros que encapsulem o código
unsafee exponham uma API segura para o resto do crate. - Teste exaustivamente com ferramentas como Valgrind ou AddressSanitizer para detectar vazamentos e acessos inválidos.
- Prefira usar bibliotecas já existentes como
libcpara funções comuns, em vez de redefinir tudo.
A FFI é uma ferramenta poderosa, mas exige disciplina. Com os cuidados certos, você pode aproveitar o melhor dos dois mundos: a segurança e produtividade do Rust, e o ecossistema vasto de C.
Referências
- The Rustonomicon: FFI
- Documentação do módulo std::ffi
- Referência da linguagem: External blocks
- Cargo: Build Scripts
- Documentação do bindgen
- The Rust Book: Unsafe Rust
Exercícios
Declare e chame a função
strlenda libc (que retorna o tamanho de uma string C) usandoextern "C". UseCStringpara passar a string e imprima o resultado.✓ Resposta:use std::ffi::CString; extern "C" { fn strlen(s: *const std::os::raw::c_char) -> usize; } fn main() { let s = CString::new("Hello, FFI!").unwrap(); unsafe { let len = strlen(s.as_ptr()); println!("Length: {}", len); } }Exporte uma função Rust
multiplyque recebe dois inteiros e retorna o produto, usando#[no_mangle]eextern "C". Mostre como ela seria declarada em C.✓ Resposta:#[no_mangle] pub extern "C" fn multiply(a: i32, b: i32) -> i32 { a * b }Em C:
int multiply(int a, int b);Escreva um
build.rsque use bindgen para gerar bindings a partir de um cabeçalhowrapper.hque contém uma funçãoint add(int, int);. Inclua o arquivo gerado em seu código e chame a função.✓ Resposta:// build.rs use std::env; use std::path::PathBuf; fn main() { let bindings = bindgen::Builder::default() .header("wrapper.h") .generate() .expect("Unable to generate bindings"); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); bindings .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); } // main.rs include!(concat!(env!("OUT_DIR"), "/bindings.rs")); fn main() { unsafe { println!("3 + 4 = {}", add(3, 4)); } }Dado o seguinte código C que aloca memória e retorna um ponteiro, explique como você criaria um wrapper seguro em Rust que libera a memória automaticamente quando o valor não é mais usado.
void* create_buffer(size_t size); void free_buffer(void* ptr);✓ Resposta:use std::ffi::c_void; use std::ptr; extern "C" { fn create_buffer(size: usize) -> *mut c_void; fn free_buffer(ptr: *mut c_void); } struct Buffer { ptr: *mut c_void, } impl Buffer { fn new(size: usize) -> Self { let ptr = unsafe { create_buffer(size) }; if ptr.is_null() { panic!("create_buffer returned null"); } Buffer { ptr } } } impl Drop for Buffer { fn drop(&mut self) { unsafe { free_buffer(self.ptr) }; } } fn main() { let buf = Buffer::new(1024); // Use buf... } // buf é liberado automaticamente aquiConsidere a seguinte função Rust exportada para C. Ela pode entrar em pânico. Modifique-a para que o pânico não atravesse a fronteira FFI, usando
catch_unwinde retornando um código de erro.#[no_mangle] pub extern "C" fn safe_div(a: i32, b: i32) -> i32 { a / b }✓ Resposta:use std::panic::{catch_unwind, AssertUnwindSafe}; #[no_mangle] pub extern "C" fn safe_div(a: i32, b: i32) -> i32 { let result = catch_unwind(AssertUnwindSafe(|| a / b)); match result { Ok(v) => v, Err(_) => { // Retorna um valor sentinela ou define um errno // Exemplo: -1 -1 } } }