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 (como c_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 unsafe e 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 libc para 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

Exercícios

  1. Declare e chame a função strlen da libc (que retorna o tamanho de uma string C) usando extern "C". Use CString para 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);
        }
    }
  2. Exporte uma função Rust multiply que recebe dois inteiros e retorna o produto, usando #[no_mangle] e extern "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);
  3. Escreva um build.rs que use bindgen para gerar bindings a partir de um cabeçalho wrapper.h que contém uma função int 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));
        }
    }
  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 aqui
  5. Considere 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_unwind e 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
            }
        }
    }