Rust Essencial

Por que Rust

Rust nasceu em 2006 como projeto pessoal de Graydon Hoare na Mozilla. A versão 1.0 saiu em 2015. O objetivo era direto: oferecer a performance de C/C++ sem os bugs de memória que geram 70% das vulnerabilidades de segurança (dado confirmado pela Microsoft e pelo Google sobre Chromium).

A proposta central é o conceito de zero-cost abstractions: o compilador garante safety em tempo de compilação, sem runtime overhead. Não existe garbage collector, não existe reference counting implícito, não existe VM.

Crescimento e adoção

Empresa         │ Uso
────────────────┼──────────────────────────────────────────────
Google          │ Android (Binder IPC), Chrome, Fuchsia OS
Microsoft       │ Windows kernel modules, Azure IoT
Amazon          │ Firecracker (VMs do Lambda), Bottlerocket OS
Cloudflare      │ Pingora (proxy HTTP que substitui Nginx)
Meta            │ Source control (Mononoke), build system (Buck2)
Discord         │ Migrou serviço de Read States de Go para Rust
Linux Kernel    │ Suporte oficial a módulos em Rust (v6.1+)

O crescimento em vagas é de aproximadamente 67% ano a ano. Rust ainda é nicho, mas a tendência é clara — especialmente em infraestrutura, sistemas embarcados e segurança.

Onde Rust brilha

  • Systems programming — kernels, drivers, firmware, runtimes
  • Infraestrutura de rede — proxies, load balancers, DNS servers (Pingora, Linkerd2-proxy)
  • CLI tools — ripgrep, fd, bat, delta — substituem utilities Unix com performance superior
  • WebAssembly — suporte de primeira classe, ideal para edge computing e browser
  • Criptografia e segurança — memory safety elimina classes inteiras de vulnerabilidades

Onde Rust NÃO é ideal

  • Prototipação rápida — borrow checker adiciona fricção; Python ou Go são mais produtivos para POCs
  • Scripts simples — bash, Python ou Node são mais pragmáticos
  • Equipes sem experiência — curva de aprendizado real; lifetimes causam frustração nas primeiras semanas
  • CRUD web simples — Go ou Java entregam mais rápido sem requisitos extremos de performance

Ownership

Ownership é a inovação fundamental do Rust — memory safety sem garbage collector. As regras:

  1. Cada valor tem exatamente um owner
  2. Quando o owner sai de escopo, o valor é dropped (memória liberada)
  3. Ownership pode ser transferida (move), não duplicada implicitamente

Move Semantics

fn main() {
    let s1 = String::from("hello"); // s1 owns the String
    let s2 = s1;                    // ownership MOVES to s2; s1 is invalid
    // println!("{}", s1);          // COMPILE ERROR: value used after move
    println!("{}", s2);             // ok — s2 is the owner now
}

O que acontece na memória:

ANTES do move:                    DEPOIS do move:
  Stack          Heap               Stack          Heap
  ┌────────┐   ┌───────┐           ┌────────┐
  │ s1     │   │ hello │           │ s1 XXX │  (invalidado)
  │ ptr ───┼──►│       │           └────────┘
  │ len: 5 │   └───────┘           ┌────────┐   ┌───────┐
  │ cap: 5 │                       │ s2     │   │ hello │
  └────────┘                       │ ptr ───┼──►│       │
                                   │ len: 5 │   └───────┘
                                   │ cap: 5 │
                                   └────────┘

Nenhuma cópia dos dados no heap. Apenas stack metadata foi copiada. Zero-cost.

Ownership em funções

fn take_ownership(s: String) {
    println!("Recebi: {}", s);
} // s é dropped aqui — memória liberada

fn main() {
    let msg = String::from("hello");
    take_ownership(msg);
    // println!("{}", msg); // COMPILE ERROR: msg foi moved
}

Copy e Clone

Tipos na stack implementam Copy — atribuição faz cópia bit-a-bit (barata):

let x: i32 = 42;
let y = x;              // CÓPIA — x continua válido
println!("{} {}", x, y); // "42 42"

Tipos Copy: i32, f64, bool, char, tuples de Copy types, arrays de Copy types.

Para heap types, cópia explícita com Clone:

