Truques para depurar código em Rust mais rápido

Depurar código em Rust pode ser desafiador devido ao sistema de ownership, lifetimes e concorrência. No entanto, com as técnicas certas, é possível reduzir drasticamente o tempo gasto identificando e corrigindo bugs. Este artigo apresenta truques práticos para acelerar seu fluxo de depuração, desde a configuração do ambiente até ferramentas de análise estática.

1. Configuração do Ambiente de Depuração

A escolha do perfil de compilação impacta diretamente a experiência de debug. O perfil debug (padrão) desabilita otimizações e inclui símbolos de depuração, enquanto o perfil release otimiza o código, dificultando o rastreamento.

No Cargo.toml, configure explicitamente:

[profile.dev]
debug = true
opt-level = 0

Para stepping eficiente, use rust-gdb (wrapper do GDB) ou rust-lldb (LLDB configurado para Rust). Exemplo de uso:

rust-gdb target/debug/meu_programa
(gdb) break main
(gdb) run
(gdb) next

O rust-gdb já carrega automaticamente os pretty-printers para tipos Rust, facilitando a inspeção de Vec, String e Option.

2. Logging Estratégico com log e env_logger

Logs bem estruturados substituem múltiplas execuções com println!. Adicione ao Cargo.toml:

[dependencies]
log = "0.4"
env_logger = "0.10"

Configure no main.rs:

fn main() {
    env_logger::init();
    log::info!("Iniciando aplicação");
    processar_dados();
}

fn processar_dados() {
    log::debug!("Processando lote de 100 itens");
    for i in 0..100 {
        log::trace!("Item {} processado", i);
    }
}

Execute com filtros seletivos:

RUST_LOG=meu_crate=debug cargo run
RUST_LOG=warn cargo run  # Apenas warnings e erros

Para logs condicionais em loops críticos, crie uma macro:

macro_rules! log_cond {
    ($cond:expr, $($arg:tt)*) => {
        if $cond {
            log::debug!($($arg)*);
        }
    };
}

for i in 0..10000 {
    log_cond!(i % 1000 == 0, "Progresso: {}%", i / 100);
}

3. Uso de dbg! e eprintln! para Inspeção Rápida

O macro dbg! é superior ao println! para debug rápido porque imprime o nome da variável, valor e localização no código:

fn calcular_media(valores: &[f64]) -> f64 {
    let soma: f64 = valores.iter().sum();
    dbg!(soma);
    let total = valores.len() as f64;
    dbg!(total);
    soma / total
}

Saída:

[src/main.rs:4] soma = 150.0
[src/main.rs:6] total = 5.0

Use eprintln! em vez de println! para evitar bufferização e mistura com saída padrão:

eprintln!("DEBUG: valor inesperado: {}", valor);

Para remover dbg! em produção, envolva com #[cfg(debug_assertions)]:

#[cfg(debug_assertions)]
dbg!(resultado);

4. Debug com Ferramentas de Análise Estática

cargo clippy detecta bugs comuns antes da execução. Execute regularmente:

cargo clippy -- -D warnings

Configure warnings críticos como erro no Cargo.toml:

[lints.clippy]
unwrap_used = "deny"
panic = "deny"

O rust-analyzer no VS Code ou Neovim fornece hints de tipo, erros em tempo real e sugestões de correção. Ative a opção "check on save" para feedback imediato:

"rust-analyzer.checkOnSave": true

5. Depuração de Erros de Memória e Concorrência

Para vazamentos de memória e uso após liberação, use valgrind:

cargo install cargo-valgrind
cargo valgrind

Para data races, execute testes com thread única:

cargo test -- --test-threads=1

Para simulação avançada de concorrência, use loom:

[dependencies]
loom = "0.7"

Exemplo de teste com loom:

#[test]
fn teste_concorrente() {
    loom::model(|| {
        let valor = loom::cell::UnsafeCell::new(0);
        let t1 = loom::thread::spawn(|| {
            *valor.get() = 1;
        });
        let t2 = loom::thread::spawn(|| {
            *valor.get() = 2;
        });
        t1.join().unwrap();
        t2.join().unwrap();
    });
}

Para rastrear deadlocks em Mutex, adicione logging:

use std::sync::{Mutex, Arc};
use log::info;

let dados = Arc::new(Mutex::new(Vec::new()));
{
    let mut guard = dados.lock().unwrap();
    info!("Lock adquirido em thread {:?}", std::thread::current().id());
    guard.push(42);
} // Lock liberado aqui

6. Técnicas de Teste para Isolar Bugs

Crie testes unitários que reproduzam cenários específicos:

#[test]
fn test_calculo_media() {
    let valores = vec![10.0, 20.0, 30.0];
    assert_eq!(calcular_media(&valores), 20.0);
}

#[test]
fn test_lista_vazia() {
    let valores: Vec<f64> = vec![];
    assert!(calcular_media(&valores).is_nan());
}

Para ver logs durante os testes:

cargo test -- --nocapture

Use #[cfg(test)] para mockar dependências:

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

    struct MockDatabase;
    impl Database for MockDatabase {
        fn buscar_usuario(&self, id: u32) -> Option<String> {
            if id == 1 {
                Some("Alice".to_string())
            } else {
                None
            }
        }
    }

    #[test]
    fn test_com_mock() {
        let db = MockDatabase;
        assert_eq!(processar_usuario(&db, 1), "Alice");
    }
}

7. Otimização do Fluxo de Depuração com Scripts

Crie aliases no shell para comandos frequentes:

alias cb='cargo build 2>&1 | head -20'
alias ct='cargo test -- --nocapture'
alias cw='cargo watch -x run'

Use cargo watch para reexecução automática:

cargo install cargo-watch
cargo watch -x "test -- --nocapture"

Script personalizado para rust-lldb com breakpoints:

#!/bin/bash
# save as debug.sh
cargo build
rust-lldb -o "breakpoint set --name main" -o "run" -o "next" target/debug/meu_programa

Execute com ./debug.sh para iniciar a depuração imediatamente.

Conclusão

Combinando configuração adequada do ambiente, logging estratégico, ferramentas de análise estática e testes isolados, você pode reduzir significativamente o tempo de depuração em Rust. Lembre-se de que a prevenção é mais eficiente que a correção — invista em clippy e testes desde o início do projeto.

Referências