Como diferenciar debug de release em Rust? Analisando cfg com exemplos reais
\n\nVocê já escreveu código em Rust e pensou: "Seria bom adicionar logs detalhados no modo debug e remover tudo desnecessário no release"? Ou talvez quisesse incluir asserts apenas durante o desenvolvimento, mas desativá-los na compilação final? Se sim, você veio ao lugar certo. Hoje vamos explorar como usar a magia do cfg para controlar o comportamento do código dependendo do tipo de compilação. Isso não é apenas teoria seca — vamos percorrer o caminho desde uma simples flag até uma solução pronta para produção.
O tópico não é aleatório: no Stack Overflow, a pergunta "How to check release / debug builds using cfg in Rust?" acumulou milhares de visualizações. Isso significa que muitos precisam disso. E vamos explicar detalhadamente.
\n\nPor que isso é importante? Dois mundos — duas abordagens
\n\nEm Rust, existem dois perfis principais de compilação: debug (com a flag --debug ou apenas cargo build) e release (cargo build --release). Eles diferem não apenas nas otimizações, mas também no comportamento de algumas verificações embutidas.
Por exemplo:
\n- \n
- debug: lento, mas com verificações de limites de arrays, símbolos de depuração e asserts. \n
- release: rápido, mas sem verificações extras — tudo em prol do desempenho. \n
E se você precisar que, na versão debug, o programa registre cada ação no log, e no release apenas erros críticos? Ou que um endpoint de teste esteja disponível apenas localmente? Para isso existe o cfg.
Ferramentas principais: cfg!() e #[cfg()]
\n\nEm Rust, existem duas maneiras de verificar o tipo de compilação:
\n- \n
- Macro
cfg!()— executada durante a compilação, retornabool. Usada dentro de funções. \n - Atributo
#[cfg()]— inclui ou exclui completamente um bloco de código da compilação. \n
Ambos usam as mesmas chaves. A mais popular para nossa tarefa é debug_assertions. Ela está ativa apenas na compilação debug. Existe também a inversa — not(debug_assertions) para release.
Exemplo 1: Verificação simples dentro de uma função
\n\nVamos começar com a macro cfg!(). Suponha que temos uma função que calcula algo complexo. No debug, queremos verificar os dados de entrada:
fn calculate_something(x: i32) -> i32 {\n // Verificação apenas na compilação debug\n if cfg!(debug_assertions) {\n println!("[DEBUG] calculate_something chamada com x = {}", x);\n assert!(x > 0, "x deve ser positivo!");\n }\n\n // Lógica principal\n x * 2\n}\n\nfn main() {\n let result = calculate_something(10);\n println!("Resultado: {}", result);\n}\n\n\nO que acontece?
\n- \n
- Com
cargo build(debug) — você verá o log e, se x ≤ 0, o programa falhará com um assert. \n - Com
cargo build --release— a linha comcfg!(debug_assertions)se tornafalse, e o compilador descarta esse bloco como código morto. Sem overhead! \n
Exemplo 2: Exclusão completa de código com #[cfg()]
\n\nÀs vezes, é necessário não apenas pular a execução, mas remover completamente uma função ou módulo do binário. Para isso, usamos o atributo:
\n\n// Este módulo será compilado APENAS na compilação debug\n#[cfg(debug_assertions)]\nmod debug_helpers {\n pub fn print_state(state: &str) {\n println!("[DEBUG] Estado atual: {}", state);\n }\n\n pub fn validate_data(data: &[u8]) -> bool {\n // Verificação custosa, desnecessária no release\n data.len() < 1000\n }\n}\n\n// Este módulo é apenas para release\n#[cfg(not(debug_assertions))]\nmod release_helpers {\n pub fn print_state(_state: &str) {\n // No release, não fazemos nada — economizamos CPU\n }\n\n pub fn validate_data(_data: &[u8]) -