Nenhuma linguagem existe no vácuo. O que torna Rust produtivo além da linguagem em si é seu ecossistema — o gerenciador de pacotes, o repositório central de bibliotecas, as ferramentas de desenvolvimento e as crates que a comunidade construiu ao longo de uma década. Neste artigo fazemos um tour pelo ecossistema Rust: o que existe, por que importa, e o que você deve conhecer para trabalhar com projetos reais.
Cargo — muito além de um gerenciador de pacotes
O Cargo é o centro gravitacional do ecossistema Rust. Ele gerencia dependências, compila código, executa testes, gera documentação e muito mais. Se você usou npm, pip ou Maven, vai reconhecer a categoria — mas o Cargo é notavelmente bem projetado.
Comandos essenciais
# Criar novo projeto binário
cargo new meu_projeto
# Criar nova biblioteca
cargo new minha_lib --lib
# Compilar (modo debug)
cargo build
# Compilar (modo release — otimizado)
cargo build --release
# Compilar e executar
cargo run
# Executar testes
cargo test
# Verificar erros sem compilar (mais rápido)
cargo check
# Gerar documentação
cargo doc --open
# Atualizar dependências
cargo update
# Adicionar dependência (Cargo 1.62+)
cargo add serde --features derive
# Remover dependência
cargo remove serde
# Formatar código
cargo fmt
# Verificar erros de estilo e bugs potenciais
cargo clippy
# Publicar crate no crates.io
cargo publish
Perfis de compilação
O Cargo.toml permite configurar perfis de compilação:
[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# Perfil debug — padrão para cargo build
[profile.dev]
opt-level = 0 # sem otimização — compilação rápida
debug = true # símbolos de debug incluídos
# Perfil release — cargo build --release
[profile.release]
opt-level = 3 # otimização máxima
debug = false
lto = true # Link Time Optimization
codegen-units = 1 # melhor otimização, compilação mais lenta
strip = true # remove símbolos — binário menor
# Perfil customizado
[profile.profiling]
inherits = "release"
debug = true # release com símbolos para profiling
Workspaces — múltiplos crates no mesmo projeto
Para projetos grandes com múltiplos crates relacionados:
meu_workspace/
├── Cargo.toml ← workspace root
├── servidor/
│ ├── Cargo.toml
│ └── src/main.rs
├── cliente/
│ ├── Cargo.toml
│ └── src/main.rs
└── compartilhado/
├── Cargo.toml
└── src/lib.rs
Cargo.toml da raiz:
[workspace]
members = [
"servidor",
"cliente",
"compartilhado",
]
# Dependências compartilhadas entre todos os membros
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
servidor/Cargo.toml:
[package]
name = "servidor"
version = "0.1.0"
edition = "2021"
[dependencies]
compartilhado = { path = "../compartilhado" }
serde.workspace = true # herda da definição do workspace
tokio.workspace = true
Workspaces permitem compilar todos os crates com cargo build na raiz, compartilhando o cache de compilação.
crates.io — o repositório central
crates.io é onde a comunidade Rust publica e distribui bibliotecas. No momento da escrita deste artigo, o repositório tem mais de 150.000 crates publicadas. A qualidade varia — algumas são mantidas por grandes empresas, outras por indivíduos.
Para avaliar uma crate antes de usar:
- Downloads e tendência — crates muito usadas tendem a ser mais maduras
- Última atualização — crates sem atualização há anos podem ser abandonadas
- Issues abertas e pull requests — indicadores de saúde do projeto
- docs.rs — documentação gerada automaticamente para toda crate publicada
- lib.rs — alternativa ao crates.io com melhor interface de descoberta
Ferramentas de desenvolvimento essenciais
Antes de explorar as bibliotecas, veja as ferramentas que todo desenvolvedor Rust deve ter instaladas:
# Formatador — estilo oficial Rust
rustup component add rustfmt
cargo fmt
# Linter — detecta padrões problemáticos
rustup component add clippy
cargo clippy
# Servidor de linguagem — para editores
rustup component add rust-analyzer
# Expandir macros (diagnóstico)
cargo install cargo-expand
# Auditoria de segurança das dependências
cargo install cargo-audit
cargo audit
# Cobertura de testes
cargo install cargo-tarpaulin
cargo tarpaulin
# Benchmark
cargo install cargo-criterion
# Watch — recompila ao salvar arquivos
cargo install cargo-watch
cargo watch -x run
# Tamanho do binário por dependência
cargo install cargo-bloat
cargo bloat --release
cargo clippy — seu segundo compilador
Clippy é o linter oficial do Rust. Ele detecta padrões problemáticos que o compilador aceita mas que provavelmente são erros ou código não idiomático:
fn main() {
let v = vec![1, 2, 3];
// Clippy avisa: use .is_empty() em vez de .len() == 0
if v.len() == 0 {
println!("vazio");
}
// Clippy avisa: loop desnecessário — use iter diretamente
let mut soma = 0;
for i in 0..v.len() {
soma += v[i];
}
// Clippy avisa: clone() desnecessário em tipos Copy
let x = 5i32;
let y = x.clone();
}
Após cargo clippy:
warning: length comparison to zero
--> src/main.rs:4:8
|
4 | if v.len() == 0 {
| ^^^^^^^^^^^^ help: use `is_empty()`: `v.is_empty()`
warning: the loop variable `i` is only used to index `v`
--> src/main.rs:10:14
|
10 | for i in 0..v.len() {
| ^^^^^^^^^^ help: use `.iter()` or `.iter_mut()`
warning: using `clone` on type `i32` which implements the `Copy` trait
Execute cargo clippy -- -D warnings em CI para tratar avisos como erros.
As crates mais importantes
Aqui estão as bibliotecas que você encontrará em praticamente todo projeto Rust sério, organizadas por categoria.
Serialização — serde
A crate mais baixada do ecossistema. Vimos no artigo de macros — serde com serde_json é o padrão para JSON em Rust:
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};
use serde_json;
#[derive(Debug, Serialize, Deserialize)]
struct Produto {
id: u32,
nome: String,
preco: f64,
#[serde(default)]
disponivel: bool,
}
fn main() -> Result<(), serde_json::Error> {
let produto = Produto {
id: 1,
nome: String::from("Teclado Mecânico"),
preco: 350.00,
disponivel: true,
};
let json = serde_json::to_string_pretty(&produto)?;
println!("{json}");
let de_volta: Produto = serde_json::from_str(&json)?;
println!("{:?}", de_volta);
Ok(())
}
Serde suporta dezenas de formatos além de JSON: TOML, YAML, MessagePack, CBOR, CSV e mais — basta trocar a crate de formato.
Tratamento de erros — thiserror e anyhow
Para bibliotecas, use thiserror — define tipos de erro customizados com mínimo boilerplate:
thiserror = "1"
use thiserror::Error;
#[derive(Debug, Error)]
enum ErroApp {
#[error("Arquivo não encontrado: {caminho}")]
ArquivoNaoEncontrado { caminho: String },
#[error("Erro de IO: {0}")]
Io(#[from] std::io::Error),
#[error("Dados inválidos: {0}")]
DadosInvalidos(String),
#[error("Falha na conexão após {tentativas} tentativas")]
ConexaoFalhou { tentativas: u32 },
}
fn ler_config(caminho: &str) -> Result<String, ErroApp> {
std::fs::read_to_string(caminho).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ErroApp::ArquivoNaoEncontrado {
caminho: caminho.to_string()
}
} else {
ErroApp::Io(e)
}
})
}
fn main() {
match ler_config("config.toml") {
Ok(conteudo) => println!("{conteudo}"),
Err(e) => eprintln!("Erro: {e}"),
}
}
Para aplicações (não bibliotecas), use anyhow — aceita qualquer erro sem definir tipos:
anyhow = "1"
use anyhow::{Context, Result};
fn main() -> Result<()> {
let conteudo = std::fs::read_to_string("config.toml")
.context("Falha ao ler config.toml")?;
let linhas = conteudo.lines().count();
println!("Config tem {linhas} linhas");
Ok(())
}
HTTP assíncrono — reqwest
O cliente HTTP mais popular para Rust:
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
use reqwest::Client;
use serde::Deserialize;
use anyhow::Result;
#[derive(Debug, Deserialize)]
struct IpInfo {
ip: String,
city: String,
country: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new();
let info: IpInfo = client
.get("https://ipapi.co/json/")
.send()
.await?
.json()
.await?;
println!("IP: {}", info.ip);
println!("Cidade: {}", info.city);
println!("País: {}", info.country);
Ok(())
}
Logging — tracing
O sistema de logging moderno para Rust:
tracing = "0.1"
tracing-subscriber = "0.3"
use tracing::{info, warn, error, debug, instrument};
#[instrument]
fn processar(id: u32) -> Result<String, String> {
debug!("Iniciando processamento");
if id == 0 {
warn!("ID zero recebido — comportamento inesperado");
}
if id > 1000 {
error!("ID muito alto: {id}");
return Err(format!("ID inválido: {id}"));
}
info!("Processamento concluído para id={id}");
Ok(format!("resultado_{id}"))
}
fn main() {
tracing_subscriber::fmt::init();
info!("Aplicação iniciando");
for id in [1, 0, 500, 1500] {
match processar(id) {
Ok(r) => info!("Sucesso: {r}"),
Err(e) => error!("Falha: {e}"),
}
}
}
Banco de dados — sqlx
O driver de banco de dados mais idiomático em Rust — queries verificadas em tempo de compilação:
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros"] }
tokio = { version = "1", features = ["full"] }
use sqlx::{SqlitePool, Row};
#[derive(Debug)]
struct Usuario {
id: i64,
nome: String,
email: String,
}
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
let pool = SqlitePool::connect("sqlite::memory:").await?;
// Criar tabela
sqlx::query(
"CREATE TABLE usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)"
)
.execute(&pool)
.await?;
// Inserir dados
let nomes = [("Ana Silva", "ana@ex.com"), ("Carlos", "carlos@ex.com")];
for (nome, email) in nomes {
sqlx::query("INSERT INTO usuarios (nome, email) VALUES (?, ?)")
.bind(nome)
.bind(email)
.execute(&pool)
.await?;
}
// Buscar dados
let rows = sqlx::query("SELECT id, nome, email FROM usuarios")
.fetch_all(&pool)
.await?;
println!("── Usuários ──");
for row in &rows {
let usuario = Usuario {
id: row.get("id"),
nome: row.get("nome"),
email: row.get("email"),
};
println!("{:?}", usuario);
}
Ok(())
}
Linha de comando — clap
A crate padrão para criar CLIs em Rust:
clap = { version = "4", features = ["derive"] }
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "ferramenta")]
#[command(about = "Uma ferramenta de exemplo", long_about = None)]
struct Cli {
#[command(subcommand)]
comando: Comando,
#[arg(short, long, default_value_t = false)]
verbose: bool,
}
#[derive(Subcommand, Debug)]
enum Comando {
/// Processa um arquivo
Processar {
/// Caminho do arquivo de entrada
#[arg(short, long)]
entrada: String,
/// Caminho do arquivo de saída
#[arg(short, long, default_value = "saida.txt")]
saida: String,
},
/// Lista arquivos disponíveis
Listar {
/// Diretório para listar
#[arg(default_value = ".")]
diretorio: String,
},
}
fn main() {
let cli = Cli::parse();
if cli.verbose {
println!("Modo verbose ativado");
}
match cli.comando {
Comando::Processar { entrada, saida } => {
println!("Processando: {entrada} → {saida}");
}
Comando::Listar { diretorio } => {
println!("Listando: {diretorio}");
}
}
}
$ ferramenta --help
Uma ferramenta de exemplo
Usage: ferramenta [OPTIONS] <COMMAND>
Commands:
processar Processa um arquivo
listar Lista arquivos disponíveis
help Print help
$ ferramenta processar --entrada dados.csv --saida resultado.csv
Processando: dados.csv → resultado.csv
Expressões regulares — regex
regex = "1"
use regex::Regex;
fn main() {
let re_email = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}").unwrap();
let re_cpf = Regex::new(r"d{3}.d{3}.d{3}-d{2}").unwrap();
let textos = [
"Contato: ana@exemplo.com",
"CPF: 123.456.789-09",
"Email inválido: nao-e-email",
"Outro email: carlos@empresa.org.br",
];
for texto in &textos {
if let Some(email) = re_email.find(texto) {
println!("Email encontrado: {}", email.as_str());
}
if let Some(cpf) = re_cpf.find(texto) {
println!("CPF encontrado: {}", cpf.as_str());
}
}
// Capturando grupos
let re_data = Regex::new(r"(d{2})/(d{2})/(d{4})").unwrap();
let data = "Nascimento: 15/03/1990";
if let Some(caps) = re_data.captures(data) {
println!("Dia: {}, Mês: {}, Ano: {}",
&caps[1], &caps[2], &caps[3]);
}
}
Um projeto completo: CLI de gerenciamento de tarefas
Vamos combinar várias crates num projeto coeso — uma CLI de gerenciamento de tarefas com persistência em JSON:
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
chrono = { version = "0.4", features = ["serde"] }
use anyhow::{Context, Result};
use chrono::{DateTime, Local};
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Tarefa {
id: u32,
titulo: String,
concluida: bool,
criada_em: DateTime<Local>,
}
impl Tarefa {
fn nova(id: u32, titulo: String) -> Self {
Tarefa {
id,
titulo,
concluida: false,
criada_em: Local::now(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct ListaTarefas {
tarefas: Vec<Tarefa>,
proximo_id: u32,
}
impl ListaTarefas {
fn adicionar(&mut self, titulo: String) -> &Tarefa {
let id = self.proximo_id + 1;
self.proximo_id = id;
self.tarefas.push(Tarefa::nova(id, titulo));
self.tarefas.last().unwrap()
}
fn concluir(&mut self, id: u32) -> Result<&Tarefa> {
let tarefa = self.tarefas.iter_mut()
.find(|t| t.id == id)
.context(format!("Tarefa #{id} não encontrada"))?;
tarefa.concluida = true;
Ok(tarefa)
}
fn remover(&mut self, id: u32) -> Result<Tarefa> {
let pos = self.tarefas.iter()
.position(|t| t.id == id)
.context(format!("Tarefa #{id} não encontrada"))?;
Ok(self.tarefas.remove(pos))
}
fn listar(&self, apenas_pendentes: bool) -> Vec<&Tarefa> {
self.tarefas.iter()
.filter(|t| !apenas_pendentes || !t.concluida)
.collect()
}
}
fn caminho_arquivo() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".tarefas.json")
}
fn carregar() -> Result<ListaTarefas> {
let caminho = caminho_arquivo();
if !caminho.exists() {
return Ok(ListaTarefas::default());
}
let conteudo = fs::read_to_string(&caminho)
.context("Erro ao ler arquivo de tarefas")?;
serde_json::from_str(&conteudo)
.context("Erro ao parsear tarefas")
}
fn salvar(lista: &ListaTarefas) -> Result<()> {
let json = serde_json::to_string_pretty(lista)?;
fs::write(caminho_arquivo(), json)
.context("Erro ao salvar tarefas")?;
Ok(())
}
#[derive(Parser)]
#[command(name = "tarefas", about = "Gerenciador de tarefas em linha de comando")]
struct Cli {
#[command(subcommand)]
comando: Comando,
}
#[derive(Subcommand)]
enum Comando {
/// Adicionar nova tarefa
Add {
#[arg(help = "Título da tarefa")]
titulo: String,
},
/// Listar tarefas
List {
#[arg(short, long, help = "Mostrar apenas pendentes")]
pendentes: bool,
},
/// Marcar tarefa como concluída
Done {
#[arg(help = "ID da tarefa")]
id: u32,
},
/// Remover tarefa
Remove {
#[arg(help = "ID da tarefa")]
id: u32,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
let mut lista = carregar()?;
match cli.comando {
Comando::Add { titulo } => {
let tarefa = lista.adicionar(titulo);
println!("✓ Tarefa #{} adicionada: "{}"", tarefa.id, tarefa.titulo);
}
Comando::List { pendentes } => {
let tarefas = lista.listar(pendentes);
if tarefas.is_empty() {
println!("Nenhuma tarefa encontrada.");
} else {
let titulo = if pendentes { "Tarefas Pendentes" } else { "Todas as Tarefas" };
println!("── {titulo} ──");
for t in tarefas {
let status = if t.concluida { "✓" } else { "○" };
let data = t.criada_em.format("%d/%m/%Y");
println!(" [{status}] #{:02} {} ({})", t.id, t.titulo, data);
}
}
}
Comando::Done { id } => {
let tarefa = lista.concluir(id)?;
println!("✓ Tarefa #{} concluída: "{}"", tarefa.id, tarefa.titulo);
}
Comando::Remove { id } => {
let tarefa = lista.remover(id)?;
println!("✓ Tarefa #{} removida: "{}"", tarefa.id, tarefa.titulo);
}
}
salvar(&lista)?;
Ok(())
}
Uso:
$ tarefas add "Estudar Rust"
✓ Tarefa #1 adicionada: "Estudar Rust"
$ tarefas add "Escrever testes"
✓ Tarefa #2 adicionada: "Escrever testes"
$ tarefas list
── Todas as Tarefas ──
[○] #01 Estudar Rust (02/03/2026)
[○] #02 Escrever testes (02/03/2026)
$ tarefas done 1
✓ Tarefa #1 concluída: "Estudar Rust"
$ tarefas list --pendentes
── Tarefas Pendentes ──
[○] #02 Escrever testes (02/03/2026)
O mapa do ecossistema por categoria
Para referência rápida, as crates mais estabelecidas por domínio:
Web (servidor): axum, actix-web, warp
Web (cliente): reqwest, hyper, surf
Banco de dados: sqlx, diesel, sea-orm
Async runtime: tokio, async-std, smol
Serialização: serde, bincode, postcard
CLI: clap, argh, structopt
Logging: tracing, log, env_logger
Erros: thiserror, anyhow, color-eyre
Datas: chrono, time
Paralelismo: rayon, crossbeam
Criptografia: ring, rustls, sha2
Testes: proptest, mockall, criterion
Parsing: nom, pest, chumsky
Embarcados: embedded-hal, rtic, embassy
WebAssembly: wasm-bindgen, yew, leptos
Fontes e leituras recomendadas
- The Cargo Book — guia completo do Cargo — https://doc.rust-lang.org/cargo/
- crates.io — repositório central — https://crates.io
- lib.rs — catálogo alternativo com melhor descoberta — https://lib.rs
- docs.rs — documentação de todas as crates publicadas — https://docs.rs
- "Blessed.rs" — curadoria das melhores crates por categoria — https://blessed.rs/crates
- "Are we web yet?" — estado das crates web em Rust — https://www.arewewebyet.org
- "Are we async yet?" — estado do ecossistema async — https://areweasyncyet.rs
- Rust Clippy Lints — lista completa de verificações do Clippy — https://rust-lang.github.io/rust-clippy/