Apostila completa de Rust: sistemas, CLI e WebAssembly
Rust dá performance de C++ com uma garantia que nenhuma delas oferece: o borrow checker prova, em tempo de compilação, que seu programa não tem use-after-free, double-free, ponteiro pendente nem data race. A curva é real; a recompensa é código que, quando compila, tende a funcionar. Esta apostila vai de ownership a async, de CLI a WebAssembly, com o olho no que empresas pedem em vagas de sistemas, infra e performance.
Por que Rust e onde ele brilha
Objetivo: entender a proposta única de Rust — segurança de memória em tempo de compilação sem GC — os domínios em que compensa a curva, e quando não é a escolha certa.
1.1 O trilema que Rust quebra
Historicamente você escolhia dois de três: seguro, rápido, sem coletor de lixo. C/C++ são rápidos e sem GC, mas inseguros (a maioria das CVEs críticas em software de sistema é corrupção de memória). Java/Go/C# são seguros e rápidos, mas com GC (pausas, uso de memória, sem controle fino). Rust entrega os três: a segurança é garantida pelo sistema de tipos e pelo borrow checker em tempo de compilação, com zero custo em runtime — não há GC, não há verificação de bounds onde o compilador prova que é seguro.
O lema de Rust (herdado de C++): você não paga em runtime pelo que não usa, e o que usa não poderia ser escrito à mão de forma mais eficiente. Iterators, Option, closures, genéricos — tudo compila para código tão rápido quanto o loop manual equivalente. As garantias de segurança são verificadas em compilação e apagadas do binário.
1.2 As garantias
| Bug de memória | Rust |
|---|---|
| Use-after-free, dangling pointer | Impossível em código seguro — o borrow checker rejeita |
| Double-free | Impossível — cada valor tem um dono, liberado uma vez |
| Buffer overflow | Checagem de bounds (removida onde provada desnecessária); slices carregam tamanho |
| Null pointer dereference | Não há null — ausência é Option<T>, que você é obrigado a tratar |
| Data race | Impossível em código seguro — os traits Send/Sync e o borrow checker garantem "aliasing XOR mutability" |
| Iterator invalidation | Rejeitado — não dá para mutar uma coleção enquanto a itera |
Tudo isso sem runtime pesado. O custo é: você aprende a satisfazer o borrow checker, e alguns padrões (grafos, listas duplamente ligadas, cache mutável compartilhado) exigem ferramentas específicas (Módulo 6) ou unsafe encapsulado.
1.3 Onde Rust brilha
- Ferramentas de linha de comando: binário único, rápido, cross-compile fácil —
ripgrep,fd,bat,uv,ruff,biomesão Rust. - Infraestrutura e sistemas: partes do Linux kernel, Android, Windows; navegadores (Servo, partes do Firefox); bancos (TiKV), proxies, service meshes (Linkerd2-proxy).
- WebAssembly: Rust é a linguagem com melhor tooling para WASM — no browser (jogos, editores, cripto) e server-side/edge (Cloudflare Workers, Fastly, plugins WASM).
- Serviços de latência crítica: onde pausas de GC ou p99 importam (Discord migrou serviços de Go para Rust por causa disso).
- Bibliotecas de performance embutidas em outras linguagens: extensões nativas de Python (via PyO3), Node (napi-rs), Ruby — o padrão "escreva o núcleo quente em Rust".
- Embedded /
no_std: microcontroladores, firmware, sem sistema operacional. - Blockchain / criptografia (correção e performance).
1.4 Quando NÃO Rust
- Prototipagem rápida / scripts — o ciclo "brigar com o compilador" pesa; Python/TS são mais ágeis.
- CRUD de negócio comum sem exigência de performance — o custo de desenvolvimento raramente compensa.
- Time sem tempo para a curva — Rust exige investimento de semanas antes de produtividade.
- Ecossistema imaturo para o seu domínio específico (GUI desktop, alguns SDKs de nuvem ainda são melhores em outras linguagens).
- ML/ciência de dados — é Python (embora Rust apareça na infra de inferência).
Vagas de Rust são menos numerosas que Go/Java mas bem pagas e concentradas em: fintech/cripto, infra cloud-native, browsers, embedded, e times de plataforma de big tech. É a linguagem "mais amada" há vários anos na pesquisa do Stack Overflow. Pergunta de abertura: "Por que Rust em vez de C++ ou de Go?" — a resposta cita segurança de memória verificada + sem GC (vs Go) e sem a classe de bugs de C++, reconhecendo o custo da curva.
✏️ Exercício 1 — Rust ou não?
Para cada projeto: Rust compensa? (a) reescrever um linter de JS que hoje é lento; (b) um MVP de SaaS B2B com prazo de 6 semanas; (c) um plugin de áudio em tempo real (sem pausas de GC aceitas); (d) um script de automação de deploy; (e) um módulo de parsing de alto volume dentro de um serviço Python.
Gabarito: (a) Sim — CLIs de dev tooling são o caso de ouro (ver Biome/Ruff). (b) Não — prazo curto + CRUD; use TS/Python. (c) Sim — sem GC é requisito duro; Rust ou C++. (d) Não — script; use Bash/Python. (e) Sim — o padrão "núcleo quente em Rust via PyO3", mantendo o resto em Python.
Ownership, borrowing e lifetimes
Objetivo: o modelo mental que define Rust — cada valor tem um dono, referências seguem regras rígidas, e o compilador prova que nenhuma referência sobrevive ao dado que aponta.
2.1 Ownership
Três regras:
- Cada valor tem exatamente um dono (uma variável).
- Quando o dono sai de escopo, o valor é liberado (chamada automática de
drop) — determinístico, sem GC. - Só pode haver um dono por vez — atribuir ou passar por valor move a posse.
let s1 = String::from("olá"); let s2 = s1; // MOVE: s1 não é mais válido // println!("{s1}"); // ERRO de compilação: valor movido println!("{s2}"); // ok fn consome(s: String) { /* s é dono; ao fim da função, drop(s) */ } consome(s2); // s2 movido para dentro; não usável depois
- Tipos que implementam
Copy(inteiros,bool,char, tuplas deCopy) são copiados, não movidos — o velho valor continua válido. .clone()faz uma cópia profunda explícita (custa; por isso não é implícito).
2.2 Borrowing (referências)
Para usar um valor sem tomar posse, você o empresta com uma referência:
fn tamanho(s: &String) -> usize { s.len() } // &String: empréstimo imutável let s = String::from("olá"); let n = tamanho(&s); // empresta; s continua dono println!("{s} tem {n}"); // s ainda válido fn maiuscula(s: &mut String) { s.push_str("!"); } // &mut: empréstimo mutável
A qualquer momento, para um dado, você pode ter OU várias referências imutáveis (&T) OU exatamente uma referência mutável (&mut T) — nunca as duas coisas ao mesmo tempo. Isso é o que elimina data races e iterator invalidation por construção: se alguém pode mutar, ninguém mais pode nem ler.
let mut v = vec![1, 2, 3]; let r = &v[0]; // empréstimo imutável ativo // v.push(4); // ERRO: não pode mutar v enquanto r existe println!("{r}"); // r usado aqui v.push(4); // ok: r não é mais usado (non-lexical lifetimes)
2.3 Lifetimes
Um lifetime é o intervalo em que uma referência é válida. O compilador rastreia isso e garante que nenhuma referência sobreviva ao dado. Na maioria dos casos ele infere (elisão); às vezes você anota:
// "a referência retornada vive tanto quanto o menor dos dois inputs" fn maior<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // isto NÃO compila — retornaria referência a um valor que morre: // fn dangle() -> &String { let s = String::from("x"); &s } // s morre ao fim
Lifetimes não mudam quanto tempo o dado vive — só descrevem relações para o compilador verificar. São a parte mais estranha no começo e a que "clica" com prática.
2.4 Stack, heap e String vs &str
String: dona, na heap, cresce;&str: visão emprestada (slice) de texto UTF-8, não possui nada. Aceite&strem parâmetros (mais flexível), guardeStringem structs quando precisa de posse.Vec<T>(dona, heap) vs&[T](slice emprestado) — mesma relação.
É o coração de qualquer entrevista de Rust: "Explique ownership, move e borrow", "Qual a regra sobre & e &mut?" (aliasing XOR mutability), "O que é um lifetime e quando você precisa anotá-lo?", "Por que este código não compila?" (quase sempre: referência que sobreviveria ao dono, ou &mut junto de &). "String vs &str".
✏️ Exercício 2 — Por que não compila?
Explique o erro: fn main() { let r; { let x = 5; r = &x; } println!("{r}"); }. E: como você reescreveria para funcionar?
Gabarito: r é uma referência a x, mas x sai de escopo no fim do bloco interno — depois disso r seria um ponteiro pendente. O borrow checker rejeita: "x does not live long enough". Correções: (a) declarar x no mesmo escopo de r (ou mais externo), para que viva tanto quanto r; (b) não usar referência — let r = { let x = 5; x }; move/copia o valor (i32 é Copy), e r passa a ser dono de um 5 independente.
O sistema de tipos: enums, traits, genéricos
Objetivo: os tipos algébricos e o pattern matching, Option/Result no lugar de null e exceções, e traits como o mecanismo de abstração de Rust.
3.1 Enums são tipos-soma de verdade
enum Evento { Clique { x: i32, y: i32 }, // variante com campos nomeados Tecla(char), // variante com dado posicional Fechar, // variante sem dado } fn tratar(e: Evento) { match e { Evento::Clique { x, y } => println!("clique em {x},{y}"), Evento::Tecla(c) => println!("tecla {c}"), Evento::Fechar => println!("fechando"), } // match é EXAUSTIVO: esqueceu uma variante? erro de compilação }
Diferente dos enums "lista de constantes" de C/Java, os enums de Rust carregam dados por variante — são tipos algébricos. Combinados com match exaustivo, modelam estados impossíveis de representar errado.
3.2 Sem null: Option<T>
enum Option<T> { Some(T), None } // da stdlib let achado: Option<&Usuario> = usuarios.iter().find(|u| u.id == id); match achado { Some(u) => println!("{}", u.nome), None => println!("não encontrado"), } // o compilador NÃO deixa você usar o valor sem tratar o None
Não existe null. "Pode não haver valor" é Option<T>, e o compilador força você a lidar com o None. A classe inteira de "null pointer exception" desaparece.
3.3 Sem exceções: Result<T, E> e ?
enum Result<T, E> { Ok(T), Err(E) } fn ler_config(path: &str) -> Result<Config, std::io::Error> { let texto = std::fs::read_to_string(path)?; // ? : se Err, retorna o Err daqui let cfg = parse(&texto)?; Ok(cfg) }
Erros recuperáveis são valores Result. O operador ? propaga o erro para cima (early return) sem try/catch, e a assinatura da função declara o que pode falhar. Detalhes no Módulo 4.
3.4 Traits (as "interfaces" de Rust)
trait Area { fn area(&self) -> f64; fn descricao(&self) -> String { // método com implementação padrão format!("área = {:.2}", self.area()) } } struct Circulo { r: f64 } impl Area for Circulo { fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r } }
- Traits definem comportamento compartilhado;
impl Trait for Tipoimplementa. - Coherence / orphan rule: você só pode implementar um trait para um tipo se possui o trait ou o tipo — evita conflitos entre crates.
- Traits famosos da stdlib:
Debug,Clone,PartialEq,Iterator,From/Into,Display,Default— muitos deriváveis com#[derive(...)].
3.5 Genéricos e trait bounds
// "T qualquer, desde que seja comparável por ordem" fn maior<T: PartialOrd>(lista: &[T]) -> &T { let mut m = &lista[0]; for x in lista { if x > m { m = x; } } m }
- Monomorfização: o compilador gera uma versão especializada por tipo concreto usado — zero custo em runtime (vs dynamic dispatch).
- Static dispatch (
impl Trait/ genérico) vs dynamic dispatch (&dyn Trait/Box<dyn Trait>, via vtable — flexível, custo pequeno de indireção). whereclauses para bounds complexos legíveis.
Perguntas: "O que são enums em Rust e por que match exaustivo importa?" (tipos-soma; estados inválidos viram erro de compilação), "Como Rust evita null e exceções?" (Option e Result, tratamento forçado), "O que é um trait e a orphan rule?", "Static vs dynamic dispatch (impl Trait vs dyn Trait)", "O que é monomorfização?".
✏️ Exercício 3 — Modele com o tipo
Modele o estado de uma requisição HTTP que pode estar: pendente, concluída com um corpo (String) e status (u16), ou falha com uma mensagem. Depois escreva o match que loga cada caso. Por que isso é melhor que 3 campos Option num struct?
Gabarito: enum Requisicao { Pendente, Concluida { status: u16, corpo: String }, Falha(String) }. O match obriga a tratar os três casos e dá acesso só aos dados daquele caso. Melhor que struct { status: Option<u16>, corpo: Option<String>, erro: Option<String> } porque este permite estados sem sentido (concluída com erro preenchido, pendente com corpo) que você teria que checar à mão; o enum torna esses estados irrepresentáveis.
Erros: Result, ? e panics
Objetivo: o modelo de erros de Rust em detalhe — propagação com ?, conversão entre tipos de erro, as crates thiserror e anyhow, e quando entra panic!.
4.1 Recuperável (Result) vs irrecuperável (panic!)
Result<T, E> | panic! |
|---|---|
| Erro esperado: arquivo ausente, input inválido, rede caiu, parse falhou | Bug / invariante violada / "isto nunca deveria acontecer" |
| O chamador decide o que fazer | Aborta a thread (por padrão); pode desenrolar a stack ou abortar o processo |
| Faz parte do contrato da função (assinatura) | Não aparece na assinatura |
| O caso normal em bibliotecas e serviços | unwrap()/expect() em protótipos, testes, e onde você provou que não falha |
4.2 Propagação com ? e conversão de erro
fn carregar() -> Result<Dados, MeuErro> { let raw = std::fs::read_to_string("dados.json")?; // io::Error -> MeuErro via From let d: Dados = serde_json::from_str(&raw)?; // serde_json::Error -> MeuErro Ok(d) }
O ? faz duas coisas: se Ok(v), extrai v; se Err(e), converte e para o tipo de erro da função (via trait From) e faz return Err(...). É por isso que definir From<io::Error> for MeuErro (ou usar thiserror) faz o ? "simplesmente funcionar".
4.3 thiserror (bibliotecas) e anyhow (aplicações)
// EM BIBLIOTECA: erro tipado, para o chamador ramificar use thiserror::Error; #[derive(Error, Debug)] pub enum RepoError { #[error("pedido {0} não encontrado")] NotFound(String), #[error("falha de banco")] Db(#[from] sqlx::Error), // gera From automaticamente -> ? funciona } // EM APLICAÇÃO (main, handlers): não importa o tipo exato, só o contexto use anyhow::{Context, Result}; fn run() -> Result<()> { let cfg = ler_config().context("carregar config")?; // erro final: "carregar config: arquivo dados.json não existe" Ok(()) }
Biblioteca → thiserror (erro concreto e tipado; o consumidor precisa poder distinguir os casos). Aplicação/binário → anyhow (você só quer propagar com contexto e imprimir bonito no fim). anyhow::Error é um Box<dyn Error + Send + Sync> com backtrace e cadeia de contexto.
4.4 unwrap, expect, e as alternativas
.unwrap()— pega o valor oupanic!. Ok em testes/protótipo; em produção, sinal de código a revisar..expect("mensagem")— como unwrap mas com mensagem; use quando você provou que não falha ("regex literal é sempre válida")..unwrap_or(default),.unwrap_or_else(|| ...),.unwrap_or_default()— fallback sem panic.if let/let ... else/match— tratamento explícito.- Configurar
panic = "abort"no perfil release para binário menor e sem unwinding (comum em embedded/CLI).
Perguntas: "Como Rust trata erros e por que não tem exceções?" (Result como valor; contrato explícito; ? para propagar), "Quando panic!?" (irrecuperável/bug; nunca para erro esperado), "thiserror ou anyhow?" (lib vs app), "O que o ? faz exatamente?" (extrai Ok ou converte-e-retorna Err via From).
✏️ Exercício 4 — Escolha a estratégia
Você está escrevendo (a) uma crate de cliente HTTP para outros usarem; (b) o main de uma CLI que usa essa crate. Que abordagem de erro em cada um, e por quê? O main pode retornar Result?
Gabarito: (a) Crate cliente: thiserror com um enum HttpError distinguindo Timeout, Status(u16), Connect(...), Decode(...) — o consumidor precisa poder tratar timeout diferente de 404. Implementar #[from] para os erros de dependências. (b) main da CLI: anyhow — fn main() -> anyhow::Result<()> (sim, main pode retornar Result; o runtime imprime o Debug do erro e sai com código ≠ 0). Usar .context("baixando X") nos pontos-chave para uma mensagem de erro legível no fim.
Coleções, iterators e closures
Objetivo: as coleções da stdlib, os iterators lazy de custo zero, e as closures com suas três variedades — o estilo funcional idiomático de Rust.
5.1 Coleções principais
| Tipo | Uso |
|---|---|
Vec<T> | Array dinâmico — a coleção padrão |
String / &str | Texto UTF-8 (dono / emprestado) |
HashMap<K, V> / BTreeMap | Mapa hash / mapa ordenado |
HashSet / BTreeSet | Conjunto |
VecDeque | Fila dupla |
Box<[T]>, &[T] | Slice possuído / emprestado |
5.2 Iterators: lazy e zero-cost
let nums = vec![1, 2, 3, 4, 5, 6]; let soma_pares_ao_quadrado: i32 = nums .iter() // &i32 .filter(|&&x| x % 2 == 0) // nada roda ainda (lazy) .map(|&x| x * x) .sum(); // consumidor: AGORA a cadeia executa, num passo só // resultado: 4 + 16 + 36 = 56 — compilado tão rápido quanto o loop manual
- Lazy: adaptadores (
map,filter,take,zip,enumerate...) não fazem nada até um consumidor (collect,sum,for,fold,find,count). - Zero-cost: a cadeia inteira compila para um único loop sem alocações intermediárias — não há penalidade por escrever no estilo funcional.
- Três formas de iterar:
.iter()(&T),.iter_mut()(&mut T),.into_iter()(T, consome a coleção). .collect::<Vec<_>>()ou.collect::<Result<Vec<_>, _>>()(curto-circuita no primeiro erro — idioma muito útil).
5.3 Closures e os traits Fn
let fator = 3; let multiplica = |x: i32| x * fator; // captura `fator` do ambiente println!("{}", multiplica(10)); // 30
| Trait | Como captura | Exemplo |
|---|---|---|
Fn | Por referência imutável (&); pode chamar várias vezes | Ler uma variável do ambiente |
FnMut | Por referência mutável; pode chamar várias vezes, muta o capturado | Incrementar um contador externo |
FnOnce | Por valor (move); só pode ser chamada uma vez | Mover uma String para dentro e consumi-la |
- O compilador infere o trait mais permissivo possível.
moveforça captura por valor (essencial ao passar para outra thread). - Passar closures: genérico
fn f<F: Fn(i32) -> i32>(g: F)(static dispatch) ouBox<dyn Fn(i32) -> i32>(dynamic, quando precisa guardar em struct/coleção).
5.4 Padrões idiomáticos
// agrupar let mut por_categoria: HashMap<&str, Vec<&Produto>> = HashMap::new(); for p in &produtos { por_categoria.entry(p.categoria.as_str()).or_default().push(p); } // transformar com early-return de erro let numeros: Result<Vec<i32>, _> = linhas.iter().map(|l| l.parse::<i32>()).collect();
Perguntas: "Por que iterators em Rust são 'zero-cost'?" (lazy + monomorfização compilam para um loop sem alocação), "Diferença entre iter, iter_mut e into_iter", "Fn, FnMut, FnOnce — quando cada um?", "O que move faz numa closure e por que é necessário para thread::spawn?".
✏️ Exercício 5 — Reescreva o loop
Reescreva no estilo iterator: "dado Vec<Pedido> onde cada pedido tem .pago: bool e .valor: f64, some o valor dos pagos que passam de 100". Depois: por que a versão iterator não é mais lenta que o for?
Gabarito: let total: f64 = pedidos.iter().filter(|p| p.pago && p.valor > 100.0).map(|p| p.valor).sum();. Não é mais lenta porque os adaptadores são lazy e o compilador (com monomorfização + inlining + otimizações do LLVM) funde a cadeia num único loop equivalente ao for manual — mesmo código de máquina, sem alocações intermediárias nem overhead de fechamento.
Smart pointers e interior mutability
Objetivo: as ferramentas para os casos que o ownership simples não cobre — posse na heap, posse compartilhada, e mutação através de referência compartilhada, com as regras verificadas em runtime.
6.1 Box<T>
Aloca T na heap; o Box é o dono único. Usos: tipos recursivos (enum No { Folha, Ramo(Box<No>) } — o compilador precisa de um tamanho conhecido), mover um valor grande sem copiar, e Box<dyn Trait> para dynamic dispatch / apagar o tipo concreto.
6.2 Rc<T> e Arc<T> — posse compartilhada
use std::rc::Rc; let config = Rc::new(Config::carregar()); let a = Rc::clone(&config); // +1 no contador; NÃO copia a Config let b = Rc::clone(&config); // a Config é liberada quando o último Rc sai de escopo
Rc<T>: contagem de referências, single-thread. Vários donos de leitura; o valor vive enquanto houver umRc.Arc<T>: o mesmo, mas atômico — seguro para compartilhar entre threads (Módulo 7). Custa um pouco mais.- Ambos dão acesso imutável. Para mutar o conteúdo compartilhado, combine com interior mutability.
- Ciclos de
Rcvazam memória (o contador nunca chega a zero) — quebre comWeak<T>.
6.3 Interior mutability: Cell, RefCell, Mutex
Às vezes você precisa mutar um valor através de uma referência imutável (&T) — por exemplo, um cache dentro de um método &self. As regras de borrow passam a ser checadas em runtime:
| Tipo | Checagem | Contexto |
|---|---|---|
Cell<T> | Sem referências ao interior; só get/set/replace de valores Copy | Single-thread, tipos simples |
RefCell<T> | Empréstimos verificados em runtime: .borrow() / .borrow_mut(); panic se violar aliasing XOR mutability | Single-thread |
Mutex<T> / RwLock<T> | Bloqueio; .lock() devolve um guard | Multi-thread (Módulo 7) |
use std::cell::RefCell; struct Cache { dados: RefCell<HashMap<String, String>> } impl Cache { fn get(&self, k: &str) -> Option<String> { // &self, mas muta o interior if let Some(v) = self.dados.borrow().get(k) { return Some(v.clone()); } let v = calcular(k); self.dados.borrow_mut().insert(k.to_string(), v.clone()); Some(v) } }
6.4 Os padrões combinados
Rc<RefCell<T>>: posse compartilhada + mutação, single-thread. O padrão para grafos, árvores com pais, observadores. Cuidado com panics de borrow e com ciclos.Arc<Mutex<T>>: a versão multi-thread — estado compartilhado entre threads com bloqueio.Arc<RwLock<T>>: quando há muito mais leitura que escrita.
Rc<RefCell<T>> cedo demaisMuitas vezes é sinal de que o design está "pensando em C" (ponteiros por todo lado). Antes de partir para isso, pergunte: dá para modelar com ownership em árvore (um dono claro, o resto empresta)? com índices num Vec em vez de ponteiros (arena pattern)? com canais em vez de estado compartilhado? Rust idiomático usa Rc<RefCell> menos do que parece.
6.5 Deref e Drop
Deref: faz um tipo se comportar como uma referência ao que ele contém (Box<T>,Rc<T>,String→&str). É o que permite chamar métodos deTnumBox<T>.Drop: código a rodar quando o valor é liberado (fechar arquivo, liberar lock, log). É determinístico — roda no fim do escopo, ordem inversa da declaração. É o RAII de C++, garantido.
Perguntas: "Quando Box, Rc, Arc?" (heap dono único; compartilhado single-thread; compartilhado multi-thread), "O que é interior mutability e o que RefCell faz de diferente?" (regras de borrow em runtime, panic se violar), "Quando Rc<RefCell<T>> e quais os riscos?" (ciclos, panic de borrow; considerar arena/índices antes), "O que é Drop e RAII em Rust?".
✏️ Exercício 6 — Escolha o ponteiro
Para cada caso, qual smart pointer (ou combinação)? (a) uma árvore sintática onde cada nó é dono dos filhos; (b) um cache compartilhado por várias threads; (c) várias partes do código que só leem a mesma configuração grande, single-thread; (d) um contador incrementado dentro de um método &self, single-thread.
Gabarito: (a) Box<Node> nos filhos — ownership em árvore, dono único claro. (b) Arc<Mutex<Cache>> (ou Arc<RwLock> se leitura domina). (c) Rc<Config> — vários leitores, sem mutação, single-thread. (d) Cell<u64> (valor Copy, get/set) ou RefCell se for algo mais complexo — interior mutability para mutar sob &self.
Concorrência sem data races e async
Objetivo: a "fearless concurrency" — threads e Send/Sync, paralelismo de dados com rayon, e o modelo async/await com tokio, sabendo quando usar cada um.
7.1 Threads e o porquê de "fearless"
use std::{sync::{Arc, Mutex}, thread}; let contador = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let c = Arc::clone(&contador); handles.push(thread::spawn(move || { // move: a thread toma posse do que captura let mut n = c.lock().unwrap(); *n += 1; })); } for h in handles { h.join().unwrap(); } println!("{}", *contador.lock().unwrap()); // 10, sempre
Dois marker traits que o compilador aplica automaticamente: Send = pode ser movido para outra thread; Sync = &T pode ser compartilhado entre threads. Rc não é Send (contador não atômico) — o compilador te impede de passá-lo para thread::spawn. É assim que "data race em código seguro é impossível" — a regra aliasing-XOR-mutability + Send/Sync cobrem o caso concorrente sem esforço seu.
7.2 Paralelismo de dados com rayon
use rayon::prelude::*; let soma: u64 = numeros.par_iter().map(|&x| caro(x)).sum(); // troca .iter() por .par_iter() e a stdlib distribui em todos os cores. // o borrow checker garante que a paralelização é segura.
rayon é a forma mais fácil de ganhar paralelismo: em cargas CPU-bound "embaraçosamente paralelas" (processar cada item independente), muitas vezes é só trocar o iterator. Usa um pool de threads com work-stealing por baixo.
7.3 async/await
async fn buscar_usuario(id: u64) -> Result<Usuario> { let resp = client.get(&url(id)).send().await?; // suspende aqui, não bloqueia a thread let u = resp.json().await?; Ok(u) } // concorrência: dois fetches ao mesmo tempo let (a, b) = tokio::join!(buscar_usuario(1), buscar_usuario(2));
async fnretorna umFuture— uma computação que ainda não rodou. Ela só progride quando um runtime (tokio, o dominante;async-std,smol) a executa e faz poll.- Zero-cost: o compilador transforma a
async fnnuma máquina de estados; não há alocação por await, sem "green thread" pesada. - Escala para centenas de milhares de conexões concorrentes numa mão-cheia de threads do SO — o modelo para servidores de rede I/O-bound.
tokio::spawnpara tarefas concorrentes;select!para esperar a primeira de várias; canais async (tokio::sync::mpsc).
7.4 O "function coloring" e quando async
async fn só pode ser chamada de outra async fn (ou .awaitada dentro de um runtime). Isso "propaga" async pela sua base de código e cria atrito com bibliotecas síncronas (às vezes é preciso spawn_blocking). O ecossistema async ainda é mais complexo (lifetimes em futures, Pin, Send bounds em traits async). Regra: async para I/O concorrente em escala (servidores web, muitos clientes de rede); threads + rayon para paralelismo de CPU; síncrono simples para CLIs e a maioria das ferramentas — não pague o custo do async sem a necessidade.
| Cenário | Ferramenta |
|---|---|
| CLI, script, ferramenta de build | Síncrono. Sem runtime, sem complexidade. |
| Processar N itens independentes usando todos os cores | rayon (par_iter) |
| Poucas tarefas concorrentes de trabalho pesado | std::thread + Arc<Mutex> / canais |
| Servidor web, milhares de conexões, muito I/O | async + tokio (+ axum/hyper) |
| CPU-bound dentro de um contexto async | tokio::task::spawn_blocking ou um pool rayon à parte |
Perguntas: "O que são Send e Sync e como garantem ausência de data race?", "Por que Rc não pode cruzar threads?" (não é Send), "async vs threads — quando cada um?" (I/O concorrente em escala vs CPU-bound), "O que é 'function coloring'?", "O que é um Future e o que o runtime faz?" (computação lazy; o runtime faz poll). rayon e tokio são os nomes a conhecer.
✏️ Exercício 7 — Escolha o modelo
(a) Processar 10 milhões de linhas de um CSV, transformando cada uma (independente), o mais rápido possível numa máquina de 16 cores. (b) Um gateway que aceita 50 mil conexões WebSocket simultâneas, cada uma quase sempre ociosa. (c) Uma CLI que baixa 20 arquivos e junta. Qual abordagem para cada?
Gabarito: (a) rayon — par_iter()/par_bridge() sobre as linhas; CPU-bound e embaraçosamente paralelo, usa os 16 cores sem você gerir threads. (b) async + tokio — 50 mil conexões majoritariamente ociosas seriam 50 mil threads desperdiçadas; async multiplexa tudo em poucas threads. (c) Depende do peso: 20 downloads podem ser std::thread + join (simples, sem runtime), ou async com tokio se você já usa. Não vale trazer tokio só para 20 arquivos — threads bastam.
Cargo, ferramentas e unsafe
Objetivo: o ecossistema de build e qualidade (cargo, clippy, testes, workspaces, features), a compilação release, e o que unsafe realmente significa.
8.1 Cargo
cargo new/init,cargo build/run/test/bench,cargo add,cargo doc --open.Cargo.toml(manifesto) +Cargo.lock(versões exatas — commite em binários, opcional em libs).- Workspaces: vários crates no mesmo repositório com
Cargo.locketarget/compartilhados — o padrão para projetos médios/grandes ([workspace] members = [...]). - Features: flags de compilação opcionais (
[features]) para ligar/desligar partes de um crate e suas dependências;default = [...]; cuidado com feature unification. - Perfis:
[profile.release]comopt-level = 3,lto = true,codegen-units = 1,panic = "abort",strip = true— binário menor e mais rápido, build mais lento.
8.2 Qualidade
| Ferramenta | O que faz |
|---|---|
rustfmt (cargo fmt) | Formatação canônica — sem debate de estilo |
clippy (cargo clippy) | Centenas de lints: idiomas, armadilhas, performance, correção. Rode com -D warnings no CI |
| Testes | #[test] (unit, no mesmo arquivo, em mod tests); tests/ (integração, caixa-preta); doctests (exemplos na doc que são compilados e executados) |
cargo-nextest | Runner de testes mais rápido e com melhor output |
cargo-audit / cargo-deny | Vulnerabilidades conhecidas (RustSec), licenças, dependências banidas |
| Cobertura | cargo-llvm-cov / tarpaulin |
| Fuzzing / property | cargo-fuzz, proptest, quickcheck |
| Bench | criterion (estatístico, com gráficos) |
/// Soma dois números. /// ``` /// assert_eq!(minha_crate::soma(2, 3), 5); // este exemplo É um teste /// ``` pub fn soma(a: i32, b: i32) -> i32 { a + b }
8.3 Cross-compilation
rustup target add x86_64-unknown-linux-musl e cargo build --target .... Com o target musl você gera um binário totalmente estático (roda em qualquer Linux, imagem Docker FROM scratch). Ferramentas: cross (usa Docker para os toolchains), cargo-zigbuild. Rust facilita entregar um binário por plataforma.
8.4 unsafe
unsafe não desliga o borrow checker. Ele só libera cinco superpoderes que o compilador não consegue verificar: desreferenciar ponteiro cru (*const T/*mut T), chamar função unsafe (inclui FFI), acessar/mutar static mut, implementar um trait unsafe, e acessar campos de union.
- O contrato: dentro de um bloco
unsafe, você garante as invariâncias que o compilador garantiria. Um bug ali pode causar UB (undefined behavior) em qualquer lugar. - O padrão: encapsular o
unsafemínimo numa API segura (a stdlib faz isso o tempo todo —Vecusaunsafepor dentro, mas você o usa 100% seguro). - Quando: FFI (chamar C), estruturas de dados que o borrow checker não expressa, otimizações provadas,
no_std/embedded, SIMD manual. - Verificação: Miri (interpretador que detecta UB),
cargo +nightly miri test. - Regra: se você não precisa de
unsafe, não use. A grande maioria do código de aplicação nunca escreve um blocounsafe.
Perguntas: "O que unsafe realmente faz?" (libera 5 operações não verificáveis; não desliga o borrow checker; você assume o contrato), "Como você entrega um binário estático?" (target musl / FROM scratch), "Para que servem features do Cargo?", "O que são doctests?", "Como rodar clippy no CI?" (cargo clippy -- -D warnings).
✏️ Exercício 8 — Configure o release
Você tem uma CLI Rust e quer: binário o menor possível, o mais rápido possível, e um só arquivo estático para distribuir no Linux. O que você coloca no Cargo.toml e como builda?
Gabarito: No Cargo.toml: [profile.release] com opt-level = "z" ou 3 (z = menor; 3 = mais rápido — escolha a prioridade), lto = true, codegen-units = 1, panic = "abort", strip = true. Build: rustup target add x86_64-unknown-linux-musl e cargo build --release --target x86_64-unknown-linux-musl — gera um binário estático (musl) que roda em qualquer distro e cabe num FROM scratch. Para releases multiplataforma automáticas: cargo-dist ou cargo-zigbuild no CI.
Rust aplicado: CLI, WebAssembly, serviços e FFI
Objetivo: os quatro domínios onde Rust mais aparece em vagas — ferramentas de linha de comando, WebAssembly, serviços web, e núcleos nativos embutidos em outras linguagens.
9.1 CLIs
clap(comderive): parsing de argumentos declarativo — subcomandos, flags, validação, help e autocompletar gerados.anyhowpara erros (Módulo 4),indicatif(barras de progresso),console/owo-colors(cor),ratatui(TUIs interativas).- Configuração:
config/figment;directoriespara paths de config por SO. - Por que Rust em CLI: binário único sem runtime, arranque instantâneo, cross-compile, e o ecossistema já provou (
ripgrep,fd,bat,delta,starship,uv,ruff).
use clap::Parser; #[derive(Parser)] #[command(version, about)] struct Args { /// arquivo de entrada input: String, /// número de threads #[arg(short, long, default_value_t = 4)] jobs: usize, } fn main() -> anyhow::Result<()> { let args = Args::parse(); // ... Ok(()) }
9.2 WebAssembly
| Alvo | Ferramentas | Uso |
|---|---|---|
| Browser | wasm-bindgen + wasm-pack; web-sys/js-sys para APIs do browser; trunk ou Vite para bundling | Trechos quentes de apps web: parsers, cripto, image/áudio, jogos, editores (Figma usa Rust/WASM); frameworks full-WASM: Leptos, Yew, Dioxus |
| Server-side / Edge | WASI (WebAssembly System Interface); runtimes wasmtime, wasmer; plataformas: Cloudflare Workers, Fastly Compute, Fermyon Spin | Funções de edge, plugins sandboxados, multi-tenant seguro (o WASM isola melhor que contêiner e arranca em microssegundos) |
| Component Model | cargo component, WIT (interface types) | Componentes WASM poliglotas e componíveis — a direção futura do ecossistema |
Rust tem o melhor tooling de WASM de qualquer linguagem: sem GC para embarcar, binários pequenos, e wasm-bindgen resolve a ponte com JS. É um dos motivos mais fortes para aprender Rust hoje.
9.3 Serviços web
axum(sobrehyper+tower): o framework mais recomendado — extractors tipados, middleware viatower, integração tokio. Alternativas:actix-web(muito rápido, ator-based),poem,rocket(mais "mágico").tower: abstração de "serviço" (Request -> Future<Response>) — timeout, retry, rate limit, load balance como camadas reutilizáveis.- Banco:
sqlx(async, checa SQL em compile-time contra o schema),sea-orm,diesel(síncrono, ORM maduro). - Observabilidade:
tracing(spans estruturados) +tracing-subscriber; OpenTelemetry viaopentelemetry. - Por que: p99 previsível (sem GC), uso de memória baixo, e o compilador pega classes inteiras de bug antes do deploy. Discord, AWS, Cloudflare, Fly.io rodam serviços Rust em produção crítica.
9.4 FFI: Rust dentro de outras linguagens
| Alvo | Ferramenta |
|---|---|
| Python | PyO3 + maturin (empacota como wheel). Usado por pydantic-core, polars, ruff, cryptography |
| Node.js | napi-rs — usado por @swc/core, Prisma engine, Biome |
| Multiplataforma (Kotlin/Swift/…) | UniFFI (Mozilla) — gera bindings a partir de uma IDL |
| C ABI | #[no_mangle] pub extern "C" + cbindgen para o header |
O padrão dominante: mantenha a aplicação na linguagem de alto nível e reescreva só o núcleo quente em Rust, exposto como extensão nativa. Ganho de 10–100× no trecho crítico sem reescrever tudo.
9.5 no_std e embedded (panorama)
#![no_std] remove a biblioteca padrão (que assume um SO), deixando o core — para microcontroladores e firmware. Ecossistema: embassy (async em embedded), embedded-hal (abstração de hardware), defmt (logging eficiente), Rust oficialmente suportado em partes do kernel Linux. É um nicho crescente com pouca concorrência de talento.
Perguntas por área: "Por que Rust é bom para CLIs?" (binário único, arranque instantâneo, cross-compile), "Como você exporia uma função Rust para WASM no browser?" (wasm-bindgen + wasm-pack), "axum ou actix?" (axum pela integração tower/tokio e ergonomia; actix por performance bruta), "Como acelerar um trecho de Python com Rust?" (PyO3 + maturin, reescrever só o núcleo).
✏️ Exercício 9 — Escolha a aplicação
Uma empresa quer: (a) acelerar a validação de schema de uma API Python que é o gargalo; (b) um plugin sandboxado que clientes podem subir para rodar no edge da plataforma; (c) uma ferramenta interna de linha de comando para os devs, multiplataforma. Como Rust entra em cada?
Gabarito: (a) Crate Rust com a lógica de validação, exposta via PyO3 e empacotada com maturin como wheel; o resto da API continua Python (padrão pydantic-core). (b) Compilar o SDK do plugin para WASM/WASI e rodar os plugins dos clientes num runtime wasmtime — isolamento forte, arranque em microssegundos, multi-tenant seguro. (c) CLI com clap + anyhow, cross-compilada no CI (cargo-dist) para Linux/macOS/Windows — um binário por plataforma, sem runtime a instalar.
Mercado de trabalho: roadmap, entrevistas e portfólio
Objetivo: converter o conteúdo dos módulos anteriores em aprovação — perfis, roadmap, banco de perguntas com respostas e projetos que geram entrevista.
10.1 Onde Rust contrata
| Setor / perfil | O que se faz |
|---|---|
| Cripto / blockchain | Protocolos, nós, contratos (Solana, Polkadot), infra — a maior concentração de vagas Rust |
| Infra cloud-native / plataforma | Proxies, agents, service meshes, bancos, sistemas de storage |
| Dev tooling | Linters, formatters, bundlers, package managers, LSPs (Biome, Ruff, uv, Turbopack) |
| Browsers / rendering / jogos | Motores, engines gráficos, WASM |
| Embedded / automotivo / IoT | Firmware seguro, no_std |
| Serviços de latência crítica | Adtech, trading, streaming, edge |
10.2 Roadmap de estudo (10–12 semanas — a curva é real)
| Semanas | Foco | Prática |
|---|---|---|
| 1–2 | Sintaxe + ownership/borrow/lifetimes (Módulos 1–2) — não avance sem isto sedimentado | "The Book" cap. 1–10 + Rustlings; uma CLI pequena que compila sem .clone() desnecessário |
| 3–4 | Enums, match, Option/Result, traits, genéricos, erros (Módulos 3–4) | Um parser (ex.: de um formato simples) com enum de tokens e thiserror |
| 5 | Iterators, closures, coleções (Módulo 5) | Reescrever loops no estilo iterator; um mini "wc"/"grep" |
| 6 | Smart pointers e interior mutability (Módulo 6) | Uma estrutura de dados (LRU cache, ou uma árvore) — sentir quando Rc<RefCell> e quando índices |
| 7–8 | Concorrência: threads, Send/Sync, rayon; depois async + tokio (Módulo 7) | Um baixador concorrente (threads) e uma pequena API com axum (async) |
| 9 | Cargo a fundo, clippy, testes, workspaces, release, unsafe conceitual (Módulo 8) | Transformar o projeto num workspace com CI (fmt + clippy -D + nextest + audit) |
| 10 | Escolher uma aplicação: WASM, ou serviço axum, ou extensão PyO3 (Módulo 9) | Um projeto real nessa trilha |
| 11–12 | Consolidação, ler código de um projeto grande, portfólio | Contribuição pequena em OSS Rust + estudo de caso publicado |
10.3 Banco de perguntas de entrevista (com a resposta que aprova)
Júnior/pleno — "Explique ownership, move e borrow"
Cada valor tem um dono; quando o dono sai de escopo, o valor é liberado (sem GC). Atribuir/passar por valor move a posse (o antigo nome fica inválido), a menos que o tipo seja Copy. Para usar sem tomar posse, você empresta: &T (várias, imutáveis) ou &mut T (uma, exclusiva) — nunca as duas ao mesmo tempo. O borrow checker prova em compilação que nenhuma referência sobrevive ao dado.
Pleno — "Como Rust evita null e exceções?"
Option<T> (Some/None) para ausência — o compilador obriga a tratar o None, não há null a desreferenciar. Result<T, E> (Ok/Err) para erro recuperável — é um valor na assinatura, propagado com ?, sem try/catch. panic! só para o irrecuperável/bug. Resultado: NPE e exceções não capturadas deixam de ser uma classe de bug.
Pleno — "Quando Box, Rc, Arc, RefCell?"
Box<T>: valor na heap, dono único (tipos recursivos, dyn Trait). Rc<T>: vários donos, leitura, single-thread. Arc<T>: idem entre threads (contador atômico). RefCell<T>: mutar através de &self, regras de borrow checadas em runtime (panic se violar), single-thread. Combinações: Rc<RefCell<T>> (grafo single-thread), Arc<Mutex<T>> (estado compartilhado entre threads).
Pleno/sénior — "Como Rust garante 'fearless concurrency'?"
A regra aliasing-XOR-mutability já impede que duas partes mutem o mesmo dado simultaneamente. Somado a isso, os marker traits Send (pode ir para outra thread) e Sync (&T pode ser compartilhado) são aplicados automaticamente e verificados: passar um Rc (não Send) para thread::spawn não compila. Data race em código seguro é impossível — é erro de compilação, não bug de runtime.
Sénior — "async vs threads em Rust"
Threads (+ rayon) para paralelismo de CPU: trabalho pesado dividido entre cores. async + tokio para concorrência de I/O em escala: milhares de conexões majoritariamente ociosas multiplexadas em poucas threads, via máquina de estados de custo zero. async "colore" o código (propaga pela base, atrito com libs síncronas, Pin/Send bounds) — não adote sem a necessidade real. CLIs e a maioria das ferramentas: síncrono simples.
Sénior — "O que unsafe faz e não faz?"
Não desliga o borrow checker. Libera 5 operações não verificáveis: desreferenciar ponteiro cru, chamar função unsafe/FFI, mexer em static mut, implementar trait unsafe, acessar union. Dentro do bloco, você assume o contrato de segurança que o compilador garantiria — um erro ali pode causar UB em qualquer lugar. O padrão idiomático é encapsular o mínimo de unsafe numa API segura (como a stdlib faz).
Armadilha — "Rust é lento de escrever / 'fighting the borrow checker'"
A curva inicial é real (semanas), e no começo você luta com o compilador. Mas isso converge: o compilador está te ensinando a modelar ownership, e depois de sedimentar, a maioria dos "erros" são pegos antes do runtime, o que reduz o tempo total (menos debugging de memória e concorrência em produção). Para o domínio certo (sistemas, performance, correção), o investimento se paga.
10.4 Projetos de portfólio que geram entrevista
- Uma CLI de verdade (âncora): algo útil (um formatador, um analisador de logs, um cliente de API) com
clap, tratamento de erro comanyhow, testes + doctests, CI com fmt/clippy/nextest, e releases cross-platform. README com as decisões. - Estrutura de dados não trivial: um LRU cache, um trie, ou um pequeno interpretador — mostrando que você modela ownership (arena/índices vs
Rc<RefCell>) conscientemente. - WASM no browser: um módulo Rust (parser, cripto, processamento de imagem) exposto via
wasm-bindgen, com um demo e um benchmark vs a versão JS. - Serviço axum: API com
sqlx,tracing, testes de integração, Dockerfilescratch, métricas. - Extensão nativa: um núcleo quente de um programa Python/Node reescrito em Rust via PyO3/napi-rs, com o "antes/depois" de performance.
- Contribuições: issues "good first issue" em crates populares; resolver os Rustlings/Advent of Code em Rust e publicar.
10.5 Fontes para continuar
- Oficial: "The Rust Programming Language" ("The Book"), "Rust by Example", Rustlings (exercícios), o "Rust Reference" e o "Nomicon" (para
unsafe). - Livros: Programming Rust (Blandy, Orendorff, Tindall) — o mais completo; Rust for Rustaceans (Jon Gjengset) — nível intermediário→avançado, essencial para subir de nível; Rust Atomics and Locks (Mara Bos) para concorrência a fundo.
- Async: "Asynchronous Programming in Rust" (o livro oficial), a doc do tokio, o blog de Alice Ryhl.
- Prática: o canal do YouTube de Jon Gjengset ("Crust of Rust"); ler o código de
ripgrepou de um crate médio;this-week-in-rustpara acompanhar o ecossistema.
Quatro ideias sustentam Rust: (1) segurança de memória e de concorrência verificadas em compilação, sem GC — o borrow checker prova o que outras linguagens só torcem para estar certo; (2) a regra aliasing XOR mutability é o eixo de tudo — ownership, lifetimes, Send/Sync derivam dela; (3) modele estados impossíveis como irrepresentáveis com enums e o sistema de tipos; (4) a curva é real, mas o erro migra do runtime em produção para o compilador na sua mesa. Aprenda quando não usar Rust tão bem quanto quando usar.