Compilação Condicional em Rust: cfg, Features e Targets

Aprenda a usar compilação condicional em Rust com #[cfg], cfg!, Cargo features e targets para criar código multiplataforma e modular com segurança.

Introdução

Uma das maiores forças do Rust é a capacidade de escrever código que funciona em múltiplas plataformas sem sacrificar performance ou segurança. No centro dessa capacidade está a compilação condicional — o mecanismo que permite incluir ou excluir código durante a compilação com base no sistema operacional, arquitetura, features habilitadas ou flags personalizadas.

Diferente da abordagem do C/C++ com #ifdef e o pré-processador textual, o Rust integra a compilação condicional diretamente no sistema de tipos e no compilador, garantindo que o código condicional seja verificado sintaticamente mesmo quando não está sendo compilado. Isso elimina uma classe inteira de bugs comuns em projetos C/C++ — para uma comparação detalhada entre as linguagens, confira nosso artigo Rust vs C++.

Neste guia, vamos explorar desde os fundamentos do atributo #[cfg()] até padrões avançados de design com Cargo features, passando por exemplos práticos de código multiplataforma.

O Atributo #[cfg()] — Fundamentos

O atributo #[cfg()] é a ferramenta principal para compilação condicional em Rust. Ele pode ser aplicado a funções, módulos, structs, impls, expressões e praticamente qualquer item da linguagem.

Condições por Sistema Operacional

#[cfg(target_os = "linux")]
fn obter_diretorio_config() -> &'static str {
    "~/.config/meu_app"
}

#[cfg(target_os = "windows")]
fn obter_diretorio_config() -> &'static str {
    "%APPDATA%\\meu_app"
}

#[cfg(target_os = "macos")]
fn obter_diretorio_config() -> &'static str {
    "~/Library/Application Support/meu_app"
}

Condições por Arquitetura

#[cfg(target_arch = "x86_64")]
fn usar_simd_avx2(dados: &[f32]) -> Vec<f32> {
    // Implementação otimizada com AVX2
    dados.iter().map(|x| x * 2.0).collect()
}

#[cfg(target_arch = "aarch64")]
fn usar_simd_neon(dados: &[f32]) -> Vec<f32> {
    // Implementação otimizada com NEON (ARM)
    dados.iter().map(|x| x * 2.0).collect()
}

Operadores Lógicos

O #[cfg()] suporta combinações com all(), any() e not():

// Somente em Linux ou macOS (sistemas Unix-like)
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn usar_unix_sockets() {
    println!("Unix sockets disponíveis!");
}

// Apenas em modo debug E em plataforma 64-bit
#[cfg(all(debug_assertions, target_pointer_width = "64"))]
fn debug_avancado() {
    println!("Debug avançado ativo em plataforma 64-bit");
}

// Em qualquer plataforma EXCETO Windows
#[cfg(not(target_os = "windows"))]
fn usar_sinais_posix() {
    println!("Sinais POSIX disponíveis");
}

A Macro cfg! — Verificação em Runtime

Enquanto #[cfg()] remove código durante a compilação, a macro cfg!() retorna um bool em tempo de execução, permitindo branching condicional sem remover código:

fn main() {
    if cfg!(target_os = "linux") {
        println!("Rodando no Linux");
    } else if cfg!(target_os = "windows") {
        println!("Rodando no Windows");
    } else {
        println!("Rodando em outro SO");
    }

    // Útil para logging condicional
    if cfg!(debug_assertions) {
        println!("Modo debug: informações extras ativas");
    }
}

A diferença fundamental: com #[cfg()], o código excluído nem é compilado. Com cfg!(), todo o código é compilado, mas o compilador pode otimizar os branches mortos. Use #[cfg()] quando o código condicional não compilaria na outra plataforma (por exemplo, chamadas a APIs específicas do SO).

O Sistema de Features do Cargo

O sistema de features do Cargo é onde a compilação condicional realmente brilha em projetos Rust. Features permitem habilitar ou desabilitar funcionalidades opcionais em bibliotecas e aplicações.

Definindo Features no Cargo.toml

[package]
name = "minha-lib"
version = "0.1.0"
edition = "2021"

[features]
default = ["json"]      # Features ativadas por padrão
json = ["dep:serde", "dep:serde_json"]
yaml = ["dep:serde", "dep:serde_yaml"]
logging = ["dep:tracing"]
full = ["json", "yaml", "logging"]  # Feature que agrupa outras

[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
tracing = { version = "0.1", optional = true }

Para um guia completo sobre serialização com serde, veja nosso Guia Completo do Serde.

Usando Features no Código

// Módulo só existe quando a feature "json" está ativa
#[cfg(feature = "json")]
pub mod json {
    use serde::{Serialize, Deserialize};

    pub fn serializar<T: Serialize>(valor: &T) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(valor)
    }

    pub fn deserializar<T: for<'de> Deserialize<'de>>(json: &str) -> Result<T, serde_json::Error> {
        serde_json::from_str(json)
    }
}

#[cfg(feature = "logging")]
pub fn inicializar_logging() {
    tracing_subscriber::fmt::init();
}

