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