Checklist de Code Review em Zig: Memória, Erros, Segurança e Performance

Code review em Zig não é só procurar estilo. A linguagem dá controle explícito sobre memória, erros, layout, compilação, integração com C e artefatos finais. Esse controle é ótimo para software de sistemas, CLIs, servidores e bibliotecas de performance, mas também muda o tipo de bug que passa por uma revisão superficial.

Um review bom em Zig responde perguntas concretas: quem é dono dessa memória? O erro libera tudo que já foi alocado? O allocator correto chega até a camada certa? O comptime facilita ou esconde complexidade? O binário foi testado no target que será distribuído? A dependência nova alterou a supply chain?

Este checklist é uma base prática para revisar pull requests em projetos Zig. Use como roteiro, não como burocracia. Para contexto complementar, veja também Clean Code em Zig, Error Handling em Zig, Alocação de Memória, Testes em Zig e Zig Supply Chain.

1. Comece pelo contrato da mudança

Antes de entrar linha por linha, entenda o que a mudança promete:

  • adiciona API pública, comando de CLI, endpoint ou biblioteca interna?
  • muda comportamento compatível ou quebra contrato?
  • toca hot path, parser, rede, arquivo, criptografia ou memória compartilhada?
  • roda em produção, build script, teste ou ferramenta local?
  • afeta targets específicos, como Linux, macOS, Windows, ARM ou WebAssembly?

Em Zig, contexto importa muito. Uma alocação aceitável em comando administrativo pode ser ruim em loop de rede. Um panic tolerável em build tool pode ser inaceitável em biblioteca. Um @ptrCast em camada de interoperabilidade C pode ser necessário; espalhado pelo domínio da aplicação, provavelmente é sinal de design frágil.

2. Gerenciamento de memória

A primeira pergunta de review é propriedade: quem aloca, quem libera e por quanto tempo o dado vive?

const dados = try allocator.alloc(u8, 1024);
defer allocator.free(dados);

const copia = try allocator.dupe(u8, entrada);
errdefer allocator.free(copia);
try registrar(copia);

Verifique:

  • todo alloc, create, dupe ou ArrayList.init tem free, destroy ou deinit correspondente;
  • caminhos de erro usam errdefer quando a função ainda não transferiu propriedade;
  • structs com init têm deinit claro e documentado;
  • funções que retornam memória alocada dizem qual allocator deve liberar;
  • slices retornados não apontam para stack frame encerrado;
  • ArenaAllocator não está escondendo vazamento em processo longo;
  • GeneralPurposeAllocator ou std.testing.allocator é usado em testes para detectar leaks.

Um padrão saudável é nomear propriedade no contrato:

/// Retorna uma string alocada com `allocator`. O chamador deve liberar.
pub fn renderMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
    return try std.fmt.allocPrint(allocator, "Olá, {s}", .{nome});
}

Se o review precisa adivinhar quem libera, o código precisa de ajuste.

3. defer, errdefer e ordem de limpeza

defer e errdefer são fortes, mas ordem importa. Eles executam em ordem inversa de declaração. Em funções com vários recursos, confira se a limpeza acontece no sentido correto.

const file = try std.fs.cwd().openFile(path, .{});
defer file.close();

var buffer = try allocator.alloc(u8, size);
errdefer allocator.free(buffer);

try file.readAll(buffer);
return buffer; // propriedade transferida; errdefer não roda no sucesso

Perguntas de review:

  • defer libera algo que ainda será usado depois?
  • errdefer deveria virar defer porque não há transferência de propriedade?
  • há múltiplos recursos que precisam ser liberados em ordem específica?
  • uma função retorna antes de registrar o defer necessário?
  • um objeto parcialmente inicializado tem errdefer para cada campo já adquirido?

Para inicialização de structs complexas, prefira etapas pequenas e limpeza explícita. O bug clássico é alocar o segundo campo, falhar no terceiro e vazar o primeiro.

4. Tratamento de erros sem engolir falhas

Zig força error unions, mas ainda é possível destruir observabilidade com catch {} ou transformar tudo em erro genérico.

// Fraco: o erro desaparece.
_ = gravarArquivo() catch {};

// Melhor: contexto, propagação ou fallback explícito.
gravarArquivo() catch |err| {
    std.log.err("falha ao gravar relatório {s}: {}", .{ caminho, err });
    return err;
};

Revise:

  • try está no nível certo ou deveria adicionar contexto antes de propagar?
  • catch unreachable é realmente impossível ou só conveniente?
  • erros de input externo viram resposta controlada, não panic;
  • bibliotecas não chamam std.process.exit ou panic para erro recuperável;
  • logs têm contexto suficiente sem expor segredo;
  • error sets públicos são estáveis e compreensíveis.

