Pular para o conteúdo

Unsafe em Rust

Todas as garantias de que este curso falou (nada de referências pendentes, nada de data races, nada de acesso fora dos limites) são cobradas pelo compilador. unsafe (inseguro) é o jeito de dizer ao compilador: “me deixe fazer uma coisa que você não consegue verificar se é segura; a responsabilidade agora é minha”.

Um equívoco comum é achar que o unsafe desliga totalmente as verificações de segurança do Rust. Não desliga. Dentro de um bloco unsafe, você pode fazer exatamente cinco coisas a mais, que fora dele são proibidas:

  1. Desreferenciar um ponteiro bruto (raw pointer)
  2. Chamar uma função unsafe
  3. Ler ou alterar uma variável static mutável
  4. Implementar uma trait unsafe
  5. Acessar campos de uma union

Todo o resto (as regras de borrowing, a semântica de move, a verificação de tipos) continua valendo por completo dentro do código unsafe. É um bisturi para cinco operações específicas, não um botão de desligar.

As referências normais do Rust (& e &mut) sempre apontam para dados válidos e bem alinhados; isso faz parte do que o compilador garante. Um ponteiro bruto (*const T ou *mut T) abre mão dessa garantia, e é por isso que ler o valor dele exige unsafe:

fn main() {
let value = 5;
let pointer = &value as *const i32;
unsafe {
println!("{}", *pointer);
}
}

Saída:

5
▶ Experimente você mesmo

Você pode criar um ponteiro bruto em código seguro. Só desreferenciar o ponteiro (ler o valor para o qual ele aponta) é que precisa de unsafe, já que o compilador não consegue mais verificar se o ponteiro é válido.

Algumas funções são marcadas como unsafe porque têm requisitos que o compilador não consegue verificar por você. Ao chamá-las, você está prometendo que conferiu esses requisitos por conta própria:

unsafe fn dangerous() {
println!("esta função confia que você conferiu os requisitos dela");
}
fn main() {
unsafe {
dangerous();
}
}

Saída:

esta função confia que você conferiu os requisitos dela
▶ Experimente você mesmo

A biblioteca padrão tem funções unsafe de verdade que você pode chamar. Uma delas é o get_unchecked dos slices. Ele funciona como numbers[i], mas pula a verificação de limites. Se i estiver fora do intervalo, ele não dá panic: lê o que estiver naquela memória, e isso se chama comportamento indefinido (undefined behavior). O seu programa pode travar, imprimir lixo ou parecer funcionar direitinho até o dia em que não funciona mais.

Então a regra para chamá-lo é “só com um índice que você sabe que é válido”. O padrão comum é você mesmo conferir essa regra, dentro de uma função normal e segura:

fn first_or_zero(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
return 0;
}
// SAFETY: o slice não está vazio, então o índice 0 está dentro dos limites.
unsafe { *numbers.get_unchecked(0) }
}
fn main() {
println!("{}", first_or_zero(&[7, 8, 9]));
println!("{}", first_or_zero(&[]));
}

Saída:

7
0
▶ Experimente você mesmo

Veja o que quem chama a função enxerga: fn first_or_zero(numbers: &[i32]) -> i32. Não há unsafe na assinatura, e não existe jeito de chamá-la errado, porque a função confere a regra antes de chegar ao bloco unsafe. O unsafe fica pequeno e escondido lá dentro.

Dois hábitos que desenvolvedores Rust experientes seguem:

  • Escreva um comentário // SAFETY: acima de cada bloco unsafe, dizendo por que as regras são cumpridas. Ele serve para a próxima pessoa que ler o código, e para você mesmo no futuro.
  • Deixe o bloco unsafe o menor possível, para que haja menos código para conferir duas vezes.

