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.rs

O 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ódulo std::os::raw ou na crate libc.
  • 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_unwind se necessário.
  • Strings: Strings C são terminadas em nulo e não têm tamanho explícito. Ao converter entre CString e String, 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

Exercícios

  1. Declare uma função externa strlen da 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);
        }
    }
  2. Crie uma função Rust double que recebe um inteiro e retorna o dobro. Exporte-a com #[no_mangle] e extern "C".

    ✓ Resposta:
    #[no_mangle]
    pub extern "C" fn double(x: i32) -> i32 {
        x * 2
    }
  3. Usando bindgen, como você geraria bindings para um arquivo de cabeçalho chamado math.h? Escreva o comando e um exemplo de build.rs que gera os bindings.

    ✓ Resposta:

    Comando: bindgen math.h -o bindings.rs

    build.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!");
    }
  4. Explique por que a função exportada para C não deve ser unsafe e o que aconteceria se ela causasse um panic.

    ✓ Resposta:

    Uma função exportada para C não deve ser unsafe porque 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, use catch_unwind e retorne um código de erro.

  5. Escreva um pequeno trecho de código que converte uma string Rust em uma string C usando CString e depois a passa para uma função externa que espera um const 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());
        }
    }