Em APIs públicas, cuidado com error set enorme que vaza detalhe interno. Em aplicações, cuidado com erro genérico que não ajuda a operar produção.

5. Allocator correto para cada camada

Allocator é decisão arquitetural. Em review, confira se o allocator vem de fora quando a função é reutilizável:

pub fn parseConfig(allocator: std.mem.Allocator, bytes: []const u8) !Config {
    // bom: chamador escolhe arena, GPA, testing allocator etc.
}

Evite criar allocator global escondido em biblioteca. Em servidor, prefira separar:

  • allocator de processo para estruturas de longa vida;
  • arena por request ou por job quando tudo morre junto;
  • buffers reutilizáveis em hot path;
  • std.testing.allocator em testes.

Se a mudança troca ArenaAllocator por alocações individuais, peça justificativa. Se troca alocações individuais por arena, confira se nenhum ponteiro da arena escapa além do ciclo de vida.

6. Slices, ponteiros e aliasing

Muitos bugs de Zig aparecem como slice válido com vida errada. Revise:

  • slice retornado aponta para buffer de chamador ou memória própria?
  • função modifica slice que parece somente leitura?
  • []u8 deveria ser []const u8?
  • ponteiros opcionais (?*T) são checados antes de uso?
  • @ptrCast, @alignCast e @constCast estão isolados e justificados?
  • há suposição de alinhamento ou endianess documentada?

Prefira reduzir área insegura:

const bytes: []const u8 = std.mem.asBytes(&header);

Quando @ptrCast for necessário, deixe o código próximo da validação de tamanho/alinhamento e cubra com teste.

7. comptime e generics legíveis

comptime é uma das melhores partes de Zig, mas review deve separar abstração útil de metaprogramação decorativa.

Pergunte:

  • o comptime reduz duplicação real ou só torna erro mais difícil?
  • mensagens de erro para uso incorreto são claras?
  • @compileError amigável quando o tipo não atende ao contrato?
  • código gerado mantém tamanho de binário aceitável?
  • testes cobrem pelo menos dois tipos/instâncias relevantes?

Exemplo de contrato melhor:

fn Repository(comptime T: type) type {
    if (!@hasDecl(T, "id")) {
        @compileError("Repository(T) exige campo ou decl id");
    }
    return struct { /* ... */ };
}

Se o código usa @typeInfo, revise com calma. Bugs de comptime costumam ser raros, mas quando quebram, a mensagem pode atingir todo consumidor da biblioteca.

8. Interoperabilidade com C

Zig chama C com facilidade. O review precisa tratar essa fronteira como área de risco.

Confira:

  • headers importados por @cImport são estáveis e versionados;
  • tipos C têm conversão explícita para tipos Zig;
  • strings C terminadas em zero são validadas;
  • ownership de ponteiros C está documentado;
  • funções C que retornam código de erro são checadas;
  • biblioteca linkada é a esperada no target final;
  • build.zig não depende de caminho local do autor.

Se um ponteiro vem de C, quem libera? Se Zig passa callback para C, qual é a vida do contexto? Essas duas perguntas pegam muitos problemas.

9. Performance e hot paths

Zig atrai projetos de performance, mas review não deve aceitar micro-otimização sem evidência. Procure primeiro problemas óbvios:

  • alocação dentro de loop apertado;
  • cópia desnecessária de buffers grandes;
  • formatação de string em caminho quente;
  • lookup linear onde mapa ou índice simples resolveria;
  • locks ou atomics sem necessidade;
  • branch complexa em parser crítico;
  • ReleaseFast escondendo comportamento que deveria ser seguro.

Exemplo típico:

var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();

for (items) |item| {
    buffer.clearRetainingCapacity();
    try buffer.appendSlice(item);
    try processar(buffer.items);
}

Se a mudança afirma ganho de performance, peça benchmark reproduzível. O guia de benchmarking em Zig e o material de depuração e profiling ajudam a validar sem chute.

10. Segurança de input, logs e segredos

Revise todo ponto onde dado externo entra: argumentos de CLI, arquivo, rede, variável de ambiente, JSON, header HTTP, path, payload binário ou resposta de processo.

Checklist:

  • tamanhos são limitados antes de alocar;
  • parser diferencia erro de formato, EOF e limite excedido;
  • paths não permitem traversal acidental;
  • logs não imprimem token, senha, cookie, chave ou DSN;
  • mensagens de erro externas não vazam detalhe sensível;
  • dados binários não são tratados como UTF-8 sem validação;
  • valores de ambiente obrigatórios são validados no boot;
  • defaults inseguros não entram em produção.

