Bem-vindo à aula sobre Unsafe Rust. Até agora, você provavelmente apreciou as garantias de segurança de memória que o Rust oferece: o compilador impede acesso inválido à memória, corridas de dados e muitos outros bugs. No entanto, existem situações em que essas garantias são restritivas demais, e é aí que entra o unsafe. Esta aula vai desmistificar o unsafe, mostrando o que ele realmente permite, como usar ponteiros crus, quando é necessário e como manter a segurança mesmo quando você está operando fora da proteção do compilador.

O Rust é uma linguagem que preza pela segurança, mas também reconhece que há casos legítimos em que o programador precisa de mais controle. O unsafe não desliga todas as verificações; ele apenas permite que você faça certas operações que o compilador não pode verificar. A responsabilidade pela segurança passa a ser sua. Vamos entender isso em detalhes.

O que unsafe permite

A palavra-chave unsafe no Rust habilita um conjunto específico de operações que são consideradas "inseguras" porque podem violar as garantias de memória da linguagem. Essas operações são:

  • Desreferenciar ponteiros crus (*const T e *mut T).
  • Chamar funções ou métodos marcados como unsafe (por exemplo, funções FFI).
  • Acessar ou modificar variáveis estáticas mutáveis.
  • Implementar traits inseguros (como Send e Sync de forma manual).
  • Acessar campos de uniões (unions).

É importante notar que unsafe não desliga o borrow checker nem as verificações de tipo. Ele apenas permite que você faça essas operações específicas. O código dentro de um bloco unsafe ainda é verificado quanto a tipos e lifetimes, mas as operações acima são permitidas sem a garantia automática de segurança.

Por exemplo, você pode criar um bloco unsafe e desreferenciar um ponteiro cru, mas o compilador não vai verificar se o ponteiro é válido. Se você errar, terá um comportamento indefinido (undefined behavior).

fn main() {
    let x = 42;
    let ptr = &x as *const i32;

    unsafe {
        // Desreferenciar um ponteiro cru é permitido aqui
        println!("Valor: {}", *ptr);
    }
}

Neste exemplo, criamos um ponteiro cru para x e o desreferenciamos dentro do bloco unsafe. Isso é seguro porque sabemos que x ainda está vivo e o ponteiro é válido. Mas se x saísse de escopo antes, o ponteiro se tornaria inválido e o comportamento seria indefinido.

Ponteiros crus

Ponteiros crus são semelhantes aos ponteiros em C ou C++. Eles podem ser imutáveis (*const T) ou mutáveis (*mut T). Ao contrário das referências (&T e &mut T), ponteiros crus não têm garantias de aliasing ou lifetime. Eles podem ser nulos, podem apontar para memória inválida, e podem ser copiados livremente.

Você pode criar ponteiros crus a partir de referências usando o operador as, ou pode obter o endereço de uma variável diretamente:

let mut x = 10;
let ptr1 = &x as *const i32;
let ptr2 = &mut x as *mut i32;

// Ponteiro nulo
let ptr_nulo: *const i32 = std::ptr::null();

Para desreferenciar um ponteiro cru, você precisa de um bloco unsafe. O compilador não vai verificar se o ponteiro é válido, então você deve garantir que ele aponte para memória válida e que a operação seja segura.

fn main() {
    let mut x = 10;
    let ptr = &mut x as *mut i32;

    unsafe {
        *ptr = 20; // Escreve através do ponteiro
        println!("x agora é {}", *ptr);
    }
}

Ponteiros crus são frequentemente usados em FFI (Foreign Function Interface) para interagir com código C, ou em estruturas de dados de baixo nível onde você precisa de controle fino sobre a memória. Eles também são usados em implementações de tipos como Vec e String internamente.

Uma diferença crucial entre ponteiros crus e referências é que referências têm garantias de não-aliasing: se você tem uma referência mutável, nenhuma outra referência pode existir para o mesmo dado naquele momento. Ponteiros crus não têm essa restrição, o que permite que você faça coisas como criar múltiplos ponteiros mutáveis para o mesmo dado, mas isso é extremamente perigoso e pode levar a corridas de dados.

Quando é necessário

O unsafe é necessário em várias situações:

  • Interoperabilidade com C (FFI): Chamar funções de bibliotecas C, como as da libc, requer unsafe, pois o compilador não pode garantir a segurança dessas funções.
  • Implementação de estruturas de dados de baixo nível: Tipos como Vec, HashMap e Rc usam unsafe internamente para gerenciar memória e otimizar desempenho.
  • Operações que o compilador não pode verificar: Por exemplo, desreferenciar um ponteiro que você sabe que é válido, mas que não pode ser provado pelo compilador.
  • Acessar hardware ou memória mapeada: Em sistemas embarcados ou drivers, você precisa acessar endereços específicos de memória.
  • Implementar abstrações seguras: Você pode usar unsafe internamente para construir uma API segura para os usuários.

Um exemplo clássico de FFI:

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    let x = -5;
    unsafe {
        println!("Valor absoluto de {} é {}", x, abs(x));
    }
}

Aqui, a função abs é definida em C e declarada com extern "C". Chamar essa função é uma operação insegura, pois o compilador não sabe o que ela faz. Você precisa envolvê-la em um bloco unsafe.

Outra situação comum é quando você quer implementar uma estrutura de dados que o compilador não consegue verificar, como uma lista encadeada com ponteiros crus. Nesse caso, você usaria unsafe para manipular os ponteiros, mas exporia uma API segura para o usuário.

