Unsafe Rust
Esta aula explora o Rust inseguro (unsafe), explicando o que ele permite, como usar ponteiros crus, quando é necessário e como garantir segurança mesmo em código unsafe. Inclui exemplos práticos, boas práticas e exercícios com respostas.
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 Te*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
SendeSyncde 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,HashMapeRcusamunsafeinternamente 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
unsafeinternamente 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
unsafeem blocos pequenos: Em vez de marcar uma função inteira comounsafe, use blocosunsafepara 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 mirie 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
- The Rust Book: Unsafe Rust
- Rust Standard Library: Pointer
- The Rustonomicon
- Rust Reference: Unsafe Keyword
- Rust by Example: Unsafe Operations
- Cargo Reference: Unsafe Code
Exercícios
-
Crie um programa que declare uma variável
let x: i32 = 5;, crie um ponteiro cru imutável paraxe, dentro de um blocounsafe, 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); } } -
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 blocounsafenomain.✓ Resposta:unsafe fn dangerous() { println!("Perigo!"); } fn main() { unsafe { dangerous(); } } -
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
unsafepara 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); } -
Declare uma variável estática mutável chamada
COUNTERdo tipou32. Dentro de um blocounsafe, incremente o valor e imprima. Explique por que isso requerunsafe.✓ 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
unsafepara acessá-las porque o compilador não pode garantir a segurança. -
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. Useunsafeapenas 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()); }