Memory safety em FFI: validando ponteiros e lifetimes

1. Fundamentos de FFI e os riscos de memory safety

FFI (Foreign Function Interface) é o mecanismo que permite que código Rust interaja com bibliotecas escritas em outras linguagens, principalmente C. Rust se destaca nesse cenário por oferecer garantias de segurança de memória mesmo na fronteira entre linguagens, desde que o programador siga contratos rigorosos.

Os principais riscos ao trabalhar com FFI incluem:
- Ponteiros nulos: desreferenciar um ponteiro que aponta para null
- Dangling pointers: acessar memória já liberada
- Data races: acesso concorrente sem sincronização adequada

Enquanto Rust possui um modelo de ownership que garante segurança em tempo de compilação, C não oferece nenhuma garantia — cabe ao desenvolvedor Rust construir uma camada segura sobre esse código inseguro.

2. Ponteiros crus (*const T e *mut T) e o contrato unsafe

Ponteiros crus são a representação raw de endereços de memória em Rust. Diferentemente de referências (&T e &mut T), eles não possuem garantias de lifetime ou validade.

// Exemplo de declaração de FFI para uma função C
extern "C" {
    fn malloc(size: usize) -> *mut std::ffi::c_void;
    fn free(ptr: *mut std::ffi::c_void);
}

fn alocar_buffer(tamanho: usize) -> *mut u8 {
    unsafe {
        malloc(tamanho) as *mut u8
    }
}

Dentro de blocos unsafe, o programador assume total responsabilidade por:
- Nunca desreferenciar sem verificar nulidade
- Garantir que o lifetime do ponteiro é válido no momento do acesso
- Respeitar regras de aliasing e alinhamento

3. Validando ponteiros recebidos de código externo

A primeira linha de defesa é verificar se o ponteiro não é nulo:

/// Processa dados recebidos de uma biblioteca C
/// 
/// # Safety
/// - `ptr` deve ser não nulo e apontar para memória válida
/// - `len` deve corresponder ao tamanho real do buffer
unsafe fn processar_dados_externos(ptr: *const u8, len: usize) -> Option<&[u8]> {
    if ptr.is_null() {
        return None;
    }

    // Verificação de alinhamento
    if (ptr as usize) % std::mem::align_of::<u8>() != 0 {
        return None;
    }

    // Cria uma fatia segura a partir do ponteiro
    Some(std::slice::from_raw_parts(ptr, len))
}

Para interfaces seguras, NonNull<T> é uma alternativa que garante que o ponteiro nunca será nulo:

use std::ptr::NonNull;

struct BufferSeguro {
    dados: NonNull<u8>,
    tamanho: usize,
}

impl BufferSeguro {
    fn new(ptr: *mut u8, tamanho: usize) -> Option<Self> {
        Some(Self {
            dados: NonNull::new(ptr)?,
            tamanho,
        })
    }
}

4. Lifetimes em FFI: o desafio da fronteira entre linguagens

Quando recebemos ponteiros de código C, o Rust não tem como inferir o lifetime automaticamente. O caso mais comum é usar 'static para dados que são gerenciados externamente e permanecem válidos durante toda a execução:

extern "C" {
    fn obter_string_global() -> *const std::ffi::c_char;
}

fn string_global_segura() -> Result<&'static str, std::str::Utf8Error> {
    unsafe {
        let ptr = obter_string_global();
        if ptr.is_null() {
            return Err(std::str::Utf8Error); // simplificado
        }
        std::ffi::CStr::from_ptr(ptr).to_str()
    }
}

Um erro comum é assumir que um ponteiro retornado por uma função C vive além da chamada da função. Sempre documente claramente as expectativas de lifetime.

5. Estratégias para garantir lifetimes corretos

Uso de PhantomData

PhantomData permite simular ownership de dados externos sem ocupar espaço na struct:

use std::marker::PhantomData;

struct RecursoExterno<T> {
    ptr: *mut T,
    _marcador: PhantomData<T>, // Simula ownership
}

impl<T> Drop for RecursoExterno<T> {
    fn drop(&mut self) {
        unsafe {
            // Libera o recurso quando a struct sai de escopo
            extern "C" { fn liberar_recurso(ptr: *mut std::ffi::c_void); }
            liberar_recurso(self.ptr as *mut std::ffi::c_void);
        }
    }
}