#[cfg(not(feature = "logging"))]
pub fn inicializar_logging() {
    // No-op quando logging não está habilitado
}

Habilitando e Desabilitando Features

# Compilar com features padrão
cargo build

# Compilar sem features padrão
cargo build --no-default-features

# Compilar com features específicas
cargo build --features "json,logging"

# Compilar com todas as features
cargo build --all-features

Essa integração com o sistema de ferramentas do Cargo torna o gerenciamento de features extremamente ergonômico.

cfg_attr — Atributos Condicionais

O cfg_attr permite aplicar outros atributos condicionalmente:

// Derivar Serialize apenas quando a feature "json" está ativa
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct Config {
    pub nome: String,
    pub versao: u32,
    pub ativo: bool,
}

// Aplicar lint condicional
#[cfg_attr(not(debug_assertions), deny(unused_variables))]
fn processar_dados(entrada: &str) -> String {
    let debug_info = "info extra"; // Warning apenas em release
    entrada.to_uppercase()
}

Compilação Cross-Platform na Prática

Um exemplo real de código multiplataforma que utiliza compilação condicional para I/O de arquivos:

use std::path::PathBuf;

pub fn diretorio_dados() -> PathBuf {
    #[cfg(target_os = "linux")]
    {
        let home = std::env::var("HOME").unwrap_or_default();
        PathBuf::from(home).join(".local/share/meu_app")
    }

    #[cfg(target_os = "macos")]
    {
        let home = std::env::var("HOME").unwrap_or_default();
        PathBuf::from(home).join("Library/Application Support/meu_app")
    }

    #[cfg(target_os = "windows")]
    {
        let appdata = std::env::var("APPDATA").unwrap_or_default();
        PathBuf::from(appdata).join("meu_app")
    }
}

// Dependências específicas por target no Cargo.toml:
// [target.'cfg(windows)'.dependencies]
// windows = { version = "0.52", features = ["Win32_Foundation"] }
//
// [target.'cfg(unix)'.dependencies]
// nix = { version = "0.28", features = ["signal"] }

Para projetos embarcados com Rust, a compilação condicional é ainda mais importante, permitindo suportar diferentes microcontroladores com o mesmo codebase.

Custom Flags com –cfg

Você pode definir flags personalizadas durante a compilação:

RUSTFLAGS='--cfg minha_flag' cargo build
#[cfg(minha_flag)]
fn funcionalidade_experimental() {
    println!("Feature experimental ativa!");
}

Isso é especialmente útil em pipelines de CI/CD para habilitar comportamentos específicos de teste ou deploy.

Testando com Features

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_funcionalidade_basica() {
        // Teste que sempre roda
        assert!(true);
    }

    #[test]
    #[cfg(feature = "json")]
    fn test_serializacao_json() {
        let config = Config {
            nome: "teste".to_string(),
            versao: 1,
            ativo: true,
        };
        let json = json::serializar(&config).unwrap();
        assert!(json.contains("teste"));
    }
}

Para um guia completo sobre testes em Rust, consulte nosso tutorial sobre testes.

Boas Práticas para Compilação Condicional

  1. Features devem ser aditivas: Habilitar uma feature nunca deve quebrar funcionalidades existentes. Cada feature adiciona capacidade — nunca remove.

  2. Evite o “cfg hell”: Muitas condições #[cfg()] aninhadas tornam o código difícil de manter. Considere abstrair plataformas em módulos separados.

  3. Teste todas as combinações: Use cargo test --all-features e cargo test --no-default-features no seu CI para garantir que todas as combinações compilam.

  4. Documente suas features: Liste features disponíveis e o que cada uma habilita no README e na documentação.

  5. Use all-features no docs.rs: Adicione ao Cargo.toml:

    [package.metadata.docs.rs]
    all-features = true
    

Comparação: Rust cfg vs C #ifdef

AspectoRust #[cfg()]C #ifdef
Verificação sintática✅ Mesmo quando excluído❌ Texto é ignorado
Integração com tipos✅ Total❌ Pré-processador textual
Composabilidadeall(), any(), not()⚠️ &&, ||, ! limitados
Segurança de memória✅ Garantida pelo compilador❌ Nenhuma
Integração com build system✅ Cargo features nativas⚠️ CMake/Make manual

Conclusão

A compilação condicional em Rust é significativamente mais segura e ergonômica do que em C/C++, graças à integração com o sistema de tipos e o Cargo. Dominar #[cfg()], features e targets é essencial para escrever código Rust multiplataforma e modular.

Se você está começando com Rust, confira nosso guia Como Aprender Rust em 2026. Para quem quer explorar paralelismo e performance, não deixe de ler nosso artigo sobre Rayon: Paralelismo de Dados sem Medo.

Para entender como compilação condicional se encaixa no sistema de módulos do Rust, veja nosso artigo sobre o Sistema de Módulos.

Veja Também

Se você programa em outras linguagens, veja como a compilação condicional se compara: Go usa build tags, enquanto Zig oferece comptime para metaprogramação em tempo de compilação. Já Python resolve isso em runtime com verificações de plataforma.