let s1 = String::from("hello");
let s2 = s1.clone();           // deep copy — nova alocação no heap
println!("{} {}", s1, s2);     // ambos válidos

Borrowing e References

Borrowing empresta uma referência ao valor sem transferir ownership:

fn calculate_length(s: &String) -> usize {
    s.len()
} // s sai de escopo, mas o valor NÃO é dropped (é apenas referência)

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // emprestamos referência
    println!("'{}' tem {} bytes", s1, len); // s1 continua válido
}

Regras do Borrow Checker

Regra 1: OU múltiplas &T OU exatamente uma &mut T — nunca ambos. Regra 2: Referências devem sempre ser válidas (no dangling).

// COMPILA: múltiplas referências imutáveis
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// NÃO COMPILA: &mut coexistindo com &
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;     // ERRO: não pode ter &mut enquanto &s existe
println!("{}", r1);
// COMPILA: NLL (Non-Lexical Lifetimes) — &s termina antes de &mut
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);   // último uso de r1 — borrow termina aqui
let r2 = &mut s;      // ok — nenhum borrow imutável ativo
r2.push_str(" world");

Slices

&str é um fat pointer (ponteiro + comprimento) — a forma idiomática de passar strings:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' { return &s[..i]; }
    }
    &s[..]
}

Lifetimes

Lifetimes informam ao compilador por quanto tempo uma referência é válida.

// NÃO COMPILA: compilador não sabe qual lifetime retornar
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// Solução: anotar com lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Lifetime Elision Rules

O compilador infere lifetimes automaticamente com três regras:

  1. Cada parâmetro referência recebe seu próprio lifetime
  2. Se há exatamente um input lifetime, ele é atribuído a todos os outputs
  3. Se um parâmetro é &self/&mut self, o lifetime de self é atribuído aos outputs

Lifetimes em Structs

struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn announce(&self, msg: &str) -> &str {
        println!("Atenção: {}", msg);
        self.text // lifetime de &self (regra 3)
    }
}

'static indica que a referência vive por toda a duração do programa — string literals são 'static:

let s: &'static str = "eu vivo para sempre no binário";

Tipos: Structs, Enums, Pattern Matching

Structs

struct User {
    id: u64, name: String, email: String, active: bool,
}

impl User {
    fn new(id: u64, name: String, email: String) -> Self {
        Self { id, name, email, active: true }
    }
    fn display_name(&self) -> &str { &self.name }
    fn deactivate(&mut self) { self.active = false; }
}

Enums (Algebraic Data Types)

Cada variante pode carregar dados diferentes. O match é exhaustive:

enum OrderStatus {
    Pending,
    Processing { estimated_minutes: u32 },
    Shipped(String),        // tracking code
    Delivered { at: String },
    Cancelled { reason: String },
}

fn handle_order(status: &OrderStatus) {
    match status {
        OrderStatus::Pending => println!("Aguardando pagamento"),
        OrderStatus::Processing { estimated_minutes } =>
            println!("ETA: {} min", estimated_minutes),
        OrderStatus::Shipped(tracking) =>
            println!("Rastreio: {}", tracking),
        OrderStatus::Delivered { at } => println!("Entregue em {}", at),
        OrderStatus::Cancelled { reason } => println!("Cancelado: {}", reason),
    }
}

Option e Result — sem null

// Option<T>: Some(T) | None — substitui null
// Result<T, E>: Ok(T) | Err(E) — error handling explícito

fn find_user(id: u64) -> Option<User> {
    if id == 1 {
        Some(User::new(1, "Alice".into(), "alice@example.com".into()))
    } else {
        None
    }
}

// if let — quando só interessa um caso
if let Some(user) = find_user(1) {
    println!("Nome: {}", user.display_name());
}

Traits

Traits são interfaces com superpoderes — implementações default, generic bounds, dispatch estático ou dinâmico.

trait Summary {
    fn summarize_author(&self) -> String;
    // Implementação default
    fn summarize(&self) -> String {
        format!("(Leia mais de {}...)", self.summarize_author())
    }
}

struct Article { title: String, author: String, content: String }

impl Summary for Article {
    fn summarize_author(&self) -> String { self.author.clone() }
    fn summarize(&self) -> String {
        format!("{} por {}", self.title, self.author)
    }
}