Sendo honesto: você não escreveria exatamente esta função em código de verdade. numbers.first() faz o mesmo trabalho de forma segura, e o compilador é muito bom em remover verificações de limites que ele consegue provar que são desnecessárias. Mas o padrão (conferir as regras e depois chamar o unsafe dentro de uma função segura) é exatamente como Vec, String e companhia são construídos. A página de FFI usa o mesmo truque para embrulhar funções em C.

Se o Rust é todo sobre segurança, por que permitir isso? Porque algumas coisas são de fato impossíveis de provar seguras em tempo de compilação, mesmo sendo perfeitamente seguras na prática: falar direto com o hardware, chamar uma biblioteca em C (veja FFI) ou construir certas estruturas de dados de alto desempenho. O unsafe permite que a biblioteca padrão (e, raramente, o seu próprio código) implemente essas peças de baixo nível, enquanto tudo o que é construído em cima delas continua sendo Rust seguro comum, verificado por completo.

Escrever unsafe não significa que o seu código agora tem bugs. Significa que o compilador não consegue mais pegar certos tipos de erro por você naquele bloco específico, e cabe a você manter as mesmas garantias na mão. A maioria dos desenvolvedores Rust experientes escreve muito pouco unsafe diretamente; ele costuma ficar em trechos pequenos e revisados com cuidado, lá no fundo de uma biblioteca, embrulhados em uma API segura para todo mundo usar.

Crie um let mut score = 10;, faça um ponteiro bruto *mut i32 para ele e use o ponteiro para somar 5. Depois imprima score. Dica: crie o ponteiro com &mut score as *mut i32 e escreva *pointer += 5 dentro de um bloco unsafe.

Ver solução
fn main() {
let mut score = 10;
let pointer = &mut score as *mut i32; // criar o ponteiro é seguro
unsafe {
*pointer += 5; // escrever através dele, não
}
println!("pontuação = {score}");
}

Saída:

pontuação = 15
▶ Experimente você mesmo

Na prática, isso é seguro porque score ainda está viva e nada mais a está usando enquanto escrevemos. Cumprir essas promessas passa a ser trabalho seu quando você usa unsafe.

Escreva fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Ela retorna o item na posição index, ou default se o índice estiver fora do intervalo. Use get_unchecked para a leitura e garanta que a função nunca possa ser chamada de um jeito que quebre a regra dele. Dica: compare index < numbers.len() primeiro, e só chame get_unchecked quando isso for verdade.

Ver solução
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 {
if index < numbers.len() {
// SAFETY: acabamos de conferir que o índice está dentro dos limites.
unsafe { *numbers.get_unchecked(index) }
} else {
default
}
}
fn main() {
let numbers = [10, 20, 30];
println!("{}", get_or(&numbers, 1, -1));
println!("{}", get_or(&numbers, 5, -1));
}

Saída:

20
-1
▶ Experimente você mesmo

O índice 5 nunca chega ao bloco unsafe, então ele é tratado com segurança. Para comparar, a versão totalmente segura é numbers.get(index).copied().unwrap_or(default), e é isso que você normalmente escreveria.

  • unsafe libera exatamente cinco operações (principalmente ponteiros brutos e chamadas a funções unsafe); todo o resto continua sendo verificado.
  • Um ponteiro bruto (*const T / *mut T) pode ser criado em código seguro; desreferenciá-lo exige unsafe.
  • Embrulhe o unsafe em uma função pequena e segura que confere as regras primeiro, e explique por que ela é correta em um comentário // SAFETY:.
  • O unsafe existe para que código de baixo nível (acesso ao hardware, integração com C) possa ser construído, enquanto o código que o usa continua seguro.
  • Como iniciante, você raramente vai precisar escrevê-lo: procure primeiro uma abstração segura.

Teste o que aprendeu

1. O que um bloco unsafe realmente libera?

2. Dá para criar um ponteiro bruto em código seguro?

3. Como iniciante, com que frequência você deve esperar escrever unsafe?

4. Escrever código unsafe significa que o seu programa agora tem bugs?

Pontuação: 0 / 4