Rust

O Ecossistema Rust — Cargo, crates.io e as Bibliotecas Essenciais Já leu

14 min de leitura

O Ecossistema Rust — Cargo, crates.io e as Bibliotecas Essenciais
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

 

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/
Comentários

Mais em Rust

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos
Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos

Rust — Artigo #46 Rust e Machine Learning — Inferência, ONNX e Aceleração de...

Rust para CLI — Construindo Ferramentas de Linha de Comando Profissionais com clap
Rust para CLI — Construindo Ferramentas de Linha de Comando Profissionais com clap

Rust — Artigo #40 Rust para CLI — Construindo Ferramentas de Linha de Comando...

Coleções — Vec, HashMap e HashSet na Prática
Coleções — Vec, HashMap e HashSet na Prática

&nbsp; At&eacute; agora trabalhamos com dados de tamanho fixo &mdash; arrays...