Trait Bounds e Generics

// Trait bound explícito
fn notify<T: Summary + std::fmt::Display>(item: &T) {
    println!("Breaking: {}", item.summarize());
}

// impl Trait (syntactic sugar)
fn notify_simple(item: &impl Summary) {
    println!("{}", item.summarize());
}

// where clause — legível com múltiplos bounds
fn process<T, U>(t: &T, u: &U) -> String
where T: Summary + Clone, U: std::fmt::Display + std::fmt::Debug {
    format!("{} — {:?}", t.summarize(), u)
}

Static vs Dynamic Dispatch

// Static — monomorphization (zero overhead, mais code bloat)
fn print_summary(item: &impl Summary) { println!("{}", item.summarize()); }

// Dynamic — vtable (indireção, mas permite coleções heterogêneas)
fn print_summary_dyn(item: &dyn Summary) { println!("{}", item.summarize()); }

fn get_summaries() -> Vec<Box<dyn Summary>> {
    vec![Box::new(article1), Box::new(article2)]
}

Derive Macros e Orphan Rule

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
struct Point { x: i32, y: i32 }

Orphan rule: só pode implementar trait se você é dono do trait ou do tipo. Workaround: newtype pattern:

struct UserList(Vec<User>);
impl std::fmt::Display for UserList { /* ... */ }

Error Handling

panic! vs Result

panic!: bugs, estados impossíveis. Result<T, E>: erros esperados (I/O, rede, validação).

O operador ?

fn read_username() -> Result<String, io::Error> {
    let content = fs::read_to_string("config.txt")?; // propaga Err
    Ok(content.lines().next().unwrap_or("anonymous").to_string())
}

thiserror (libraries) + anyhow (applications)

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("user {id} not found")]
    NotFound { id: i64 },
    #[error("unauthorized")]
    Unauthorized,
}
use anyhow::{Context, Result};

async fn fetch_user(id: i64) -> Result<User> {
    let user = db::find_user(id).await
        .context("falha ao buscar usuário no banco")?;
    user.ok_or_else(|| anyhow::anyhow!("usuário {} não encontrado", id))
}

Padrão idiomático: thiserror para libraries (tipos específicos para match), anyhow para binaries (mensagem + backtrace).


Concorrência Segura

Rust garante ausência de data races em compile time via Send e Sync:

  • Send: tipo pode ser transferido para outra thread
  • Sync: tipo pode ser compartilhado entre threads via &T

O compilador verifica automaticamente. Tipos não-Send/Sync: Rc<T>, *mut T, Cell<T>.

Arc e Mutex

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles { handle.join().unwrap(); }
    println!("Resultado: {}", *counter.lock().unwrap()); // 10
}

RwLock e Channels

// RwLock — múltiplos readers OU um writer
let config = Arc::new(RwLock::new(AppConfig::default()));
let val = config.read().unwrap();   // múltiplas threads simultaneamente
let mut val = config.write().unwrap(); // exclusivo
// Channels — message passing (mpsc)
let (tx, rx) = mpsc::channel();
for i in 0..5 {
    let tx = tx.clone();
    thread::spawn(move || { tx.send(format!("msg {}", i)).unwrap(); });
}
drop(tx);
for msg in rx { println!("Recebido: {}", msg); }

Rayon: paralelismo data-parallel

use rayon::prelude::*;

let numbers: Vec<u64> = (0..1_000_000).collect();
let sum: u64 = numbers.par_iter().map(|&n| n * n).sum(); // paralelo automático

Exemplo: web scraper concorrente

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let urls = vec!["https://example.com/1", "https://example.com/2", "https://example.com/3"];
    let results = Arc::new(Mutex::new(Vec::new()));
    let mut handles = vec![];

    for url in urls {
        let results = Arc::clone(&results);
        let url = url.to_string();
        handles.push(thread::spawn(move || {
            let result = scrape_url(&url);
            results.lock().unwrap().push((url, result));
        }));
    }

    for h in handles { h.join().unwrap(); }
    for (url, res) in results.lock().unwrap().iter() {
        match res {
            Ok(c) => println!("{}: {} bytes", url, c.len()),
            Err(e) => eprintln!("{}: ERRO — {}", url, e),
        }
    }
}

Async Rust

