Cargo features e compilação condicional
1. Introdução às Features no Cargo
Features no Cargo são um mecanismo poderoso para habilitar compilação condicional em projetos Rust. Elas permitem que você ofereça funcionalidades opcionais, reduza o tamanho das dependências e forneça diferentes backends ou comportamentos sem alterar a API pública da sua biblioteca.
A estrutura básica no Cargo.toml utiliza a seção [features]:
[package]
name = "meu-pacote"
version = "0.1.0"
[features]
default = ["std"]
std = []
serde_support = ["dep:serde"]
Features podem ser:
- Features simples: apenas um nome, sem dependências
- Features dependentes: ativam outras features
- Features implícitas: criadas automaticamente quando uma dependência é marcada como opcional
2. Declarando e Ativando Features
Declaração de Features
No Cargo.toml, declaramos features com a seguinte sintaxe:
[features]
default = ["std", "serde"]
std = []
async = ["tokio"]
serde = ["dep:serde", "dep:serde_json"]
experimental = []
Ativação via Linha de Comando
# Ativar features específicas
cargo build --features "serde,async"
# Ativar todas as features
cargo build --all-features
# Build sem features padrão
cargo build --no-default-features
Ativação em Dependências
[dependencies]
meu-pacote = { version = "0.1", default-features = false, features = ["serde"] }
3. Compilação Condicional com cfg
A macro #[cfg()] é a espinha dorsal da compilação condicional em Rust:
// Feature simples
#[cfg(feature = "std")]
fn usar_std() {
println!("Usando std");
}
// Módulo condicional
#[cfg(feature = "experimental")]
mod experimental {
pub fn nova_funcionalidade() {
println!("Funcionalidade experimental ativa");
}
}
// Struct condicional
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Config {
pub nome: String,
pub valor: i32,
}
// Import condicional
#[cfg(feature = "async")]
use tokio::time::sleep;
Expressões cfg Complexas
// any() - pelo menos uma condição verdadeira
#[cfg(any(feature = "std", feature = "alloc"))]
fn precisa_de_alocacao() {}
// all() - todas as condições verdadeiras
#[cfg(all(feature = "serde", feature = "json"))]
fn serializar_json() {}
// not() - negação
#[cfg(not(feature = "std"))]
fn sem_std() {}
// Combinações complexas
#[cfg(all(
feature = "async",
any(target_os = "linux", target_os = "macos")
))]
fn async_para_unix() {}
cfg!() para Condicionais em Tempo de Execução
fn main() {
if cfg!(feature = "std") {
println!("Compilado com suporte std");
} else {
println!("Modo no_std ativo");
}
// Útil para código que precisa de comportamento diferente
// mas não pode usar #[cfg] diretamente
let resultado = if cfg!(feature = "otimizado") {
algoritmo_rapido()
} else {
algoritmo_seguro()
};
}
4. Features e Dependências Opcionais
Dependências opcionais criam features implícitas automaticamente:
[dependencies]
serde = { version = "1.0", optional = true }
tokio = { version = "1.0", optional = true, features = ["full"] }
[features]
default = ["serde"]
full = ["serde", "tokio"]
Exemplo Prático: Backend de Banco de Dados
// Cargo.toml
[dependencies]
rusqlite = { version = "0.31", optional = true }
tokio-postgres = { version = "0.7", optional = true }
diesel = { version = "2.1", optional = true }
[features]
default = ["sqlite"]
sqlite = ["rusqlite"]
postgres = ["tokio-postgres"]
diesel_support = ["diesel"]
// lib.rs
#[cfg(feature = "sqlite")]
mod backend_sqlite {
use rusqlite::Connection;
pub fn conectar(path: &str) -> Result<Connection, rusqlite::Error> {
Connection::open(path)
}
}
#[cfg(feature = "postgres")]
mod backend_postgres {
use tokio_postgres::{Client, NoTls};
pub async fn conectar(conn_str: &str) -> Result<Client, tokio_postgres::Error> {
let (client, connection) = tokio_postgres::connect(conn_str, NoTls).await?;
tokio::spawn(connection);
Ok(client)
}
}
#[cfg(feature = "diesel_support")]
mod backend_diesel {
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
pub fn conectar(path: &str) -> Result<SqliteConnection, diesel::ConnectionError> {
SqliteConnection::establish(path)
}
}
// Função principal que usa cfg para decidir qual backend usar
#[cfg(any(feature = "sqlite", feature = "postgres", feature = "diesel_support"))]
pub fn conectar_banco() {
#[cfg(feature = "sqlite")]
{
let conn = backend_sqlite::conectar("meu_banco.db");
println!("Conectado ao SQLite");
}
#[cfg(feature = "postgres")]
{
let conn = backend_postgres::conectar("host=localhost dbname=mydb");
println!("Conectado ao PostgreSQL");
}
}
5. Boas Práticas e Convenções
Nomenclatura
[features]
# Use snake_case
suporte_serializacao = ["dep:serde"]
backend_otimizado = []
Documentação de Features
/// Módulo para serialização JSON
///
/// Este módulo só está disponível quando a feature "serde" está ativa.
#[cfg(feature = "serde")]
#[doc(cfg(feature = "serde"))]
pub mod serializacao {
// ...
}
/// Função que requer a feature "experimental"
#[cfg(feature = "experimental")]
#[doc(cfg(feature = "experimental"))]
pub fn funcionalidade_experimental() {
// ...
}
Evitando Features Mutuamente Exclusivas
[features]
# Use dep: para features que ativam dependências
serializacao_json = ["dep:serde_json"]
serializacao_yaml = ["dep:serde_yaml"]
# Para features mutuamente exclusivas, documente claramente
# e use cfg com not() para prevenir uso simultâneo
#[cfg(all(feature = "serializacao_json", feature = "serializacao_yaml"))]
compile_error!("As features serializacao_json e serializacao_yaml são mutuamente exclusivas");
6. Testes e Features
Testes Condicionais
#[cfg(test)]
mod tests {
#[test]
fn teste_basico() {
assert_eq!(2 + 2, 4);
}
#[cfg(feature = "serde")]
#[test]
fn teste_serializacao() {
let config = super::Config { nome: "teste".into(), valor: 42 };
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("teste"));
}
}
Testes de Integração com Features
// tests/integracao.rs
use meu_pacote::*;
#[cfg(feature = "sqlite")]
#[test]
fn test_conexao_sqlite() {
let conn = conectar_banco();
assert!(conn.is_ok());
}
# Executar testes com features específicas
cargo test --features "sqlite"
cargo test --all-features
7. Publicação e Compatibilidade
Versionamento Semântico
- Adicionar feature: versão minor (não breaking)
- Remover feature: versão major (breaking)
- Adicionar dependência a feature existente: versão minor
- Remover dependência de feature: versão major
Verificando Features
# Verificar build com diferentes combinações
cargo check --no-default-features
cargo check --features "serde,async"
cargo check --all-features
# Listar features disponíveis
cargo metadata --no-deps | jq '.packages[0].features'
Features em Workspaces
# workspace/Cargo.toml
[workspace]
members = ["crate_a", "crate_b"]
[workspace.dependencies]
serde = "1.0"
# crate_a/Cargo.toml
[features]
default = ["serde"]
serde = ["dep:serde"]
O sistema de features do Cargo é uma ferramenta essencial para criar bibliotecas flexíveis e modulares em Rust. Dominar seu uso permite oferecer diferentes níveis de funcionalidade sem comprometer a simplicidade para usuários que precisam apenas do básico.
Referências
- Documentação Oficial do Cargo - Features — Guia completo sobre features, dependências opcionais e compilação condicional
- The Rust Reference - Conditional Compilation — Documentação detalhada sobre a macro
cfge expressões condicionais - Rust by Example - Conditional Compilation — Exemplos práticos de uso de
#[cfg()]ecfg!() - Cargo Book - Features Examples — Exemplos reais de como grandes projetos organizam suas features
- Rust API Guidelines - Cargo Features — Boas práticas para design de features em bibliotecas Rust
- The Cargo Book - Workspaces — Gerenciamento de features em workspaces e dependências compartilhadas