Unsafe Rust: Guia Completo e Seguro | Rust Brasil

Guia de unsafe Rust: raw pointers, FFI, inline assembly e boas práticas. Quando e como usar unsafe com segurança.

O Rust é famoso por suas garantias de segurança de memória. Mas existem operações que o compilador não consegue verificar estaticamente — e é aí que entra o unsafe. Longe de ser algo a evitar a todo custo, unsafe é uma ferramenta essencial que precisa ser compreendida em profundidade. Neste artigo, vamos explorar o que unsafe realmente significa, quando é necessário e como usá-lo de forma responsável.

O Que unsafe Realmente Significa

Um equívoco comum: unsafe não desativa todas as verificações do Rust. O borrow checker, o sistema de tipos e os lifetime checks continuam ativos. O unsafe apenas desbloqueia cinco superpoderes específicos:

Superpoderes do unsafe:
┌────────────────────────────────────────────────┐
│ 1. Desreferenciar raw pointers (*const T, *mut T) │
│ 2. Chamar funções/métodos unsafe               │
│ 3. Acessar/modificar variáveis globais mutáveis │
│ 4. Implementar unsafe traits                    │
│ 5. Acessar campos de unions                     │
└────────────────────────────────────────────────┘

Tudo mais — sistema de tipos, ownership, borrowing, lifetime checking — continua funcionando normalmente dentro de unsafe.

Superpoder 1: Raw Pointers

Raw pointers (*const T e *mut T) são como ponteiros de C: sem garantias de segurança.

Criando e Usando Raw Pointers

fn main() {
    let mut valor = 42;

    // Criar raw pointers é safe
    let ptr_imut: *const i32 = &valor;
    let ptr_mut: *mut i32 = &mut valor;

    // Desreferenciar é unsafe
    unsafe {
        println!("Valor via *const: {}", *ptr_imut);
        *ptr_mut = 100;
        println!("Valor modificado: {}", *ptr_mut);
    }

    println!("Valor final: {}", valor);
}

Quando Raw Pointers São Necessários

1. Implementar estruturas de dados com ponteiros:

use std::ptr;

struct Pilha<T> {
    dados: *mut T,
    capacidade: usize,
    tamanho: usize,
}

impl<T> Pilha<T> {
    fn nova(capacidade: usize) -> Self {
        let layout = std::alloc::Layout::array::<T>(capacidade).unwrap();
        let dados = unsafe { std::alloc::alloc(layout) as *mut T };

        if dados.is_null() {
            std::alloc::handle_alloc_error(layout);
        }

        Pilha {
            dados,
            capacidade,
            tamanho: 0,
        }
    }

    fn push(&mut self, valor: T) {
        assert!(self.tamanho < self.capacidade, "Pilha cheia");
        unsafe {
            ptr::write(self.dados.add(self.tamanho), valor);
        }
        self.tamanho += 1;
    }

    fn pop(&mut self) -> Option<T> {
        if self.tamanho == 0 {
            return None;
        }
        self.tamanho -= 1;
        unsafe { Some(ptr::read(self.dados.add(self.tamanho))) }
    }
}

impl<T> Drop for Pilha<T> {
    fn drop(&mut self) {
        // Destruir elementos restantes
        while self.pop().is_some() {}

        let layout = std::alloc::Layout::array::<T>(self.capacidade).unwrap();
        unsafe {
            std::alloc::dealloc(self.dados as *mut u8, layout);
        }
    }
}

fn main() {
    let mut pilha = Pilha::nova(10);
    pilha.push(1);
    pilha.push(2);
    pilha.push(3);

    while let Some(v) = pilha.pop() {
        println!("{}", v); // 3, 2, 1
    }
}

2. Otimizações que o compilador não consegue provar seguras:

fn dividir_no_meio(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) {
    let meio = slice.len() / 2;
    let ptr = slice.as_mut_ptr();

    // O borrow checker não permite dois &mut para o mesmo slice
    // Mas sabemos que as duas metades não se sobrepõem
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, meio),
            std::slice::from_raw_parts_mut(ptr.add(meio), slice.len() - meio),
        )
    }
}