Concorrência cooperativa para I/O-bound workloads via Futures — state machines geradas pelo compilador.

Future Trait

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
enum Poll<T> { Ready(T), Pending }

Futures são lazy — não executam até serem polled. O runtime (Tokio) chama poll quando podem progredir.

async/await e State Machines

async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;     // estado 1 → 2
    let body = response.text().await?;            // estado 2 → 3
    Ok(body)                                      // estado 3 (final)
}

O compilador gera uma state machine — cada .await é um estado. Zero-cost: sem alocação heap, sem boxing.

Tokio Runtime

#[tokio::main]
async fn main() {
    let handle1 = tokio::spawn(async { fetch_data("https://api.example.com/users").await });
    let handle2 = tokio::spawn(async { fetch_data("https://api.example.com/posts").await });
    let (users, posts) = tokio::join!(handle1, handle2);
}

Patterns: select!, Streams

use tokio::select;
use tokio::time::{sleep, Duration};

async fn with_timeout() {
    select! {
        result = fetch_data("https://slow-api.com") => println!("{:?}", result),
        _ = sleep(Duration::from_secs(5)) => println!("Timeout"),
    }
}
use tokio_stream::StreamExt;

async fn process_stream() {
    let mut stream = tokio_stream::iter(vec![1, 2, 3, 4, 5])
        .map(|n| n * 2).filter(|n| *n > 4);
    while let Some(value) = stream.next().await {
        println!("Valor: {}", value);
    }
}

Memory Layout

Stack vs Heap

Stack (rápido, LIFO, tamanho fixo)     Heap (dinâmico, alocado em runtime)
┌─────────────────────────┐
│ x: i32 = 42    [4B]    │
│ ptr ────────────────────┼────► ┌──────────────┐
│ len: 5                  │      │ h e l l o    │
│ cap: 5                  │      └──────────────┘
└─────────────────────────┘

Box, Rc, Arc

let boxed: Box<i32> = Box::new(42);       // heap, um owner

use std::rc::Rc;
let a = Rc::new("shared".to_string());
let b = Rc::clone(&a);                     // ref count (single-thread)

use std::sync::Arc;
let a = Arc::new(vec![1, 2, 3]);
let b = Arc::clone(&a);                    // atomic ref count (multi-thread)

Interior Mutability

use std::cell::{Cell, RefCell};

let cell = Cell::new(42);
cell.set(100);                              // mutação sem &mut

let ref_cell = RefCell::new(vec![1, 2, 3]);
ref_cell.borrow_mut().push(4);             // borrow check em RUNTIME

Layout de tipos comuns:

String/Vec<T>: [ptr | len | cap]  (24 bytes na stack, dados no heap)
Box<T>:        [ptr]              (8 bytes na stack, T no heap)
&str:          [ptr | len]        (16 bytes, fat pointer)

Cargo e Ecosystem

Cargo.toml

[package]
name = "my-api"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tracing = "0.1"
anyhow = "1"
thiserror = "1"

[profile.release]
lto = true
codegen-units = 1
strip = true

Crates essenciais

Serialização   │ serde, serde_json        HTTP server  │ axum, actix-web
HTTP client    │ reqwest                   Async        │ tokio
Database       │ sqlx, diesel, sea-orm     Logging      │ tracing
CLI            │ clap                      Errors       │ anyhow, thiserror
Auth           │ jsonwebtoken, argon2      Config       │ config, dotenvy

Rust para Web: Axum

Axum é o framework web do ecossistema Tokio — built on tower (middleware) e hyper (HTTP).

Hello World

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

REST API completa

use axum::{extract::{Path, Query, State, Json}, routing::{get, post}, Router, http::StatusCode};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

type AppState = Arc<RwLock<Vec<User>>>;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct User { id: u64, name: String, email: String }

#[derive(Deserialize)]
struct CreateUser { name: String, email: String }

async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
    Json(state.read().await.clone())
}

async fn get_user(
    State(state): State<AppState>, Path(id): Path<u64>,
) -> Result<Json<User>, StatusCode> {
    state.read().await.iter().find(|u| u.id == id)
        .cloned().map(Json).ok_or(StatusCode::NOT_FOUND)
}

