O Rust é famoso por suas garantias de segurança de memória. Mas existem operações que o compilador não consegue verificar estaticamente — e é aí que entra o unsafe. Longe de ser algo a evitar a todo custo, unsafe é uma ferramenta essencial que precisa ser compreendida em profundidade. Neste artigo, vamos explorar o que unsafe realmente significa, quando é necessário e como usá-lo de forma responsável.
O Que unsafe Realmente Significa
Um equívoco comum: unsafe não desativa todas as verificações do Rust. O borrow checker, o sistema de tipos e os lifetime checks continuam ativos. O unsafe apenas desbloqueia cinco superpoderes específicos:
Superpoderes do unsafe:
┌────────────────────────────────────────────────┐
│ 1. Desreferenciar raw pointers (*const T, *mut T) │
│ 2. Chamar funções/métodos unsafe │
│ 3. Acessar/modificar variáveis globais mutáveis │
│ 4. Implementar unsafe traits │
│ 5. Acessar campos de unions │
└────────────────────────────────────────────────┘
Tudo mais — sistema de tipos, ownership, borrowing, lifetime checking — continua funcionando normalmente dentro de unsafe.
Superpoder 1: Raw Pointers
Raw pointers (*const T e *mut T) são como ponteiros de C: sem garantias de segurança.
Criando e Usando Raw Pointers
fn main() {
let mut valor = 42;
// Criar raw pointers é safe
let ptr_imut: *const i32 = &valor;
let ptr_mut: *mut i32 = &mut valor;
// Desreferenciar é unsafe
unsafe {
println!("Valor via *const: {}", *ptr_imut);
*ptr_mut = 100;
println!("Valor modificado: {}", *ptr_mut);
}
println!("Valor final: {}", valor);
}
Quando Raw Pointers São Necessários
1. Implementar estruturas de dados com ponteiros:
use std::ptr;
struct Pilha<T> {
dados: *mut T,
capacidade: usize,
tamanho: usize,
}
impl<T> Pilha<T> {
fn nova(capacidade: usize) -> Self {
let layout = std::alloc::Layout::array::<T>(capacidade).unwrap();
let dados = unsafe { std::alloc::alloc(layout) as *mut T };
if dados.is_null() {
std::alloc::handle_alloc_error(layout);
}
Pilha {
dados,
capacidade,
tamanho: 0,
}
}
fn push(&mut self, valor: T) {
assert!(self.tamanho < self.capacidade, "Pilha cheia");
unsafe {
ptr::write(self.dados.add(self.tamanho), valor);
}
self.tamanho += 1;
}
fn pop(&mut self) -> Option<T> {
if self.tamanho == 0 {
return None;
}
self.tamanho -= 1;
unsafe { Some(ptr::read(self.dados.add(self.tamanho))) }
}
}
impl<T> Drop for Pilha<T> {
fn drop(&mut self) {
// Destruir elementos restantes
while self.pop().is_some() {}
let layout = std::alloc::Layout::array::<T>(self.capacidade).unwrap();
unsafe {
std::alloc::dealloc(self.dados as *mut u8, layout);
}
}
}
fn main() {
let mut pilha = Pilha::nova(10);
pilha.push(1);
pilha.push(2);
pilha.push(3);
while let Some(v) = pilha.pop() {
println!("{}", v); // 3, 2, 1
}
}
2. Otimizações que o compilador não consegue provar seguras:
fn dividir_no_meio(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) {
let meio = slice.len() / 2;
let ptr = slice.as_mut_ptr();
// O borrow checker não permite dois &mut para o mesmo slice
// Mas sabemos que as duas metades não se sobrepõem
unsafe {
(
std::slice::from_raw_parts_mut(ptr, meio),
std::slice::from_raw_parts_mut(ptr.add(meio), slice.len() - meio),
)
}
}
fn main() {
let mut dados = vec![1, 2, 3, 4, 5, 6];
let (esquerda, direita) = dividir_no_meio(&mut dados);
esquerda.iter_mut().for_each(|x| *x *= 10);
direita.iter_mut().for_each(|x| *x *= 100);
println!("{:?}", dados); // [10, 20, 30, 400, 500, 600]
}
Nota: na prática, use slice.split_at_mut(meio) da biblioteca padrão, que faz exatamente isso.
Superpoder 2: Funções Unsafe
Funções marcadas como unsafe fn possuem pré-condições que o chamador deve garantir:
/// # Safety
/// `dados` deve apontar para pelo menos `tamanho` elementos válidos de `T`.
/// Os elementos apontados não devem ser acessados por outra referência
/// durante a vida deste slice.
unsafe fn criar_slice<'a, T>(dados: *const T, tamanho: usize) -> &'a [T] {
std::slice::from_raw_parts(dados, tamanho)
}
fn main() {
let vetor = vec![10, 20, 30, 40, 50];
let ptr = vetor.as_ptr();
// Chamador garante as pré-condições
let slice = unsafe { criar_slice(ptr, 3) };
println!("{:?}", slice); // [10, 20, 30]
}
Documentando Pré-condições com # Safety
Toda função unsafe deve ter uma seção # Safety na documentação explicando as pré-condições:
/// Converte bytes em uma string UTF-8 sem verificação.
///
/// # Safety
///
/// Os bytes fornecidos DEVEM ser UTF-8 válido.
/// Caso contrário, o comportamento é indefinido ao usar a String resultante.
unsafe fn string_de_bytes_sem_verificar(bytes: Vec<u8>) -> String {
String::from_utf8_unchecked(bytes)
}
fn main() {
let bytes = vec![79, 108, 195, 161, 33]; // "Olá!" em UTF-8
let texto = unsafe { string_de_bytes_sem_verificar(bytes) };
println!("{}", texto); // Olá!
}
Superpoder 3: Variáveis Globais Mutáveis
Variáveis static mut são unsafe porque múltiplas threads podem acessá-las simultaneamente:
static mut CONTADOR: u64 = 0;
fn incrementar() {
unsafe {
CONTADOR += 1;
}
}
fn obter_contagem() -> u64 {
unsafe { CONTADOR }
}
fn main() {
// Em single-thread, é seguro (mas evite se possível)
for _ in 0..100 {
incrementar();
}
println!("Contagem: {}", obter_contagem());
}
Na prática, prefira alternativas safe:
use std::sync::atomic::{AtomicU64, Ordering};
static CONTADOR: AtomicU64 = AtomicU64::new(0);
fn incrementar() {
CONTADOR.fetch_add(1, Ordering::Relaxed);
}
fn obter_contagem() -> u64 {
CONTADOR.load(Ordering::Relaxed)
}
fn main() {
for _ in 0..100 {
incrementar();
}
println!("Contagem: {}", obter_contagem());
}
Ou use std::sync::OnceLock para inicialização lazy:
use std::sync::OnceLock;
static CONFIG: OnceLock<String> = OnceLock::new();
fn obter_config() -> &'static str {
CONFIG.get_or_init(|| {
// Inicialização pesada, executada apenas uma vez
String::from("modo=produção")
})
}
fn main() {
println!("{}", obter_config());
println!("{}", obter_config()); // usa valor já inicializado
}
Superpoder 4: FFI (Foreign Function Interface)
FFI permite chamar código C (e outras linguagens) a partir do Rust:
// Declarando funções externas de C
extern "C" {
fn abs(input: i32) -> i32;
fn sqrt(input: f64) -> f64;
}
fn main() {
let x = -42;
let resultado = unsafe { abs(x) };
println!("abs({}) = {}", x, resultado);
let y = 144.0;
let raiz = unsafe { sqrt(y) };
println!("sqrt({}) = {}", y, raiz);
}
Expondo Rust para C
/// Função Rust que pode ser chamada por C
#[no_mangle]
pub extern "C" fn somar(a: i32, b: i32) -> i32 {
a + b
}
/// Processando strings de C
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn cumprimentar(nome: *const c_char) -> *mut c_char {
let nome_c = unsafe {
assert!(!nome.is_null());
CStr::from_ptr(nome)
};
let nome_rust = nome_c.to_str().unwrap_or("desconhecido");
let saudacao = format!("Olá, {}!", nome_rust);
CString::new(saudacao).unwrap().into_raw()
}
/// Liberar string alocada por Rust
#[no_mangle]
pub extern "C" fn liberar_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
fn main() {
// Demonstração sem FFI real
println!("somar(3, 4) = {}", somar(3, 4));
}
Layout de Memória para FFI
/// Struct compatível com C
#[repr(C)]
struct Ponto {
x: f64,
y: f64,
}
/// Enum compatível com C
#[repr(C)]
enum Cor {
Vermelho = 0,
Verde = 1,
Azul = 2,
}
/// Union compatível com C
#[repr(C)]
union Numero {
inteiro: i64,
decimal: f64,
}
fn main() {
let p = Ponto { x: 1.0, y: 2.0 };
println!("Tamanho do Ponto: {} bytes", std::mem::size_of::<Ponto>());
println!("Ponto({}, {})", p.x, p.y);
let n = Numero { inteiro: 42 };
// Acessar union é unsafe porque o compilador não sabe qual campo está ativo
unsafe {
println!("Inteiro: {}", n.inteiro);
}
}
Superpoder 5: Unsafe Traits
Traits marcadas como unsafe trait possuem invariantes que o implementador deve garantir:
/// # Safety
/// O tipo deve garantir que todos os seus bytes são válidos
/// e que pode ser construído a partir de qualquer padrão de bits.
unsafe trait PodType: Copy + 'static {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() == std::mem::size_of::<Self>() {
let mut valor = std::mem::MaybeUninit::<Self>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
valor.as_mut_ptr() as *mut u8,
bytes.len(),
);
Some(valor.assume_init())
}
} else {
None
}
}
}
// Implementar unsafe trait requer unsafe impl
unsafe impl PodType for u32 {}
unsafe impl PodType for f32 {}
unsafe impl PodType for [u8; 4] {}
fn main() {
let bytes = [0x42, 0x28, 0x00, 0x00]; // 10306 em little-endian
if let Some(valor) = u32::from_bytes(&bytes) {
println!("u32: {}", valor);
}
if let Some(valor) = f32::from_bytes(&bytes) {
println!("f32: {}", valor);
}
}
Os exemplos mais conhecidos de unsafe traits na biblioteca padrão são Send e Sync.
transmute: O Último Recurso
std::mem::transmute reinterpreta os bits de um valor como outro tipo. É extremamente perigoso:
fn main() {
// Exemplo legítimo: converter entre tipos com mesmo layout
let valor: u32 = 0x41424344;
let bytes: [u8; 4] = unsafe { std::mem::transmute(valor) };
println!("Bytes: {:?}", bytes);
// Exemplo legítimo: verificar representação de float
let pi: f64 = std::f64::consts::PI;
let bits: u64 = unsafe { std::mem::transmute(pi) };
println!("Bits de PI: {:#018x}", bits);
// PREFIRA alternativas safe quando existirem:
let valor2: u32 = 0x41424344;
let bytes2 = valor2.to_ne_bytes(); // safe!
println!("Bytes (safe): {:?}", bytes2);
let bits2 = pi.to_bits(); // safe!
println!("Bits de PI (safe): {:#018x}", bits2);
}
Alternativas a transmute
┌──────────────────────────────────┬─────────────────────────────┐
│ Em vez de transmute para... │ Use... │
├──────────────────────────────────┼─────────────────────────────┤
│ u32 → [u8; 4] │ u32::to_ne_bytes() │
│ f64 → u64 │ f64::to_bits() │
│ &T → &U (mesmo layout) │ &*(ptr as *const U) │
│ Vec<u8> → String │ String::from_utf8() │
│ Enum → inteiro │ enum as u32 │
│ Ponteiro → usize │ ptr as usize │
└──────────────────────────────────┴─────────────────────────────┘
Minimizando o Escopo de unsafe
A regra de ouro: mantenha blocos unsafe o menor possível e encapsule-os em abstrações safe.
/// Módulo que encapsula unsafe em uma API safe
mod buffer_circular {
pub struct BufferCircular<T> {
dados: Box<[Option<T>]>,
leitura: usize,
escrita: usize,
tamanho: usize,
}
impl<T> BufferCircular<T> {
pub fn novo(capacidade: usize) -> Self {
let dados: Vec<Option<T>> = (0..capacidade).map(|_| None).collect();
BufferCircular {
dados: dados.into_boxed_slice(),
leitura: 0,
escrita: 0,
tamanho: 0,
}
}
pub fn escrever(&mut self, valor: T) -> Result<(), T> {
if self.tamanho == self.dados.len() {
return Err(valor); // buffer cheio
}
self.dados[self.escrita] = Some(valor);
self.escrita = (self.escrita + 1) % self.dados.len();
self.tamanho += 1;
Ok(())
}
pub fn ler(&mut self) -> Option<T> {
if self.tamanho == 0 {
return None;
}
let valor = self.dados[self.leitura].take();
self.leitura = (self.leitura + 1) % self.dados.len();
self.tamanho -= 1;
valor
}
pub fn tamanho(&self) -> usize {
self.tamanho
}
}
}
fn main() {
use buffer_circular::BufferCircular;
let mut buf = BufferCircular::novo(3);
buf.escrever(1).unwrap();
buf.escrever(2).unwrap();
buf.escrever(3).unwrap();
assert!(buf.escrever(4).is_err()); // cheio
println!("{:?}", buf.ler()); // Some(1)
println!("{:?}", buf.ler()); // Some(2)
buf.escrever(4).unwrap(); // agora tem espaço
println!("{:?}", buf.ler()); // Some(3)
println!("{:?}", buf.ler()); // Some(4)
}
Note que este exemplo não usa unsafe — a implementação com Option<T> é totalmente safe. Em código real, só use unsafe quando realmente precisar de performance que abstrações safe não oferecem.
Checklist de Revisão para Código Unsafe
Antes de escrever unsafe, verifique:
- Existe uma alternativa safe? Verifique a biblioteca padrão e crates populares
- As pré-condições estão documentadas? Seção
# Safetyobrigatória - O escopo é mínimo? Apenas o código que realmente precisa de
unsafe - Há testes? Incluindo testes com Miri (
cargo +nightly miri test) - O código é revisado? Outro desenvolvedor verificou as invariantes?
# Instale e use Miri para detectar UB (Undefined Behavior)
rustup +nightly component add miri
cargo +nightly miri test
Erros Comuns com Unsafe
1. Criar referência a dados desalinhados
#[repr(packed)]
struct Empacotado {
a: u8,
b: u32, // pode estar desalinhado!
}
fn main() {
let e = Empacotado { a: 1, b: 42 };
// ERRADO: &e.b pode ser desalinhado → UB
// let referencia = &e.b;
// CORRETO: copie o valor
let valor = { e.b }; // cópia, não referência
println!("b = {}", valor);
// Ou use read_unaligned
let valor2 = unsafe {
std::ptr::addr_of!(e.b).read_unaligned()
};
println!("b = {}", valor2);
}
2. Esquecer de liberar memória alocada manualmente
Sempre implemente Drop para tipos que alocam memória com alloc.
3. Violar invariantes de tipos
fn main() {
// NUNCA faça isso: String exige UTF-8 válido
// let s = unsafe { String::from_utf8_unchecked(vec![0xFF, 0xFE]) };
// CORRETO: use a versão que verifica
match String::from_utf8(vec![0xFF, 0xFE]) {
Ok(s) => println!("{}", s),
Err(e) => println!("Bytes inválidos: {}", e),
}
}
Veja Também
- Cheatsheet Rust — referência rápida incluindo unsafe
- Smart Pointers em Rust — abstrações safe sobre ponteiros
- Entendendo Lifetimes em Rust — sistema de segurança que unsafe complementa
- Trait Objects vs Generics — vtable e layout de memória
- Async/Await em Rust — Pin e unsafe em futures