Entendendo Unsafe Rust
Unsafe Rust é um subconjunto da linguagem que permite operações que o compilador não consegue verificar automaticamente. Contrariamente ao que muitos acreditam, "unsafe" não significa "sem segurança" — significa que você assume a responsabilidade de garantir a segurança. O compilador confia em você para não introduzir comportamentos indefinidos (undefined behavior).
A razão de existir é prática: algumas operações são necessárias em sistemas de baixo nível, chamadas de funções C, manipulação de ponteiros e otimizações críticas. Rust oferece as ferramentas, mas exige que você entenda as consequências. Isso é um contrato entre você e a linguagem.
Operações Unsafe Principais
Desreferenciar Ponteiros Brutos
Ponteiros brutos (*const T e *mut T) ignoram as regras de borrowing do Rust. Você pode ter múltiplos ponteiros mutáveis apontando para o mesmo local, causando data races se não tiver cuidado. Desreferenciar um ponteiro inválido é undefined behavior.
fn main() {
let x = 5;
let raw_ptr = &x as *const i32;
unsafe {
println!("Valor: {}", *raw_ptr); // Válido, aponta para x
}
}
Este exemplo é seguro porque sabemos que raw_ptr aponta para x durante toda sua vida útil. Porém, nunca dereference um ponteiro nulo ou inválido.
Chamar Funções Unsafe
Funções externas (FFI) e algumas funções Rust marcadas como unsafe precisam estar dentro de blocos unsafe. A função sinaliza: "eu tenho precondições que você deve garantir".
extern "C" {
fn strlen(s: *const u8) -> usize;
}
fn main() {
let c_string = b"hello\0";
unsafe {
let len = strlen(c_string.as_ptr());
println!("Comprimento: {}", len);
}
}
Aqui, strlen requer que o ponteiro seja válido e termine com \0. É sua responsabilidade garantir isso. Se passar um ponteiro inválido, comportamento indefinido ocorrerá.
Mutabilidade Estática e Raw Pointers
Variáveis estáticas mutáveis são inerentemente thread-unsafe. Acessá-las exige unsafe porque múltiplas threads podem modificá-las simultaneamente.
static mut COUNTER: i32 = 0;
fn increment() {
unsafe {
COUNTER += 1;
}
}
fn main() {
increment();
unsafe {
println!("Contador: {}", COUNTER);
}
}
Se você precisar de um contador thread-safe, use std::sync::atomic::AtomicI32 em vez disso — é seguro e não requer unsafe.
Quando Usar (e Não Usar) Unsafe
Casos Legítimos
Use unsafe apenas quando for absolutamente necessário: integração com C, implementar estruturas de dados de baixo nível (alocadores customizados, concorrência primitiva), ou quando a performance crítica é bloqueadora. Mesmo assim, minimize o escopo do bloco unsafe.
pub struct RawBuffer {
ptr: *mut u8,
capacity: usize,
}
impl RawBuffer {
pub fn new(capacity: usize) -> Self {
let ptr = unsafe {
std::alloc::alloc(std::alloc::Layout::from_size_align_unchecked(
capacity, 1
)) as *mut u8
};
RawBuffer { ptr, capacity }
}
pub fn get(&self, index: usize) -> Option<u8> {
if index < self.capacity {
unsafe { Some(*self.ptr.add(index)) }
} else {
None
}
}
}
impl Drop for RawBuffer {
fn drop(&mut self) {
unsafe {
std::alloc::dealloc(
self.ptr,
std::alloc::Layout::from_size_align_unchecked(
self.capacity, 1
)
);
}
}
}
Este exemplo encapsula a complexidade: o API público é totalmente seguro, e unsafe fica contido internamente com verificações adequadas.
Antipadrões Comuns
Não use unsafe para:
- Ignorar o type system: Se o compilador reclama, há uma razão.
- Ganho de performance especulativo: Meça primeiro. unsafe muitas vezes não oferece ganhos reais.
- Contornar regras de borrowing porque "você sabe melhor": Você provavelmente não sabe. As regras existem por razão.
Boas Práticas e Responsabilidade
Quando você escreve unsafe, coloque um comentário detalhado explicando por que é seguro. Isso ajuda revisores de código e você mesmo no futuro.
pub fn safe_slice_from_raw(ptr: *const u8, len: usize) -> Option<&'static [u8]> {
unsafe {
// SEGURANÇA: Requer que ptr aponte para len bytes válidos e inicializados,
// e que permaneçam válidos por 'static. Chamador deve garantir.
if ptr.is_null() {
return None;
}
Some(std::slice::from_raw_parts(ptr, len))
}
}
Docummente as precondições. Use tipos que forçam segurança quando possível: Option<T>, Result<T, E>, tipos phantom para lifetime tracking. Teste com cargo test e ferramentas como Miri para detectar undefined behavior.
cargo +nightly miri test
Miri emula execução Rust e detecta muitos erros de unsafe que passariam despercebidos.
Conclusão
Unsafe Rust não é o vilão — é uma ferramenta específica para casos específicos. Lembre-se de três pontos críticos: (1) Use unsafe apenas quando absolutamente necessário e minimize seu escopo; (2) Documente rigorosamente as precondições e invariantes; (3) Encapsule unsafe em APIs seguras quando possível, deixando complexidade para dentro.
O compilador Rust protege você 99% do tempo. Quando você entra em territorio unsafe, você assume esse 1% restante. Use essa responsabilidade com sabedoria.