Interoperabilidade com C: cbindgen e extern blocks

1. Fundamentos da Interoperabilidade com C em Rust

A interoperabilidade entre Rust e C é possível graças à compatibilidade na ABI (Application Binary Interface). Rust pode se comunicar diretamente com código C utilizando o bloco extern "C", que define a convenção de chamada e a representação binária esperada.

O módulo std::os::raw fornece tipos primitivos compatíveis:

use std::os::raw::{c_int, c_char, c_void, c_double};

// Equivalências comuns:
// c_char  -> i8 (ou u8 dependendo da plataforma)
// c_int   -> i32
// c_void  -> () em Rust, mas representado como *mut c_void
// c_double -> f64

O bloco extern "C" permite declarar funções e variáveis definidas em código C, tornando-as acessíveis no contexto Rust.

2. Declarando e Chamando Funções C a partir de Rust

Para chamar funções de bibliotecas C, usamos extern blocks dentro de blocos unsafe, pois o compilador não pode garantir a segurança das chamadas FFI (Foreign Function Interface).

use std::ffi::CString;
use std::os::raw::c_char;

extern "C" {
    fn printf(format: *const c_char, ...) -> i32;
    fn malloc(size: usize) -> *mut c_void;
    fn free(ptr: *mut c_void);
}

fn main() {
    unsafe {
        let msg = CString::new("Hello from Rust via C printf!\n").unwrap();
        printf(msg.as_ptr());

        let ptr = malloc(64);
        if !ptr.is_null() {
            // Utiliza a memória alocada...
            free(ptr);
        }
    }
}

Note que toda chamada a funções declaradas em extern "C" deve estar em contexto unsafe. O programador é responsável por garantir que os argumentos estejam corretos e que a memória seja gerenciada adequadamente.

3. Exportando Funções Rust para C

Para expor funções Rust para código C, utilizamos #[no_mangle] (desabilita o name mangling) e extern "C":

// lib.rs - Biblioteca Rust que será consumida por C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_greet(name: *const c_char) -> *mut c_char {
    let c_str = unsafe { CStr::from_ptr(name) };
    let rust_str = c_str.to_str().unwrap_or("unknown");
    let greeting = format!("Hello, {}!", rust_str);
    CString::new(greeting).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe { let _ = CString::from_raw(ptr); }
    }
}

Para compilar como biblioteca dinâmica:

cargo build --release
# Gera: target/release/libminha_biblioteca.so (Linux) ou .dll (Windows) ou .dylib (macOS)

O código C correspondente:

// main.c
#include <stdio.h>
#include <stdlib.h>

extern int rust_add(int a, int b);
extern char* rust_greet(const char* name);
extern void rust_free_string(char* ptr);

int main() {
    printf("Soma: %d\n", rust_add(10, 20));
    char* msg = rust_greet("Mundo");
    printf("%s\n", msg);
    rust_free_string(msg);
    return 0;
}

4. Gerenciamento de Memória e Ponteiros

O gerenciamento de memória no FFI requer cuidados especiais. Rust usa Box::into_raw e Box::from_raw para transferir ownership através da fronteira C:

#[repr(C)]
pub struct Point {
    x: f64,
    y: f64,
}

#[no_mangle]
pub extern "C" fn point_new(x: f64, y: f64) -> *mut Point {
    Box::into_raw(Box::new(Point { x, y }))
}

#[no_mangle]
pub extern "C" fn point_get_x(p: *const Point) -> f64 {
    unsafe { (*p).x }
}

#[no_mangle]
pub extern "C" fn point_free(p: *mut Point) {
    if !p.is_null() {
        unsafe { let _ = Box::from_raw(p); }
    }
}

A regra fundamental é: quem aloca, libera. Se Rust aloca com Box::into_raw, Rust deve liberar com Box::from_raw. Se C aloca com malloc, C deve liberar com free.

5. cbindgen: Gerando Headers C Automaticamente

