FFI: chamando código C a partir do Rust

1. Introdução ao FFI e extern "C"

FFI (Foreign Function Interface) é o mecanismo que permite ao Rust interagir com código escrito em outras linguagens, principalmente C. Isso é essencial para reutilizar bibliotecas legadas, acessar APIs do sistema operacional ou otimizar partes críticas com código C já testado.

O bloco extern "C" declara funções e variáveis externas seguindo a convenção de chamada C. Os tipos primitivos têm correspondência direta: i32int, f64double, *const c_voidvoid*. Ponteiros brutos (*const T, *mut T) representam ponteiros C sem garantias de segurança.

extern "C" {
    fn printf(format: *const u8, ...) -> i32;
    static errno: i32;
}

2. Configuração do projeto e linking com bibliotecas C

Para compilar código C junto com Rust, usamos o crate cc no build.rs. Isso gera uma biblioteca estática que o Rust pode linkar.

// build.rs
fn main() {
    cc::Build::new()
        .file("src/math_ops.c")
        .compile("math_ops");
}

Para bibliotecas do sistema, usamos #[link(name = "...")]:

#[link(name = "m")] // libm (math library)
extern "C" {
    fn sqrt(x: f64) -> f64;
}

Bibliotecas dinâmicas (.so/.dylib) podem ser carregadas em tempo de execução com libloading ou linkadas estaticamente com #[link(name = "...", kind = "static")].

3. Tipos de dados complexos: structs e enums

Structs C exigem #[repr(C)] para garantir o mesmo layout de memória. O alinhamento e padding devem ser idênticos.

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

extern "C" {
    fn distance(p1: *const Point, p2: *const Point) -> f64;
}

Enums C sem tag (apenas inteiros) mapeiam para enums Rust com #[repr(C)]:

#[repr(C)]
enum Status {
    Ok = 0,
    Error = -1,
    Timeout = 1,
}

Unions C são representadas por union em Rust, mas o acesso é inseguro:

#[repr(C)]
union Data {
    int_val: i32,
    float_val: f32,
}

4. Ponteiros, strings e arrays

Ponteiros brutos são convertidos com as cast. Strings C usam CStr (leitura) e CString (criação).

use std::ffi::{CStr, CString};

extern "C" {
    fn strlen(s: *const u8) -> usize;
    fn greet(name: *const u8) -> *mut u8;
}

fn safe_strlen(s: &str) -> usize {
    let c_str = CString::new(s).expect("CString::new failed");
    unsafe { strlen(c_str.as_ptr()) }
}

fn safe_greet(name: &str) -> String {
    let c_name = CString::new(name).unwrap();
    let result_ptr = unsafe { greet(c_name.as_ptr()) };
    let result = unsafe { CStr::from_ptr(result_ptr) };
    result.to_str().unwrap().to_owned()
}

Arrays C são passados como ponteiros + tamanho explícito. Convertemos para slices Rust com std::slice::from_raw_parts:

extern "C" {
    fn process_buffer(buf: *const i32, len: usize);
}

fn safe_process(slice: &[i32]) {
    unsafe {
        process_buffer(slice.as_ptr(), slice.len());
    }
}

5. Callbacks: passando funções Rust para C

Callbacks são declaradas como extern "C" fn com tipos de função. Para passar contexto, usamos *mut c_void apontando para um Box.

use std::os::raw::c_void;

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

extern "C" {
    fn register_callback(cb: Option<Callback>, user_data: *mut c_void);
    fn trigger_callbacks();
}

extern "C" fn my_callback(data: *mut c_void, value: i32) {
    let state = unsafe { &mut *(data as *mut i32) };
    *state += value;
}

fn main() {
    let mut counter = 42i32;
    let counter_ptr = &mut counter as *mut i32 as *mut c_void;

    unsafe {
        register_callback(Some(my_callback), counter_ptr);
        trigger_callbacks();
    }

    println!("Counter: {}", counter); // 42 + valor do callback
}

Evite closures que capturam variáveis não estáticas, pois isso exigiria closures FnOnce e gerenciamento complexo de lifetime.

6. Tratamento de erros e valores de retorno

C frequentemente retorna int indicando sucesso (0) ou erro (-1). Convertemos para Result<T, E>.

use std::io::{Error, ErrorKind};

extern "C" {
    fn open_file(path: *const u8) -> i32;
}

fn safe_open(path: &str) -> Result<i32, Error> {
    let c_path = CString::new(path).map_err(|_| Error::new(ErrorKind::InvalidInput, "nul byte in path"))?;
    let fd = unsafe { open_file(c_path.as_ptr()) };
    if fd < 0 {
        Err(Error::last_os_error()) // captura errno
    } else {
        Ok(fd)
    }
}

Sempre valide ponteiros nulos antes de dereferenciar:

fn safe_process_ptr(ptr: *const i32) -> Option<i32> {
    if ptr.is_null() {
        return None;
    }
    Some(unsafe { *ptr })
}

7. Segurança e boas práticas no FFI

O unsafe é necessário para chamar funções externas, mas deve ser isolado em wrappers seguros. Nunca exponha unsafe na API pública.

// Wrapper seguro
pub struct MathLib;

impl MathLib {
    pub fn sqrt_safe(x: f64) -> f64 {
        unsafe { sqrt(x) }
    }
}

Regras de gerenciamento de memória:
- Se C aloca com malloc, C deve liberar com free
- Se Rust aloca, Rust deve liberar (ou passar ownership para C via Box::into_raw)
- Use extern "C" fn com Box para callbacks com lifetime controlado

Ferramentas de verificação:
- cargo test com unsafe blocks testados
- Valgrind para detectar vazamentos de memória
- Sanitizers (-Z sanitizer=address) em builds de debug

// Exemplo completo: biblioteca C simples
// math_ops.c:
// double multiply(double a, double b) { return a * b; }

// Rust wrapper
mod ffi {
    extern "C" {
        pub fn multiply(a: f64, b: f64) -> f64;
    }
}

pub fn multiply_safe(a: f64, b: f64) -> f64 {
    unsafe { ffi::multiply(a, b) }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_multiply() {
        assert_eq!(multiply_safe(2.0, 3.0), 6.0);
    }
}

Referências