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”.
O que o unsafe faz, e o que ele não faz
Seção intitulada “O que o unsafe faz, e o que ele não faz”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:
- Desreferenciar um ponteiro bruto (raw pointer)
- Chamar uma função
unsafe - Ler ou alterar uma variável
staticmutável - Implementar uma trait
unsafe - 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.
Ponteiros brutos
Seção intitulada “Ponteiros brutos”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:
5Você 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.
Chamando uma função unsafe
Seção intitulada “Chamando uma função unsafe”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 delaEmbrulhando o unsafe em uma função segura
Seção intitulada “Embrulhando o unsafe em uma função segura”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:
70Veja 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 blocounsafe, 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
unsafeo 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.
Por que o unsafe existe
Seção intitulada “Por que o unsafe existe”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.
A troca, sendo honesto
Seção intitulada “A troca, sendo honesto”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.
Prática
Seção intitulada “Prática”1. Alterar um valor através de um ponteiro bruto
Seção intitulada “1. Alterar um valor através de um ponteiro bruto”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 = 15Na 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.
2. Um get_or seguro
Seção intitulada “2. Um get_or seguro”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-1O í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.
unsafelibera 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 exigeunsafe. - Embrulhe o
unsafeem uma função pequena e segura que confere as regras primeiro, e explique por que ela é correta em um comentário// SAFETY:. - O
unsafeexiste 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Unsafe Rust — The Rust Book (em inglês): os cinco “superpoderes do unsafe” em detalhe, incluindo como construir um
split_at_mutseguro em cima de ponteiros brutos. - The Rustonomicon (em inglês): o guia oficial para escrever código unsafe corretamente. É avançado, mas o primeiro capítulo é uma ótima leitura.
- The
unsafekeyword — documentação da biblioteca padrão (em inglês): blocos, funções e traits unsafe, além da convenção do comentário// SAFETY:. - Unsafety — The Rust Reference (em inglês): a lista exata das operações que precisam de
unsafe. slice::get_unchecked— documentação da biblioteca padrão (em inglês): uma função unsafe de verdade, com uma seção “Safety” que explica a regra que você precisa seguir.