async fn create_user(
    State(state): State<AppState>, Json(input): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    let mut users = state.write().await;
    let user = User { id: users.len() as u64 + 1, name: input.name, email: input.email };
    users.push(user.clone());
    (StatusCode::CREATED, Json(user))
}

#[tokio::main]
async fn main() {
    let state: AppState = Arc::new(RwLock::new(Vec::new()));
    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/{id}", get(get_user))
        .with_state(state);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Middleware e Error Handling

use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/users", get(list_users))
    .layer(TraceLayer::new_for_http())
    .layer(CorsLayer::permissive());
use axum::response::{IntoResponse, Response};

enum ApiError { NotFound(String), Internal(anyhow::Error) }

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        match self {
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg).into_response(),
            ApiError::Internal(err) => {
                tracing::error!("{:?}", err);
                (StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response()
            }
        }
    }
}

Interop

FFI com C

extern "C" {
    fn abs(input: i32) -> i32;
}
fn main() { unsafe { println!("abs(-5) = {}", abs(-5)); } }

// Expor para C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b }

WebAssembly

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n { 0 => 0, 1 => 1, _ => fibonacci(n - 1) + fibonacci(n - 2) }
}
wasm-pack build --target web
# Output: .wasm + JavaScript glue code

Casos de uso: Cloudflare Workers, Figma, funções CPU-intensive no frontend.


Comparação com Go, Java e C++

Critério           │ Rust          │ Go            │ Java          │ C++
───────────────────┼───────────────┼───────────────┼───────────────┼───────────────
Memory model       │ Ownership     │ GC            │ GC (JVM)      │ Manual
Memory safety      │ Compile-time  │ Runtime (GC)  │ Runtime (GC)  │ Nenhuma *
Null safety        │ Option<T>     │ nil (unsafe)  │ Optional      │ nullptr
Concurrency        │ Send/Sync     │ Goroutines    │ Virtual Thrs  │ Manual
Error handling     │ Result<T,E>   │ error return  │ Exceptions    │ Exceptions
Compile time       │ Lento         │ Muito rápido  │ Médio         │ Muito lento
Ecosystem maturity │ Crescendo     │ Maduro        │ Muito maduro  │ Muito maduro
Learning curve     │ Alta          │ Baixa         │ Média         │ Muito alta

Escolha Rust quando performance é hard requirement, memory safety é crítica, ou não pode ter GC pauses. Escolha Go quando quer produtividade alta com boa performance, microserviços, ou concorrência como prioridade. Escolha Java quando precisa de ecossistema enterprise maduro ou o time já tem expertise JVM.


Exercícios

1. Ownership e Borrowing

Implemente longest_word que recebe &str e retorna a palavra mais longa como &str:

fn longest_word(text: &str) -> &str { todo!() }

#[test]
fn test_longest_word() {
    assert_eq!(longest_word("the quick brown fox"), "quick");
    assert_eq!(longest_word("a bb ccc dd"), "ccc");
}

2. Enums e Pattern Matching

Modele um sistema de pagamentos com enum para estados e transições válidas:

enum PaymentStatus {
    Pending,
    Authorized { auth_code: String },
    Captured { amount_cents: u64 },
    Refunded { reason: String },
    Failed { error: String },
}

impl PaymentStatus {
    fn can_capture(&self) -> bool { todo!() }
    fn capture(self, amount_cents: u64) -> Result<Self, String> { todo!() }
    fn refund(self, reason: String) -> Result<Self, String> { todo!() }
}

3. Traits e Generics

Implemente Repository<T> genérico com implementação in-memory usando HashMap:

trait Repository<T> {
    fn find_by_id(&self, id: u64) -> Option<&T>;
    fn save(&mut self, id: u64, entity: T);
    fn delete(&mut self, id: u64) -> Option<T>;
    fn count(&self) -> usize;
}

4. Concorrência com threads

Download simulado de N URLs em paralelo com Arc<Mutex<Vec<_>>>. Colete resultados e imprima tempo total.

5. Async com Tokio

Converta o exercício 4 para tokio::spawn + tokio::join!. Compare ergonomia threads vs async.

6. Axum API

API REST com Axum para TODO list: GET /todos, POST /todos, PUT /todos/{id}, DELETE /todos/{id} com Arc<RwLock<Vec<Todo>>>.


Referências