Wrappers com Box::from_raw

Para dados alocados por Rust e passados para C, podemos usar Box::from_raw e Box::into_raw:

fn criar_dado_para_c(valor: i32) -> *mut Dado {
    let dado = Box::new(Dado { campo: valor });
    Box::into_raw(dado) // Transfere ownership para C
}

// Quando C devolve o controle
fn recuperar_dado_de_c(ptr: *mut Dado) -> Box<Dado> {
    unsafe {
        Box::from_raw(ptr) // Retoma ownership
    }
}

6. Ferramentas e boas práticas de validação

Testes com Miri

Miri é um interpretador que detecta undefined behavior em Rust, incluindo em operações FFI:

cargo +nightly miri test

Pré-condições com assert!

/// # Safety
/// - `ptr` deve ser não nulo e ter alinhamento de 4 bytes
/// - `tamanho` deve ser múltiplo de 4
unsafe fn processar_buffer_alinhado(ptr: *const u32, tamanho: usize) -> &[u32] {
    debug_assert!(!ptr.is_null(), "Ponteiro não pode ser nulo");
    debug_assert_eq!(
        ptr as usize % std::mem::align_of::<u32>(),
        0,
        "Ponteiro deve estar alinhado para u32"
    );
    debug_assert_eq!(tamanho % 4, 0, "Tamanho deve ser múltiplo de 4");

    std::slice::from_raw_parts(ptr, tamanho / 4)
}

Documentação de safety invariants

/// Executa uma função callback fornecida por C
///
/// # Safety
///
/// - `callback` deve ser um ponteiro de função válido
/// - `user_data` deve ser não nulo e permanecer válido durante a execução do callback
/// - O callback não deve lançar exceções ou causar panics
pub unsafe fn executar_com_callback(
    callback: Option<extern "C" fn(user_data: *mut std::ffi::c_void)>,
    user_data: *mut std::ffi::c_void,
) -> Result<(), &'static str> {
    let cb = callback.ok_or("Callback nulo")?;
    cb(user_data);
    Ok(())
}

7. Padrões avançados: callbacks e ponteiros opacos

Callbacks seguros com contexto

type CallbackC = extern "C" fn(evento: u32, user_data: *mut std::ffi::c_void);

struct GerenciadorEventos {
    callbacks: Vec<Box<dyn Fn(u32)>>,
}

extern "C" fn callback_tradutor(evento: u32, user_data: *mut std::ffi::c_void) {
    unsafe {
        let gerenciador = &*(user_data as *mut GerenciadorEventos);
        for cb in &gerenciador.callbacks {
            cb(evento);
        }
    }
}

Tipos opacos (Opaque types)

Para expor tipos Rust para C sem vazar detalhes de layout:

// Arquivo de header C equivalente:
// typedef struct RustLista RustLista;

#[repr(C)]
pub struct RustLista {
    dados: Vec<i32>,
}

// Funções exportadas para C
#[no_mangle]
pub extern "C" fn rust_lista_criar() -> *mut RustLista {
    Box::into_raw(Box::new(RustLista { dados: Vec::new() }))
}

#[no_mangle]
pub extern "C" fn rust_lista_adicionar(lista: *mut RustLista, valor: i32) {
    unsafe {
        if let Some(lista) = lista.as_mut() {
            lista.dados.push(valor);
        }
    }
}

8. Conclusão: construindo bindings robustos e auditáveis

Construir bindings FFI seguros em Rust requer disciplina e atenção aos detalhes. Antes de liberar uma interface pública, verifique:

  1. ✅ Todos os ponteiros recebidos são verificados quanto a nulidade
  2. ✅ Lifetimes estão claramente documentados
  3. ✅ Recursos alocados por C são liberados corretamente
  4. ✅ Callbacks incluem contexto (user_data) para evitar closures inseguras
  5. ✅ Testes com Miri não apontam undefined behavior

Comparado a Python (ctypes/cffi) ou Node.js (node-ffi/napi), Rust oferece vantagens significativas: o compilador verifica ownership e lifetimes dentro do código seguro, enquanto blocos unsafe são explicitamente marcados e auditáveis. A combinação de NonNull, PhantomData, Box::from_raw e documentação rigorosa de safety invariants permite construir bindings tão seguros quanto o ecossistema Rust permite.

Referências