cbindgen é uma ferramenta que gera arquivos .h automaticamente a partir de código Rust anotado. Instalação:

cargo install cbindgen

Arquivo cbindgen.toml:

language = "C"
include_guard = "MINHA_BIB_H"
autogen_warning = "/* Gerado automaticamente por cbindgen */"

Exemplo de código Rust com anotações:

/// Soma dois números inteiros.
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Estrutura representando um ponto 2D.
#[repr(C)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

Geração do header:

cbindgen --config cbindgen.toml --crate minha_biblioteca --output include/minha_biblioteca.h

Resultado (include/minha_biblioteca.h):

/* Gerado automaticamente por cbindgen */

#include <stdint.h>
#include <stdlib.h>

typedef struct Point {
    double x;
    double y;
} Point;

int32_t add(int32_t a, int32_t b);

6. Trabalhando com Structs, Enums e Callbacks

Structs Rust precisam de #[repr(C)] para garantir layout compatível com C:

#[repr(C)]
pub struct Config {
    pub buffer_size: usize,
    pub timeout_ms: u32,
    pub flags: u8,
}

Enums com representação C:

#[repr(C)]
pub enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

#[repr(u8)]
pub enum Status {
    Ok = 0,
    Error = 1,
    Timeout = 2,
}

Callbacks — passando funções Rust como ponteiros de função C:

type Callback = extern "C" fn(i32, *mut c_void);

#[no_mangle]
pub extern "C" fn register_callback(cb: Option<Callback>, user_data: *mut c_void) {
    if let Some(callback) = cb {
        callback(42, user_data);
    }
}

// Uso no lado C:
// void my_handler(int value, void* data) { ... }
// register_callback(my_handler, NULL);

7. Segurança e Boas Práticas em FFI

Sempre valide ponteiros recebidos do lado C:

#[no_mangle]
pub extern "C" fn process_data(data: *const u8, len: usize) -> i32 {
    if data.is_null() {
        return -1; // Código de erro
    }

    let slice = unsafe { std::slice::from_raw_parts(data, len) };
    // Processa os dados...
    0 // Sucesso
}

Padrão de design recomendado: separe a lógica interna segura da API C exposta:

// Interno (seguro)
mod internal {
    pub struct Engine {
        data: Vec<u8>,
    }

    impl Engine {
        pub fn new() -> Self {
            Engine { data: Vec::new() }
        }

        pub fn process(&mut self, input: &[u8]) -> Vec<u8> {
            // Lógica segura
            input.to_vec()
        }
    }
}

// API C (exposta, com validações)
#[no_mangle]
pub extern "C" fn engine_new() -> *mut c_void {
    Box::into_raw(Box::new(internal::Engine::new())) as *mut c_void
}

#[no_mangle]
pub extern "C" fn engine_process(engine: *mut c_void, input: *const u8, len: usize) -> *mut u8 {
    if engine.is_null() || input.is_null() {
        return std::ptr::null_mut();
    }

    let eng = unsafe { &mut *(engine as *mut internal::Engine) };
    let input_slice = unsafe { std::slice::from_raw_parts(input, len) };
    let result = eng.process(input_slice);

    // Retorna o resultado como buffer alocado por Rust
    let mut boxed = result.into_boxed_slice();
    let ptr = boxed.as_mut_ptr();
    std::mem::forget(boxed); // Evita desalocação prematura
    ptr
}

Testes de integração com código C real:

// tests/integration_test.rs
use std::process::Command;

#[test]
fn test_c_integration() {
    // Compila o código C de teste
    let output = Command::new("gcc")
        .args(&[
            "-o", "test_c_binding",
            "tests/test_main.c",
            "-L.", "-lminha_biblioteca",
        ])
        .output()
        .expect("Falha ao compilar código C");

    assert!(output.status.success());

    // Executa o binário gerado
    let run = Command::new("./test_c_binding")
        .output()
        .expect("Falha ao executar teste C");

    assert!(run.status.success());
}

Referências