Para configuração, conecte o review ao guia de segredos e variáveis de ambiente em Zig. Para dependências e releases, conecte ao guia de supply chain.

11. Testes que realmente protegem

Procure teste no mesmo nível do risco:

  • unidade para função pura;
  • teste com std.testing.allocator para memória;
  • fixture para parser;
  • integração para arquivo, rede ou CLI;
  • regressão para bug corrigido;
  • teste por target quando a mudança é multiplataforma.

Um teste útil em Zig costuma verificar também erro e limpeza:

test "parse rejeita entrada grande" {
    const allocator = std.testing.allocator;
    const entrada = try allocator.alloc(u8, 1024 * 1024);
    defer allocator.free(entrada);
    try std.testing.expectError(error.InputTooLarge, parse(allocator, entrada));
}

Se a mudança mexe em memória, rode testes com allocator de teste. Se mexe em compilação cruzada, rode pelo menos build do target relevante. Se mexe em CLI, capture saída e exit code.

12. Build, formatação e CI

Antes de aprovar, procure evidência de comandos básicos:

zig fmt --check src build.zig
zig build test
zig build -Doptimize=ReleaseSafe

Dependendo do projeto, acrescente:

zig build -Dtarget=x86_64-linux-musl
zig build -Dtarget=aarch64-macos
zig build docs

Revise também o build.zig:

  • opções têm nomes claros;
  • targets e optimize vêm de standardTargetOptions e standardOptimizeOption quando apropriado;
  • testes estão ligados ao step padrão esperado;
  • paths não são absolutos da máquina do autor;
  • flags inseguras são comentadas;
  • dependências novas aparecem no build.zig.zon com hash.

13. Documentação e exemplos

Mudança de API sem exemplo vira dívida. Verifique:

  • README ou docs mostram o caminho feliz;
  • exemplo compila com a versão atual;
  • nomes e comentários batem com comportamento real;
  • limitações são explícitas;
  • breaking change aparece em changelog;
  • tutorial não recomenda catch unreachable ou allocator global sem explicar contexto.

Em Zig, exemplos quebrados doem mais porque a linguagem e o tooling ainda evoluem. Um snippet antigo pode confundir iniciante rapidamente.

14. Supply chain da própria mudança

Para PRs que alteram dependências, CI, release ou Docker, faça review operacional:

  • versão do Zig está fixada;
  • build.zig.zon usa URL e hash coerentes;
  • actions não usam latest sem motivo;
  • imagem Docker tem tag ou digest;
  • artefatos de release têm checksum;
  • token de publicação só existe no job de publicação;
  • SBOM ou inventário é atualizado quando exigido;
  • changelog cita dependência ou toolchain nova.

Esse bloco conversa diretamente com GitHub Actions para releases Zig e Zig Supply Chain. Code review não termina no .zig; o build também é código.

15. Checklist rápido para colar no PR

Use este resumo quando precisar revisar rápido:

  • propriedade de memória clara;
  • defer/errdefer cobre sucesso e erro;
  • sem slice apontando para vida encerrada;
  • error handling preserva contexto;
  • allocator vem do chamador quando a função é reutilizável;
  • @ptrCast, @alignCast, @constCast são necessários e isolados;
  • input externo tem limite e validação;
  • logs não expõem segredo;
  • testes cobrem caminho feliz, erro e regressão;
  • zig fmt e zig build test rodaram;
  • build.zig/build.zig.zon não introduzem caminho local ou dependência flutuante;
  • documentação e exemplos foram atualizados;
  • mudança de performance tem benchmark ou justificativa mensurável;
  • release/CI continua reproduzível.

Comparação com Rust e Go

Em Rust, o borrow checker captura parte dos problemas de memória antes do review. Em Go, garbage collector e convenções simples reduzem a superfície de ownership manual. Zig fica em outro ponto: dá controle explícito sem esconder custo. Por isso, o review humano precisa olhar propriedade, lifetime, erro e build com mais disciplina.

Isso não torna Zig “mais perigoso” por definição. Torna a revisão mais parecida com engenharia de sistemas: menos fé no framework, mais clareza no contrato. Para comparar práticas, veja comunidades de Rust no Brasil e Go no Brasil.

Conclusão

Code review em Zig é uma ferramenta de design. Ele confirma se a mudança tem ownership claro, erros previsíveis, testes úteis, build reproduzível e fronteiras seguras com C, sistema operacional e dependências.

Não tente revisar tudo com o mesmo peso. Foque primeiro no risco: memória, input externo, concorrência, build, release e API pública. Depois ajuste estilo. Um PR bonito que vaza memória, engole erro ou publica binário irreproduzível ainda não está pronto.

Conteúdo relacionado

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.