fn main() {
    let mut dados = vec![1, 2, 3, 4, 5, 6];
    let (esquerda, direita) = dividir_no_meio(&mut dados);

    esquerda.iter_mut().for_each(|x| *x *= 10);
    direita.iter_mut().for_each(|x| *x *= 100);

    println!("{:?}", dados); // [10, 20, 30, 400, 500, 600]
}

Nota: na prática, use slice.split_at_mut(meio) da biblioteca padrão, que faz exatamente isso.

Superpoder 2: Funções Unsafe

Funções marcadas como unsafe fn possuem pré-condições que o chamador deve garantir:

/// # Safety
/// `dados` deve apontar para pelo menos `tamanho` elementos válidos de `T`.
/// Os elementos apontados não devem ser acessados por outra referência
/// durante a vida deste slice.
unsafe fn criar_slice<'a, T>(dados: *const T, tamanho: usize) -> &'a [T] {
    std::slice::from_raw_parts(dados, tamanho)
}

fn main() {
    let vetor = vec![10, 20, 30, 40, 50];
    let ptr = vetor.as_ptr();

    // Chamador garante as pré-condições
    let slice = unsafe { criar_slice(ptr, 3) };
    println!("{:?}", slice); // [10, 20, 30]
}

Documentando Pré-condições com # Safety

Toda função unsafe deve ter uma seção # Safety na documentação explicando as pré-condições:

/// Converte bytes em uma string UTF-8 sem verificação.
///
/// # Safety
///
/// Os bytes fornecidos DEVEM ser UTF-8 válido.
/// Caso contrário, o comportamento é indefinido ao usar a String resultante.
unsafe fn string_de_bytes_sem_verificar(bytes: Vec<u8>) -> String {
    String::from_utf8_unchecked(bytes)
}

fn main() {
    let bytes = vec![79, 108, 195, 161, 33]; // "Olá!" em UTF-8
    let texto = unsafe { string_de_bytes_sem_verificar(bytes) };
    println!("{}", texto); // Olá!
}

Superpoder 3: Variáveis Globais Mutáveis

Variáveis static mut são unsafe porque múltiplas threads podem acessá-las simultaneamente:

static mut CONTADOR: u64 = 0;

fn incrementar() {
    unsafe {
        CONTADOR += 1;
    }
}

fn obter_contagem() -> u64 {
    unsafe { CONTADOR }
}

fn main() {
    // Em single-thread, é seguro (mas evite se possível)
    for _ in 0..100 {
        incrementar();
    }
    println!("Contagem: {}", obter_contagem());
}

Na prática, prefira alternativas safe:

use std::sync::atomic::{AtomicU64, Ordering};

static CONTADOR: AtomicU64 = AtomicU64::new(0);

fn incrementar() {
    CONTADOR.fetch_add(1, Ordering::Relaxed);
}

fn obter_contagem() -> u64 {
    CONTADOR.load(Ordering::Relaxed)
}

fn main() {
    for _ in 0..100 {
        incrementar();
    }
    println!("Contagem: {}", obter_contagem());
}

Ou use std::sync::OnceLock para inicialização lazy:

use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn obter_config() -> &'static str {
    CONFIG.get_or_init(|| {
        // Inicialização pesada, executada apenas uma vez
        String::from("modo=produção")
    })
}

fn main() {
    println!("{}", obter_config());
    println!("{}", obter_config()); // usa valor já inicializado
}

Superpoder 4: FFI (Foreign Function Interface)

FFI permite chamar código C (e outras linguagens) a partir do Rust:

// Declarando funções externas de C
extern "C" {
    fn abs(input: i32) -> i32;
    fn sqrt(input: f64) -> f64;
}

fn main() {
    let x = -42;
    let resultado = unsafe { abs(x) };
    println!("abs({}) = {}", x, resultado);

    let y = 144.0;
    let raiz = unsafe { sqrt(y) };
    println!("sqrt({}) = {}", y, raiz);
}

Expondo Rust para C

/// Função Rust que pode ser chamada por C
#[no_mangle]
pub extern "C" fn somar(a: i32, b: i32) -> i32 {
    a + b
}

