Segurança de memória sem coletor de lixo — verificada no compilador

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.

10 módulosOwnership · borrow checker · lifetimesenum · trait · Resulttokio · rayon · WASMBoxes de entrevistaExercícios com gabarito
MÓDULO 01 · BÁSICO

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.

💡 "Zero-cost abstractions"

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óriaRust
Use-after-free, dangling pointerImpossível em código seguro — o borrow checker rejeita
Double-freeImpossível — cada valor tem um dono, liberado uma vez
Buffer overflowChecagem de bounds (removida onde provada desnecessária); slices carregam tamanho
Null pointer dereferenceNão há null — ausência é Option<T>, que você é obrigado a tratar
Data raceImpossível em código seguro — os traits Send/Sync e o borrow checker garantem "aliasing XOR mutability"
Iterator invalidationRejeitado — 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

1.4 Quando NÃO Rust

💼 Mercado de trabalho

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.

MÓDULO 02 · BÁSICO

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:

  1. Cada valor tem exatamente um dono (uma variável).
  2. Quando o dono sai de escopo, o valor é liberado (chamada automática de drop) — determinístico, sem GC.
  3. 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

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 regra central: aliasing XOR mutability

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

💼 Mercado de trabalho

É 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.

MÓDULO 03 · INTERMEDIÁRIO

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 }
}

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
}
💼 Mercado de trabalho

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.

MÓDULO 04 · INTERMEDIÁRIO

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 falhouBug / invariante violada / "isto nunca deveria acontecer"
O chamador decide o que fazerAborta 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çosunwrap()/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(())
}
💡 A regra

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

💼 Mercado de trabalho

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: anyhowfn 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.

MÓDULO 05 · INTERMEDIÁRIO

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

TipoUso
Vec<T>Array dinâmico — a coleção padrão
String / &strTexto UTF-8 (dono / emprestado)
HashMap<K, V> / BTreeMapMapa hash / mapa ordenado
HashSet / BTreeSetConjunto
VecDequeFila 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

5.3 Closures e os traits Fn

let fator = 3;
let multiplica = |x: i32| x * fator;   // captura `fator` do ambiente
println!("{}", multiplica(10));           // 30
TraitComo capturaExemplo
FnPor referência imutável (&); pode chamar várias vezesLer uma variável do ambiente
FnMutPor referência mutável; pode chamar várias vezes, muta o capturadoIncrementar um contador externo
FnOncePor valor (move); só pode ser chamada uma vezMover uma String para dentro e consumi-la

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();
💼 Mercado de trabalho

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.

MÓDULO 06 · AVANÇADO

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

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:

TipoChecagemContexto
Cell<T>Sem referências ao interior; só get/set/replace de valores CopySingle-thread, tipos simples
RefCell<T>Empréstimos verificados em runtime: .borrow() / .borrow_mut(); panic se violar aliasing XOR mutabilitySingle-thread
Mutex<T> / RwLock<T>Bloqueio; .lock() devolve um guardMulti-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

⚠️ Se você está alcançando Rc<RefCell<T>> cedo demais

Muitas 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

💼 Mercado de trabalho

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.

MÓDULO 07 · AVANÇADO

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
💡 Send e Sync

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));

7.4 O "function coloring" e quando async

⚠️ async "colore" o código

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árioFerramenta
CLI, script, ferramenta de buildSíncrono. Sem runtime, sem complexidade.
Processar N itens independentes usando todos os coresrayon (par_iter)
Poucas tarefas concorrentes de trabalho pesadostd::thread + Arc<Mutex> / canais
Servidor web, milhares de conexões, muito I/Oasync + tokio (+ axum/hyper)
CPU-bound dentro de um contexto asynctokio::task::spawn_blocking ou um pool rayon à parte
💼 Mercado de trabalho

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) rayonpar_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.

MÓDULO 08 · AVANÇADO

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

8.2 Qualidade

FerramentaO 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-nextestRunner de testes mais rápido e com melhor output
cargo-audit / cargo-denyVulnerabilidades conhecidas (RustSec), licenças, dependências banidas
Coberturacargo-llvm-cov / tarpaulin
Fuzzing / propertycargo-fuzz, proptest, quickcheck
Benchcriterion (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.

💼 Mercado de trabalho

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.

MÓDULO 09 · MUITO AVANÇADO

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

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

AlvoFerramentasUso
Browserwasm-bindgen + wasm-pack; web-sys/js-sys para APIs do browser; trunk ou Vite para bundlingTrechos quentes de apps web: parsers, cripto, image/áudio, jogos, editores (Figma usa Rust/WASM); frameworks full-WASM: Leptos, Yew, Dioxus
Server-side / EdgeWASI (WebAssembly System Interface); runtimes wasmtime, wasmer; plataformas: Cloudflare Workers, Fastly Compute, Fermyon SpinFunções de edge, plugins sandboxados, multi-tenant seguro (o WASM isola melhor que contêiner e arranca em microssegundos)
Component Modelcargo 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

9.4 FFI: Rust dentro de outras linguagens

AlvoFerramenta
PythonPyO3 + maturin (empacota como wheel). Usado por pydantic-core, polars, ruff, cryptography
Node.jsnapi-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.

💼 Mercado de trabalho

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.

MÓDULO 10 · CARREIRA

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 / perfilO que se faz
Cripto / blockchainProtocolos, nós, contratos (Solana, Polkadot), infra — a maior concentração de vagas Rust
Infra cloud-native / plataformaProxies, agents, service meshes, bancos, sistemas de storage
Dev toolingLinters, formatters, bundlers, package managers, LSPs (Biome, Ruff, uv, Turbopack)
Browsers / rendering / jogosMotores, engines gráficos, WASM
Embedded / automotivo / IoTFirmware seguro, no_std
Serviços de latência críticaAdtech, trading, streaming, edge

10.2 Roadmap de estudo (10–12 semanas — a curva é real)

SemanasFocoPrática
1–2Sintaxe + 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–4Enums, match, Option/Result, traits, genéricos, erros (Módulos 3–4)Um parser (ex.: de um formato simples) com enum de tokens e thiserror
5Iterators, closures, coleções (Módulo 5)Reescrever loops no estilo iterator; um mini "wc"/"grep"
6Smart pointers e interior mutability (Módulo 6)Uma estrutura de dados (LRU cache, ou uma árvore) — sentir quando Rc<RefCell> e quando índices
7–8Concorrência: threads, Send/Sync, rayon; depois async + tokio (Módulo 7)Um baixador concorrente (threads) e uma pequena API com axum (async)
9Cargo a fundo, clippy, testes, workspaces, release, unsafe conceitual (Módulo 8)Transformar o projeto num workspace com CI (fmt + clippy -D + nextest + audit)
10Escolher uma aplicação: WASM, ou serviço axum, ou extensão PyO3 (Módulo 9)Um projeto real nessa trilha
11–12Consolidação, ler código de um projeto grande, portfólioContribuiçã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

  1. Uma CLI de verdade (âncora): algo útil (um formatador, um analisador de logs, um cliente de API) com clap, tratamento de erro com anyhow, testes + doctests, CI com fmt/clippy/nextest, e releases cross-platform. README com as decisões.
  2. 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.
  3. 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.
  4. Serviço axum: API com sqlx, tracing, testes de integração, Dockerfile scratch, métricas.
  5. 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.
  6. Contribuições: issues "good first issue" em crates populares; resolver os Rustlings/Advent of Code em Rust e publicar.

10.5 Fontes para continuar

🏁 Síntese final da apostila

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.