Garantindo segurança

O unsafe não é um passe livre para fazer qualquer coisa. Pelo contrário, você deve tomar cuidado extra para garantir que seu código não cause comportamento indefinido. Aqui estão algumas diretrizes:

  • Documente suas premissas: Comente claramente por que você acredita que o código é seguro.
  • Use unsafe em blocos pequenos: Em vez de marcar uma função inteira como unsafe, use blocos unsafe para isolar as operações perigosas.
  • Verifique invariantes: Antes de desreferenciar um ponteiro, certifique-se de que ele não é nulo e aponta para memória válida.
  • Evite aliasing mutável: Não crie dois ponteiros mutáveis para o mesmo dado simultaneamente, a menos que você tenha certeza de que é seguro (por exemplo, em dados separados).
  • Teste exaustivamente: Use ferramentas como cargo test, cargo miri e sanitizadores para detectar problemas.

Um exemplo de como isolar unsafe em uma função segura:

fn safe_increment(ptr: *mut i32) {
    unsafe {
        if !ptr.is_null() {
            *ptr += 1;
        }
    }
}

fn main() {
    let mut x = 10;
    let ptr = &mut x as *mut i32;
    safe_increment(ptr);
    println!("x = {}", x);
}

Aqui, a função safe_increment é segura para chamar de fora, mas internamente usa unsafe para desreferenciar o ponteiro. Ela verifica se o ponteiro não é nulo antes de incrementar, reduzindo o risco.

É importante entender que unsafe não desativa o borrow checker. As regras de empréstimo ainda se aplicam fora dos blocos unsafe. Dentro do bloco, você pode criar referências a partir de ponteiros crus, mas deve garantir que as referências resultantes respeitem as regras de aliasing.

Por exemplo, o seguinte código é inválido mesmo com unsafe:

fn main() {
    let mut x = 10;
    let ptr = &mut x as *mut i32;

    unsafe {
        let r1 = &*ptr; // referência imutável
        let r2 = &mut *ptr; // referência mutável - conflito!
        // Isso é proibido, pois você não pode ter uma referência mutável e imutável ao mesmo tempo.
    }
}

O compilador ainda verifica as regras de empréstimo dentro do bloco unsafe, então você não pode criar referências que violem essas regras.

Boas práticas e observações finais

O unsafe é uma ferramenta poderosa, mas deve ser usada com moderação. A comunidade Rust recomenda usar unsafe apenas quando necessário e sempre encapsulá-lo em abstrações seguras. Isso significa que, se você estiver implementando uma estrutura de dados, a API pública deve ser segura, e o unsafe deve ficar escondido nos detalhes de implementação.

Além disso, sempre que possível, use crates testados e revisados pela comunidade, como std, em vez de reimplementar funcionalidades com unsafe. Se você precisar de unsafe, documente claramente por que é seguro e como as invariantes são mantidas.

Lembre-se: unsafe não significa "código ruim" ou "código sem segurança". Significa que o programador assume a responsabilidade de garantir a segurança. Com cuidado e boas práticas, você pode escrever código unsafe tão seguro quanto o código seguro.

Referências

Exercícios

  1. Crie um programa que declare uma variável let x: i32 = 5;, crie um ponteiro cru imutável para x e, dentro de um bloco unsafe, imprima o valor apontado. Certifique-se de que o código compila e roda.

    ✓ Resposta:
    fn main() {
        let x: i32 = 5;
        let ptr = &x as *const i32;
        unsafe {
            println!("Valor: {}", *ptr);
        }
    }
    
  2. Escreva uma função unsafe fn dangerous() que faça algo simples (por exemplo, imprima "Perigo!"). Depois, chame essa função de dentro de um bloco unsafe no main.

    ✓ Resposta:
    unsafe fn dangerous() {
        println!("Perigo!");
    }
    
    fn main() {
        unsafe {
            dangerous();
        }
    }
    
  3. Crie um ponteiro cru mutável para um vetor de inteiros, modifique o valor na posição 0 e imprima o vetor. Lembre-se de usar unsafe para desreferenciar o ponteiro.

    ✓ Resposta:
    fn main() {
        let mut v = vec![1, 2, 3];
        let ptr = v.as_mut_ptr(); // ponteiro para o primeiro elemento
        unsafe {
            *ptr = 10; // modifica o primeiro elemento
        }
        println!("{:?}", v);
    }
    
  4. Declare uma variável estática mutável chamada COUNTER do tipo u32. Dentro de um bloco unsafe, incremente o valor e imprima. Explique por que isso requer unsafe.

    ✓ Resposta:
    static mut COUNTER: u32 = 0;
    
    fn main() {
        unsafe {
            COUNTER += 1;
            println!("Contador: {}", COUNTER);
        }
    }
    

    Variáveis estáticas mutáveis são globalmente acessíveis e podem causar corridas de dados se múltiplas threads as acessarem sem sincronização. O Rust exige unsafe para acessá-las porque o compilador não pode garantir a segurança.

  5. Implemente uma função segura que recebe um ponteiro cru para um i32, verifica se não é nulo e, se não for, imprime o valor. Use unsafe apenas onde for necessário.

    ✓ Resposta:
    fn print_if_not_null(ptr: *const i32) {
        if !ptr.is_null() {
            unsafe {
                println!("Valor: {}", *ptr);
            }
        }
    }
    
    fn main() {
        let x = 42;
        let ptr = &x as *const i32;
        print_if_not_null(ptr);
        print_if_not_null(std::ptr::null());
    }