/// Processando strings de C
use std::ffi::{CStr, CString};
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn cumprimentar(nome: *const c_char) -> *mut c_char {
    let nome_c = unsafe {
        assert!(!nome.is_null());
        CStr::from_ptr(nome)
    };

    let nome_rust = nome_c.to_str().unwrap_or("desconhecido");
    let saudacao = format!("Olá, {}!", nome_rust);

    CString::new(saudacao).unwrap().into_raw()
}

/// Liberar string alocada por Rust
#[no_mangle]
pub extern "C" fn liberar_string(s: *mut c_char) {
    if !s.is_null() {
        unsafe {
            drop(CString::from_raw(s));
        }
    }
}

fn main() {
    // Demonstração sem FFI real
    println!("somar(3, 4) = {}", somar(3, 4));
}

Layout de Memória para FFI

/// Struct compatível com C
#[repr(C)]
struct Ponto {
    x: f64,
    y: f64,
}

/// Enum compatível com C
#[repr(C)]
enum Cor {
    Vermelho = 0,
    Verde = 1,
    Azul = 2,
}

/// Union compatível com C
#[repr(C)]
union Numero {
    inteiro: i64,
    decimal: f64,
}

fn main() {
    let p = Ponto { x: 1.0, y: 2.0 };
    println!("Tamanho do Ponto: {} bytes", std::mem::size_of::<Ponto>());
    println!("Ponto({}, {})", p.x, p.y);

    let n = Numero { inteiro: 42 };
    // Acessar union é unsafe porque o compilador não sabe qual campo está ativo
    unsafe {
        println!("Inteiro: {}", n.inteiro);
    }
}

Superpoder 5: Unsafe Traits

Traits marcadas como unsafe trait possuem invariantes que o implementador deve garantir:

/// # Safety
/// O tipo deve garantir que todos os seus bytes são válidos
/// e que pode ser construído a partir de qualquer padrão de bits.
unsafe trait PodType: Copy + 'static {
    fn from_bytes(bytes: &[u8]) -> Option<Self> {
        if bytes.len() == std::mem::size_of::<Self>() {
            let mut valor = std::mem::MaybeUninit::<Self>::uninit();
            unsafe {
                std::ptr::copy_nonoverlapping(
                    bytes.as_ptr(),
                    valor.as_mut_ptr() as *mut u8,
                    bytes.len(),
                );
                Some(valor.assume_init())
            }
        } else {
            None
        }
    }
}

// Implementar unsafe trait requer unsafe impl
unsafe impl PodType for u32 {}
unsafe impl PodType for f32 {}
unsafe impl PodType for [u8; 4] {}

fn main() {
    let bytes = [0x42, 0x28, 0x00, 0x00]; // 10306 em little-endian

    if let Some(valor) = u32::from_bytes(&bytes) {
        println!("u32: {}", valor);
    }

    if let Some(valor) = f32::from_bytes(&bytes) {
        println!("f32: {}", valor);
    }
}

Os exemplos mais conhecidos de unsafe traits na biblioteca padrão são Send e Sync.

transmute: O Último Recurso

std::mem::transmute reinterpreta os bits de um valor como outro tipo. É extremamente perigoso:

fn main() {
    // Exemplo legítimo: converter entre tipos com mesmo layout
    let valor: u32 = 0x41424344;
    let bytes: [u8; 4] = unsafe { std::mem::transmute(valor) };
    println!("Bytes: {:?}", bytes);

    // Exemplo legítimo: verificar representação de float
    let pi: f64 = std::f64::consts::PI;
    let bits: u64 = unsafe { std::mem::transmute(pi) };
    println!("Bits de PI: {:#018x}", bits);

    // PREFIRA alternativas safe quando existirem:
    let valor2: u32 = 0x41424344;
    let bytes2 = valor2.to_ne_bytes(); // safe!
    println!("Bytes (safe): {:?}", bytes2);

    let bits2 = pi.to_bits(); // safe!
    println!("Bits de PI (safe): {:#018x}", bits2);
}

Alternativas a transmute

┌──────────────────────────────────┬─────────────────────────────┐
│ Em vez de transmute para...      │ Use...                      │
├──────────────────────────────────┼─────────────────────────────┤
│ u32 → [u8; 4]                   │ u32::to_ne_bytes()          │
│ f64 → u64                       │ f64::to_bits()              │
│ &T → &U (mesmo layout)          │ &*(ptr as *const U)         │
│ Vec<u8> → String                │ String::from_utf8()         │
│ Enum → inteiro                  │ enum as u32                 │
│ Ponteiro → usize                │ ptr as usize                │
└──────────────────────────────────┴─────────────────────────────┘

