Crates e workspaces
Nesta aula, você aprenderá a diferença entre crates binários e de biblioteca, como criar e gerenciar workspaces no Rust e como organizar projetos grandes de forma modular e eficiente, seguindo boas práticas da comunidade.
Nesta aula, vamos explorar um dos pilares da organização de projetos em Rust: os crates e os workspaces. Um crate é a unidade fundamental de compilação e distribuição no ecossistema Rust, podendo ser um binário executável ou uma biblioteca reutilizável. Já um workspace é uma coleção de crates que compartilham um alvo de compilação comum, permitindo gerenciar projetos maiores com múltiplos componentes de forma coesa.
Dominar esses conceitos é essencial para qualquer projeto Rust que cresça além de um simples arquivo de exemplo. Nesta aula, veremos como criar crates binários e de biblioteca, como configurar workspaces e como estruturar projetos grandes de maneira modular, seguindo as melhores práticas da comunidade. Ao final, você terá as ferramentas necessárias para organizar seus próprios projetos de forma profissional.
Crate binário vs biblioteca
Um crate é a menor unidade de compilação no Rust. Ele pode ser de dois tipos: binário (bin) ou biblioteca (lib). Um crate binário produz um executável, enquanto um crate de biblioteca produz um artefato que pode ser usado por outros crates, seja como dependência ou como módulo interno.
Quando você cria um novo projeto com cargo new, o Cargo gera um crate binário por padrão, com um arquivo src/main.rs. Para criar uma biblioteca, você usa cargo new --lib, que gera um src/lib.rs. A diferença fundamental está no ponto de entrada: o binário tem uma função main, enquanto a biblioteca expõe itens públicos para serem usados por outros crates.
É possível também ter ambos em um mesmo crate, se você adicionar um arquivo src/main.rs a um crate de biblioteca ou vice-versa. Isso é comum em projetos que possuem uma biblioteca principal e um executável utilitário que a utiliza.
Vejamos um exemplo de estrutura de um crate binário:
// src/main.rs
fn main() {
println!("Hello, world!");
}
E um crate de biblioteca:
// src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
Para usar a biblioteca em um binário, você pode adicioná-la como dependência no Cargo.toml do binário, ou, se estiver no mesmo workspace, referenciá-la diretamente.
No Cargo.toml de um crate, a seção [lib] ou [[bin]] pode ser usada para configurar o alvo. Por exemplo:
# Cargo.toml para um crate com biblioteca e binário
[package]
name = "my_crate"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_lib"
path = "src/lib.rs"
[[bin]]
name = "my_bin"
path = "src/main.rs"
Essa flexibilidade permite organizar código compartilhado em uma biblioteca e ter executáveis que a utilizam.
Workspaces
Workspaces são uma maneira de gerenciar múltiplos crates relacionados em um único diretório raiz. Eles permitem compartilhar um Cargo.lock e um alvo de build comum, facilitando a coordenação entre crates que dependem uns dos outros.
Um workspace é definido em um Cargo.toml na raiz, com uma seção [workspace] que lista os membros. Os membros podem ser crates binários ou de biblioteca, cada um com seu próprio Cargo.toml e código-fonte.
Para criar um workspace, você pode estruturar um diretório assim:
my_workspace/
Cargo.toml # workspace raiz
crate1/
Cargo.toml
src/
main.rs
crate2/
Cargo.toml
src/
lib.rs
No Cargo.toml raiz, você especifica os membros:
[workspace]
members = ["crate1", "crate2"]
resolver = "2"
O campo resolver é opcional, mas recomendado para a edição 2021. Você também pode usar exclude para listar diretórios que não são membros.
Dentro de um workspace, os crates podem referenciar uns aos outros como dependências de caminho, usando a sintaxe path = "../crate2" no Cargo.toml do crate dependente. Isso permite que o Cargo resolva as dependências localmente, sem precisar publicar os crates em um registro.
Por exemplo, em crate1/Cargo.toml:
[dependencies]
crate2 = { path = "../crate2" }
Um comando como cargo build na raiz do workspace compilará todos os membros, e o Cargo.lock unificado garante que as versões das dependências sejam consistentes entre todos os crates.
Workspaces são ideais para projetos que consistem em vários crates independentes, como uma biblioteca principal e um conjunto de ferramentas de linha de comando que a utilizam.
Organizando projetos grandes
Quando um projeto cresce, a organização se torna crucial. Em Rust, a combinação de crates e workspaces oferece uma estrutura poderosa para modularizar o código e gerenciar dependências.
Uma abordagem comum é dividir o projeto em uma biblioteca principal (que contém a lógica de negócios) e vários binários (que fornecem interfaces de linha de comando, servidores web, etc.). Cada binário pode ser um crate separado dentro do mesmo workspace, dependendo da biblioteca.
Outra estratégia é usar crates de biblioteca para separar grandes áreas de funcionalidade, como parsing, networking, ou utilitários. Isso facilita a testabilidade, a reutilização e a manutenção, pois cada crate pode ser desenvolvido e testado isoladamente.
Além disso, você pode usar features para controlar quais partes do código são compiladas. Por exemplo, você pode ter uma biblioteca com features opcionais que incluem funcionalidades extras, e os binários podem ativar essas features conforme necessário. Isso é especialmente útil para reduzir o tempo de compilação e o tamanho do binário final.
Aqui está um exemplo de estrutura de um projeto grande com workspace:
my_project/
Cargo.toml
crates/
core/ # biblioteca principal
Cargo.toml
src/
lib.rs
modules/
...
cli/ # binário de interface de linha de comando
Cargo.toml
src/
main.rs
server/ # binário de servidor web
Cargo.toml
src/
main.rs
No Cargo.toml raiz:
[workspace]
members = ["crates/core", "crates/cli", "crates/server"]
resolver = "2"
O crate cli pode depender de core:
# crates/cli/Cargo.toml
[dependencies]
my_core = { path = "../core" }
Essa organização permite que cada componente seja desenvolvido de forma independente, mas todos compartilham a mesma infraestrutura de build e versionamento de dependências.
Uma boa prática é manter os crates pequenos e focados, com responsabilidades claras. Evite crates gigantes que misturam muitas funcionalidades, pois isso dificulta a manutenção e os testes.
Outra prática recomendada é usar nomes consistentes para os crates e módulos, e documentar a arquitetura do projeto em um arquivo README ou na documentação do workspace.
Boas práticas e observações finais
Ao trabalhar com crates e workspaces, siga estas boas práticas:
- Use workspaces para projetos com múltiplos crates que precisam ser desenvolvidos juntos.
- Mantenha a biblioteca principal o mais pura possível, sem dependências de I/O, para facilitar testes.
- Use features para controlar funcionalidades opcionais e reduzir o tempo de compilação.
- Documente a finalidade de cada crate no seu README ou em comentários no
Cargo.toml. - Prefira crates pequenos e focados a um crate monolítico.
- Use
cargo test --workspacepara testar todos os crates de uma vez.
Lembre-se de que o Cargo é uma ferramenta poderosa que simplifica o gerenciamento de dependências e builds. Dominar crates e workspaces é um passo importante para se tornar um desenvolvedor Rust proficiente.
Exercícios
- Crie um novo crate binário chamado
helloe um crate de biblioteca chamadomath_utilsdentro de um workspace chamadomeu_projeto. No crate de biblioteca, implemente uma funçãosoma(a: i32, b: i32) -> i32. No binário, use a biblioteca para somar dois números e imprimir o resultado. - Explique a diferença entre um crate binário e um crate de biblioteca, e dê um exemplo de quando você usaria cada um.
- O que é um workspace no Rust? Quais são as vantagens de usar um workspace em um projeto com múltiplos crates?
- No contexto de um workspace, como você adiciona uma dependência de um crate para outro? Mostre o trecho de
Cargo.tomlnecessário. - Descreva uma estratégia para organizar um projeto grande em Rust usando workspaces. Inclua um exemplo de estrutura de diretórios e explique o papel de cada crate.
Estrutura do projeto:
meu_projeto/
Cargo.toml
hello/
Cargo.toml
src/main.rs
math_utils/
Cargo.toml
src/lib.rs
meu_projeto/Cargo.toml:
[workspace]
members = ["hello", "math_utils"]
resolver = "2"
math_utils/src/lib.rs:
pub fn soma(a: i32, b: i32) -> i32 {
a + b
}
hello/Cargo.toml:
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
[dependencies]
math_utils = { path = "../math_utils" }
hello/src/main.rs:
use math_utils::soma;
fn main() {
let resultado = soma(5, 3);
println!("A soma é: {}", resultado);
}
Um crate binário gera um executável, possui uma função main e é usado para criar programas que podem ser executados diretamente. Um crate de biblioteca não tem main; ele expõe funções, structs e outros itens públicos para serem reutilizados por outros crates. Exemplo: um crate binário para um jogo, e um crate de biblioteca para a lógica do jogo que poderia ser usada em outros contextos (como testes ou outros executáveis).
Um workspace é uma coleção de crates que compartilham um Cargo.lock e um diretório de build comum, definido em um Cargo.toml raiz. Vantagens: build e teste unificados, resolução de dependências consistente, fácil coordenação entre crates, e permite usar dependências de caminho sem publicar em um registro.
No Cargo.toml do crate que depende, adicione:
[dependencies]
nome_do_crate = { path = "../caminho/para/o/crate" }
Por exemplo, se o crate cli depende do crate core localizado em ../core:
[dependencies]
my_core = { path = "../core" }
Uma estratégia comum é ter uma biblioteca principal (core) que contém toda a lógica de negócios e é independente de interfaces externas. Em seguida, ter crates binários para diferentes interfaces (CLI, servidor web, etc.) que dependem da biblioteca. Exemplo:
meu_projeto/
Cargo.toml
crates/
core/ # lógica principal, testes unitários
cli/ # interface de linha de comando
web/ # servidor web
O crate core expõe funções e tipos; cli e web usam core para fornecer interfaces. Isso permite desenvolver e testar a lógica isoladamente, e também reutilizar a biblioteca em outros projetos.