FFI: interoperando com C
Esta aula aborda a interoperabilidade entre Rust e C através da FFI (Foreign Function Interface). Você aprenderá a declarar funções externas com extern "C", exportar funções Rust para C usando #[no_mangle], terá uma visão geral da ferramenta bindgen e conhecerá os cuidados de segurança essenciais para evitar bugs e vulnerabilidades.
Nesta aula, vamos explorar como Rust pode interoperar com código C, permitindo que você chame bibliotecas C a partir de Rust e também exponha funções Rust para serem usadas em C. Essa capacidade é fundamental para integrar Rust em projetos existentes, aproveitar bibliotecas maduras do ecossistema C e construir pontes entre linguagens.
A FFI (Foreign Function Interface) do Rust é baseada na ABI (Application Binary Interface) do C, o que significa que podemos declarar funções que seguem as convenções de chamada do C e manipular dados com representações compatíveis. Veremos os mecanismos essenciais para criar essa ponte com segurança.
extern "C"
A palavra-chave extern em Rust é usada para declarar funções ou blocos de funções que vêm de fora do Rust. Quando combinada com a sintaxe extern "C", estamos indicando que a função segue a ABI do C, ou seja, usa a convenção de chamada padrão do C (cdecl na maioria das plataformas). Isso permite que o compilador Rust saiba como passar argumentos e ler retornos de acordo com as regras do C.
Para chamar funções de uma biblioteca C, você precisa declará-las dentro de um bloco extern. Por exemplo:
extern "C" {
fn printf(format: *const u8, ...) -> i32;
}
fn main() {
unsafe {
printf(b"Olá, mundo!\n".as_ptr());
}
}Note que a chamada a uma função externa é insegura (unsafe), pois o compilador não pode garantir que a função seja segura. Isso é parte do contrato de segurança da FFI: o programador é responsável por garantir que os argumentos são válidos e que a função não cause comportamento indefinido.
Além de funções, você também pode declarar variáveis estáticas externas usando extern "C", mas isso é menos comum.
#[no_mangle]
Quando você quer exportar uma função Rust para que ela seja chamada a partir de C, precisa usar o atributo #[no_mangle]. Esse atributo instrui o compilador a não alterar o nome da função (evitar o name mangling), garantindo que o nome final no binário seja exatamente o que você escreveu. Isso é essencial porque o linker C espera nomes de símbolos sem decoração.
Além disso, a função deve ser marcada como pub extern "C" para que tenha a convenção de chamada correta e esteja visível externamente. Veja um exemplo:
#[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), você pode linká-la a partir de C e chamar a função add normalmente. O nome do símbolo será exatamente add.
É importante lembrar que, para que a função seja usada por C, ela deve ser segura (não pode ser unsafe), pois o C não tem o conceito de unsafe. Isso significa que a função deve garantir que seus argumentos são válidos e que não cause comportamento indefinido.
bindgen (visão geral)
Gerar declarações FFI manualmente pode ser trabalhoso e propenso a erros, especialmente para bibliotecas C grandes. O bindgen é uma ferramenta que automatiza esse processo: ela analisa arquivos de cabeçalho C (headers) e gera automaticamente módulos Rust com as declarações extern e tipos correspondentes.
O bindgen pode ser usado como uma ferramenta de linha de comando ou como uma crate (biblioteca) dentro de um build.rs. O uso típico envolve especificar o arquivo de cabeçalho e as opções de geração. Por exemplo, para gerar bindings para uma biblioteca simples, você pode rodar:
bindgen input.h -o bindings.rsO arquivo gerado contém as declarações extern e estruturas de dados com a representação correta. Para integrar com Cargo, você adiciona um script de build que chama o bindgen e inclui o resultado no crate. Aqui está um exemplo de 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!");
}O bindgen também oferece opções para personalizar a geração, como renomear tipos, adicionar anotações de segurança e lidar com macros. É uma ferramenta poderosa que reduz drasticamente o esforço de integrar bibliotecas C.
Cuidados de segurança
A FFI é uma área onde muitos bugs e vulnerabilidades podem surgir, pois você está manipulando memória e chamando código que não é verificado pelo compilador Rust. Aqui estão os principais cuidados:
- Unsafe blocks: Toda chamada a função externa e acesso a dados externos deve estar dentro de
unsafe. Use o mínimo necessário e documente claramente as invariantes. - Ponteiros: Ponteiros podem ser nulos ou inválidos. Sempre verifique se não são nulos antes de usá-los, e garanta que apontem para memória válida e com o alinhamento correto.
- Tipos: Os tipos Rust e C podem ter tamanhos e alinhamentos diferentes. Use os tipos apropriados como
c_int,c_char, etc., disponíveis no módulostd::os::rawou na cratelibc. - Ownership e lifetime: Quando você passa dados para C, é preciso garantir que a memória permaneça válida durante a chamada e que você não libere memória que o C espera gerenciar.
- Panics: Funções Rust que podem causar panic não devem ser exportadas para C, pois o panic desenrola a pilha e pode causar comportamento indefinido ao atravessar a fronteira FFI. Use
catch_unwindse necessário. - Strings: Strings C são terminadas em nulo e não têm tamanho explícito. Ao converter entre
CStringeString, use os tipos adequados e evite perdas de dados.
Além disso, ao chamar funções C, considere que elas podem não ser thread-safe, a menos que explicitamente documentadas. Sempre leia a documentação da biblioteca C.
Boas práticas e observações finais
Para projetos sérios, é recomendado encapsular toda a FFI em um módulo seguro, expondo apenas funções safe para o resto do código. Isso isola a insegurança e facilita auditorias. Use crates como libc para tipos e constantes padrão, e considere usar std::ffi para manipulação de strings.
Teste extensivamente com validação de argumentos e cenários de erro. Ferramentas como valgrind ou sanitizers podem ajudar a detectar vazamentos de memória e erros de acesso.
Referências
- The Rust Book: Unsafe Rust
- Rust Reference: External blocks
- The Rustonomicon: FFI
- bindgen User Guide
- std::ffi documentation
- libc crate documentation
Exercícios
Declare uma função externa
strlenda libc e use-a para calcular o comprimento de uma string C.✓ Resposta:use std::ffi::CString; extern "C" { fn strlen(s: *const u8) -> usize; } fn main() { let s = CString::new("hello").unwrap(); unsafe { let len = strlen(s.as_ptr()); println!("length = {}", len); } }Crie uma função Rust
doubleque recebe um inteiro e retorna o dobro. Exporte-a com#[no_mangle]eextern "C".✓ Resposta:#[no_mangle] pub extern "C" fn double(x: i32) -> i32 { x * 2 }Usando bindgen, como você geraria bindings para um arquivo de cabeçalho chamado
math.h? Escreva o comando e um exemplo debuild.rsque gera os bindings.✓ Resposta:Comando:
bindgen math.h -o bindings.rsbuild.rs:
use std::env; use std::path::PathBuf; fn main() { let bindings = bindgen::Builder::default() .header("math.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!"); }Explique por que a função exportada para C não deve ser
unsafee o que aconteceria se ela causasse um panic.✓ Resposta:Uma função exportada para C não deve ser
unsafeporque o chamador C não pode fornecer as garantias de segurança exigidas pelo Rust; a função deve ser segura para qualquer entrada válida. Se a função causar um panic, o processo tentará desenrolar a pilha, mas como o chamador é C, não há mecanismo de captura, resultando em comportamento indefinido (geralmente abort). Para evitar isso, usecatch_unwinde retorne um código de erro.Escreva um pequeno trecho de código que converte uma string Rust em uma string C usando
CStringe depois a passa para uma função externa que espera umconst char*.✓ Resposta:use std::ffi::CString; extern "C" { fn puts(s: *const u8) -> i32; } fn main() { let s = CString::new("Hello from Rust").unwrap(); unsafe { puts(s.as_ptr()); } }