Unsafe Rust: quando e como usar
1. Introdução ao Unsafe Rust
1.1 O que é unsafe e por que existe no Rust
Rust é conhecido por suas garantias de segurança de memória em tempo de compilação. No entanto, existem cenários onde o compilador não pode verificar automaticamente se o código é seguro. Para esses casos, Rust oferece o bloco unsafe, que permite ao programador assumir a responsabilidade por garantir a segurança.
O unsafe não desliga o borrow checker ou outras verificações de segurança — ele simplesmente concede acesso a operações que o compilador não pode verificar.
1.2 Diferença fundamental entre código seguro e inseguro
No código seguro do Rust, o compilador garante:
- Sem ponteiros nulos
- Sem data races
- Sem uso após liberação (use-after-free)
- Sem violações de aliasing
No código unsafe, o programador deve garantir manualmente essas invariantes.
1.3 A promessa do unsafe: responsabilidade do programador
Usar unsafe significa: "Eu, programador, prometo ao compilador que este código é seguro, mesmo que você não consiga provar isso."
2. As Superpotências do Unsafe: O que ele Permite Fazer
2.1 Desreferenciar ponteiros brutos (*const T e *mut T)
let x = 42;
let raw_ptr = &x as *const i32;
unsafe {
println!("Valor através do ponteiro bruto: {}", *raw_ptr);
}
Ponteiros brutos podem ser nulos, dangling ou apontar para memória inválida — o compilador não verifica nada disso.
2.2 Chamar funções externas via FFI
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Valor absoluto de -3: {}", abs(-3));
}
}
2.3 Acessar e modificar variáveis estáticas mutáveis
static mut CONTADOR: u32 = 0;
fn incrementar() {
unsafe {
CONTADOR += 1;
}
}
2.4 Implementar traits inseguros
struct MeuTipo(*const u8);
unsafe impl Send for MeuTipo {}
unsafe impl Sync for MeuTipo {}
3. Quando Usar Unsafe: Cenários Legítimos
3.1 Otimizações de desempenho críticas
fn soma_segura(v: &[i32]) -> i32 {
v.iter().sum()
}
fn soma_otimizada(v: &[i32]) -> i32 {
let mut soma = 0;
let ptr = v.as_ptr();
let len = v.len();
unsafe {
for i in 0..len {
soma += *ptr.add(i); // Evita bounds checking
}
}
soma
}
3.2 Interoperabilidade com código C/C++
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn rust_strlen(s: &str) -> usize {
let c_str = CString::new(s).expect("CString falhou");
unsafe { strlen(c_str.as_ptr()) }
}
3.3 Implementação de estruturas de dados de baixo nível
struct MeuVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> MeuVec<T> {
fn new() -> Self {
MeuVec {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
fn push(&mut self, valor: T) {
if self.len == self.cap {
self.crescer();
}
unsafe {
std::ptr::write(self.ptr.add(self.len), valor);
self.len += 1;
}
}
}
3.4 Manipulação direta de hardware
// Exemplo conceitual para acesso a registradores de hardware
const GPIO_BASE: *mut u32 = 0x3F200000 as *mut u32;
fn escrever_gpio(valor: u32) {
unsafe {
std::ptr::write_volatile(GPIO_BASE, valor);
}
}
4. Regras de Ouro para Usar Unsafe com Segurança
4.1 Nunca violar o borrow checker
// ERRADO: viola regras de aliasing
let mut data = vec![1, 2, 3];
let ptr = &data[0] as *const i32;
data.push(4); // Invalida o ponteiro
unsafe { println!("{}", *ptr); } // UB!
4.2 Garantir que ponteiros sejam válidos e alinhados
fn validar_alinhamento<T>(ptr: *const T) -> bool {
let align = std::mem::align_of::<T>();
(ptr as usize) % align == 0
}
4.3 Evitar data races
use std::sync::atomic::{AtomicU32, Ordering};
static CONTADOR_SEGURO: AtomicU32 = AtomicU32::new(0);
fn incrementar_seguro() {
CONTADOR_SEGURO.fetch_add(1, Ordering::SeqCst);
}
4.4 Documentar pré-condições de segurança
/// # Safety
///
/// - `ptr` deve ser não nulo e alinhado
/// - `ptr` deve apontar para memória inicializada do tipo `T`
/// - O chamador deve garantir que ninguém mais está escrevendo em `ptr`
unsafe fn ler_valor<T>(ptr: *const T) -> T {
std::ptr::read(ptr)
}
5. Padrões e Boas Práticas com Unsafe
5.1 Isolar o código unsafe em funções pequenas
// Bom: função pequena e verificável
unsafe fn inverter_bytes(ptr: *mut u8, len: usize) {
for i in 0..len / 2 {
let a = ptr.add(i);
let b = ptr.add(len - 1 - i);
std::ptr::swap(a, b);
}
}
5.2 Criar APIs seguras por cima de implementações inseguras
struct Buffer {
data: Vec<u8>,
}
impl Buffer {
fn new(tamanho: usize) -> Self {
Buffer { data: vec![0; tamanho] }
}
fn escrever(&mut self, indice: usize, byte: u8) {
// API segura que internamente pode usar unsafe
self.data[indice] = byte;
}
}
5.3 Uso de unsafe dentro de blocos delimitados
// Prefira blocos a funções inteiras unsafe
fn processar_dados(dados: &mut [u8]) {
// Código seguro aqui
let ptr = dados.as_mut_ptr();
let len = dados.len();
unsafe {
// Apenas a parte realmente insegura
for i in 0..len {
*ptr.add(i) = dados[i].wrapping_add(1);
}
}
// Mais código seguro aqui
}
5.4 Testar exaustivamente com ferramentas
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inverter_bytes() {
let mut dados = vec![1, 2, 3, 4];
unsafe { inverter_bytes(dados.as_mut_ptr(), dados.len()) };
assert_eq!(dados, vec![4, 3, 2, 1]);
}
}
6. Armadilhas Comuns e Erros Fatais
6.1 Undefined behavior
// UB: criar referência a partir de ponteiro nulo
unsafe {
let ptr: *const i32 = std::ptr::null();
let _ref = &*ptr; // UB!
}
6.2 Uso após liberação
let ptr: *const i32;
{
let x = 42;
ptr = &x as *const i32;
}
// ptr agora é dangling!
unsafe { println!("{}", *ptr); } // UB!
6.3 Invariantes de Send/Sync mal implementadas
struct NaoSync(*const u8);
unsafe impl Sync for NaoSync {} // MENTIRA! Isso pode causar data races
6.4 Exemplos de bugs reais
Um bug comum é esquecer de considerar o alinhamento ao fazer casts de ponteiros:
// ERRADO: alinhamento pode estar incorreto
let bytes: [u8; 4] = [1, 0, 0, 0];
let ptr = &bytes as *const u8 as *const u32;
unsafe {
println!("{}", *ptr); // Pode crashar em arquiteturas que exigem alinhamento
}
7. Alternativas ao Unsafe: Quando Não Usar
7.1 Soluções seguras disponíveis
// Em vez de unsafe, use:
use std::cell::RefCell;
use std::sync::Mutex;
// RefCell para mutabilidade interior em single-thread
let celula = RefCell::new(42);
*celula.borrow_mut() += 1;
// Mutex para acesso concorrente seguro
let mutex = Mutex::new(42);
*mutex.lock().unwrap() += 1;
7.2 Uso de Pin, Cell e RefCell
use std::pin::Pin;
// Pin garante que um valor não será movido na memória
struct Imovel {
dados: Vec<u8>,
}
let imovel = Imovel { dados: vec![1, 2, 3] };
let pinado = Pin::new(&imovel);
7.3 Quando o custo supera os benefícios
Antes de usar unsafe, pergunte-se:
- O ganho de performance é mensurável?
- Existe uma crate segura que faz o mesmo?
- Você entende todas as implicações de segurança?
7.4 Ferramentas de profiling
// Use perf, flamegraph, ou o profiler integrado do Rust
// para confirmar que unsafe é realmente necessário
fn main() {
let inicio = std::time::Instant::now();
// Código a ser testado
println!("Tempo: {:?}", inicio.elapsed());
}
8. Conclusão
unsafe é uma ferramenta poderosa, mas perigosa. Use-a como último recurso, isole o código inseguro em pequenas funções bem documentadas, e sempre teste exaustivamente com ferramentas como Miri e sanitizers.
Lembre-se: unsafe não significa "código ruim" — significa "código que requer mais cuidado". A comunidade Rust valoriza a segurança, mas reconhece que para certos cenários (FFI, otimizações críticas, hardware), o unsafe é necessário.
Referências
- The Rustonomicon — Guia oficial sobre Unsafe Rust, cobrindo todos os detalhes de segurança de memória e undefined behavior
- Rust Reference: Unsafe Operations — Documentação oficial da linguagem sobre o keyword
unsafee suas operações permitidas - Unsafe Rust: How and when (not) to use it — Artigo prático de Steve Donovan sobre quando usar unsafe
- Rust FFI Guide — Seção do Rustonomicon sobre Foreign Function Interface e como usar unsafe com código C
- Using Miri to detect undefined behavior — Ferramenta oficial para detectar undefined behavior em código Rust, essencial para testar código unsafe
- Rust Unsafe Code Guidelines — Diretrizes oficiais para escrever código unsafe correto e seguro