Pacotes, crates e módulos com Cargo

1. Introdução ao Sistema de Organização de Código em Rust

Rust oferece um sistema sofisticado para organizar código em diferentes níveis de abstração: pacotes, crates e módulos. Essa hierarquia permite que desenvolvedores estruturem projetos de qualquer tamanho de forma limpa e gerenciável.

  • Pacote: É a unidade mais externa, contendo um ou mais crates. Cada pacote possui um arquivo Cargo.toml que define metadados e dependências.
  • Crate: Representa uma unidade de compilação. Pode ser binário (executável) ou de biblioteca (reutilizável).
  • Módulo: Organiza o código dentro de um crate, controlando visibilidade e escopo.

O Cargo, gerenciador de pacotes do Rust, automatiza a criação e gerenciamento dessa estrutura, permitindo foco no desenvolvimento.

2. Estrutura de um Pacote com Cargo

Para criar um novo pacote, utilize o comando:

cargo new meu_projeto

Isso gera a seguinte estrutura:

meu_projeto/
├── Cargo.toml
└── src/
    └── main.rs

O arquivo Cargo.toml contém metadados e dependências:

[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"

[dependencies]

Por padrão, cargo new cria um crate binário com src/main.rs. Para criar uma biblioteca, use cargo new --lib:

cargo new minha_biblioteca --lib

Isso gera src/lib.rs em vez de main.rs.

3. Trabalhando com Crates Binários e de Biblioteca

Um pacote pode conter múltiplos crates binários. Para adicionar um segundo binário, crie o diretório src/bin/:

// src/bin/ferramenta.rs
fn main() {
    println!("Ferramenta executada!");
}

Para executar: cargo run --bin ferramenta.

Crates de biblioteca expõem funcionalidades com pub:

// src/lib.rs
pub fn saudacao() -> String {
    String::from("Olá do crate de biblioteca!")
}

É possível combinar ambos no mesmo pacote:

// src/main.rs
use meu_projeto::saudacao;

fn main() {
    println!("{}", saudacao());
}

4. Definindo e Organizando Módulos

Módulos podem ser declarados diretamente no arquivo raiz:

// src/lib.rs
mod utils;

pub fn processar() {
    utils::ajudante();
}
// src/utils.rs
pub fn ajudante() {
    println!("Ajudante executado");
}

Para submódulos, use pastas:

// src/utils/mod.rs
pub mod formatacao;

pub fn ajudante() {
    formatacao::formatar();
}
// src/utils/formatacao.rs
pub fn formatar() {
    println!("Formatando...");
}

A abordagem moderna (Rust 2018+) permite arquivos sem mod.rs:

src/
├── utils/
│   └── formatacao.rs
└── utils.rs

5. Sistema de Visibilidade e Controle de Acesso

O modificador pub controla a visibilidade:

// src/lib.rs
mod interno {
    pub fn publica() {}
    fn privada() {}

    pub(crate) fn visivel_no_crate() {}

    pub(super) fn visivel_no_modulo_pai() {}
}
  • pub: visível para todos
  • pub(crate): visível apenas dentro do crate atual
  • pub(super): visível apenas no módulo pai
  • Sem modificador: privado ao módulo atual

Exemplo prático:

mod autenticacao {
    pub struct Usuario {
        pub nome: String,
        senha: String, // privado
    }

    impl Usuario {
        pub fn novo(nome: String, senha: String) -> Self {
            Usuario { nome, senha }
        }

        pub fn verificar_senha(&self, tentativa: &str) -> bool {
            self.senha == tentativa
        }
    }
}

6. Navegando e Importando com use e as

O comando use simplifica o acesso a itens:

use std::collections::HashMap;

let mut mapa = HashMap::new();

Renomeando com as:

use std::collections::HashMap as Mapa;

let mut mapa = Mapa::new();

Re-exportação com pub use:

// src/lib.rs
mod formatacao {
    pub fn texto_negrito(s: &str) -> String {
        format!("**{}**", s)
    }
}

pub use formatacao::texto_negrito;

Isso permite que consumidores usem texto_negrito diretamente, sem conhecer o módulo interno.

7. Gerenciando Dependências Externas com Cargo

Para adicionar uma dependência externa, edite o Cargo.toml:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
reqwest = "0.11"
tokio = { version = "1", features = ["full"] }

Dependências de desenvolvimento (apenas para testes):

[dev-dependencies]
criterion = "0.5"

O Cargo usa versionamento semântico (SemVer). Por exemplo, "0.11" permite qualquer versão 0.11.x mas não 0.12.0.

Exemplo prático de uso:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Dados {
    nome: String,
    valor: i32,
}

8. Boas Práticas e Padrões de Projeto

Para projetos grandes, organize módulos por funcionalidade:

src/
├── main.rs
├── api/
│   ├── mod.rs
│   ├── handlers.rs
│   └── modelos.rs
├── db/
│   ├── mod.rs
│   ├── conexao.rs
│   └── queries.rs
└── config/
    ├── mod.rs
    └── ambiente.rs

Separe responsabilidades em múltiplos crates:

meu_projeto/
├── Cargo.toml (workspace)
├── core/
│   ├── Cargo.toml
│   └── src/lib.rs
├── web/
│   ├── Cargo.toml
│   └── src/lib.rs
└── cli/
    ├── Cargo.toml
    └── src/main.rs

Para evitar dependências circulares, use inversão de dependência: crie um crate de interface (trait) que ambos os lados implementam.

Conclusão

Dominar pacotes, crates e módulos é essencial para escrever código Rust limpo, reutilizável e de fácil manutenção. O sistema de módulos com visibilidade controlada, combinado com o gerenciamento eficiente de dependências do Cargo, oferece uma base sólida para projetos de qualquer escala.

Referências