Minimizando o Escopo de unsafe

A regra de ouro: mantenha blocos unsafe o menor possível e encapsule-os em abstrações safe.

/// Módulo que encapsula unsafe em uma API safe
mod buffer_circular {
    pub struct BufferCircular<T> {
        dados: Box<[Option<T>]>,
        leitura: usize,
        escrita: usize,
        tamanho: usize,
    }

    impl<T> BufferCircular<T> {
        pub fn novo(capacidade: usize) -> Self {
            let dados: Vec<Option<T>> = (0..capacidade).map(|_| None).collect();
            BufferCircular {
                dados: dados.into_boxed_slice(),
                leitura: 0,
                escrita: 0,
                tamanho: 0,
            }
        }

        pub fn escrever(&mut self, valor: T) -> Result<(), T> {
            if self.tamanho == self.dados.len() {
                return Err(valor); // buffer cheio
            }
            self.dados[self.escrita] = Some(valor);
            self.escrita = (self.escrita + 1) % self.dados.len();
            self.tamanho += 1;
            Ok(())
        }

        pub fn ler(&mut self) -> Option<T> {
            if self.tamanho == 0 {
                return None;
            }
            let valor = self.dados[self.leitura].take();
            self.leitura = (self.leitura + 1) % self.dados.len();
            self.tamanho -= 1;
            valor
        }

        pub fn tamanho(&self) -> usize {
            self.tamanho
        }
    }
}

fn main() {
    use buffer_circular::BufferCircular;

    let mut buf = BufferCircular::novo(3);
    buf.escrever(1).unwrap();
    buf.escrever(2).unwrap();
    buf.escrever(3).unwrap();
    assert!(buf.escrever(4).is_err()); // cheio

    println!("{:?}", buf.ler()); // Some(1)
    println!("{:?}", buf.ler()); // Some(2)

    buf.escrever(4).unwrap(); // agora tem espaço
    println!("{:?}", buf.ler()); // Some(3)
    println!("{:?}", buf.ler()); // Some(4)
}

Note que este exemplo não usa unsafe — a implementação com Option<T> é totalmente safe. Em código real, só use unsafe quando realmente precisar de performance que abstrações safe não oferecem.

Checklist de Revisão para Código Unsafe

Antes de escrever unsafe, verifique:

  1. Existe uma alternativa safe? Verifique a biblioteca padrão e crates populares
  2. As pré-condições estão documentadas? Seção # Safety obrigatória
  3. O escopo é mínimo? Apenas o código que realmente precisa de unsafe
  4. Há testes? Incluindo testes com Miri (cargo +nightly miri test)
  5. O código é revisado? Outro desenvolvedor verificou as invariantes?
# Instale e use Miri para detectar UB (Undefined Behavior)
rustup +nightly component add miri
cargo +nightly miri test

Erros Comuns com Unsafe

1. Criar referência a dados desalinhados

#[repr(packed)]
struct Empacotado {
    a: u8,
    b: u32, // pode estar desalinhado!
}

fn main() {
    let e = Empacotado { a: 1, b: 42 };

    // ERRADO: &e.b pode ser desalinhado → UB
    // let referencia = &e.b;

    // CORRETO: copie o valor
    let valor = { e.b }; // cópia, não referência
    println!("b = {}", valor);

    // Ou use read_unaligned
    let valor2 = unsafe {
        std::ptr::addr_of!(e.b).read_unaligned()
    };
    println!("b = {}", valor2);
}

2. Esquecer de liberar memória alocada manualmente

Sempre implemente Drop para tipos que alocam memória com alloc.

3. Violar invariantes de tipos

fn main() {
    // NUNCA faça isso: String exige UTF-8 válido
    // let s = unsafe { String::from_utf8_unchecked(vec![0xFF, 0xFE]) };

    // CORRETO: use a versão que verifica
    match String::from_utf8(vec![0xFF, 0xFE]) {
        Ok(s) => println!("{}", s),
        Err(e) => println!("Bytes inválidos: {}", e),
    }
}

Veja Também