Zig para Programadores C: Guia de Migração Seguro

Zig para Programadores C: Guia de Migração Seguro

Se você sabe C, Zig parece familiar nos primeiros minutos: funções livres, tipos explícitos, ponteiros, structs, controle de memória e binários nativos. A diferença aparece quando o código deixa de ser exemplo e vira projeto real. Zig tenta preservar o controle de C, mas remove várias fontes de comportamento indefinido, torna erros parte do tipo da função e substitui macros frágeis por comptime.

Este guia é para quem procura Zig para programadores C, migrar de C para Zig ou entender se Zig pode substituir C em partes de um sistema. A resposta curta: Zig é uma ótima escolha para novos módulos, ferramentas, CLIs, parsers, componentes embarcados e migrações graduais. Para bases C grandes, a estratégia mais segura não é reescrever tudo; é usar Zig como compilador, camada de build e linguagem de novos limites.

Se você ainda está avaliando a linguagem, leia também Zig vs C moderno, Interoperabilidade Zig e C e Migrar projeto C para Zig.

Mapa rápido: C mental model para Zig

Em C você pensa em…Em Zig você usa…O que muda na prática
malloc/free globaisstd.mem.Allocator passado explicitamenteQuem aloca também decide política e tempo de vida
ponteiro + tamanho separadoslice []TO tamanho viaja junto com o ponteiro
NULLoptional ?TAusência precisa ser tratada explicitamente
código de erro interror union !TA assinatura mostra que a função falha
#define e macroscomptime, inline, generics por tipoMetaprogramação com a própria linguagem
headers .himports de módulos e pubAPI pública fica no arquivo Zig
Make/CMakebuild.zigBuild é código Zig tipado
UB silenciosochecks em debug/release-safeMuitos erros aparecem cedo

A maior mudança não é sintaxe. É disciplina explícita. Zig não tenta esconder memória, erros ou plataforma. Ele obriga você a colocar essas decisões no código.

O primeiro choque: ponteiros não são todos iguais

Em C, um ponteiro pode significar muitas coisas: um único item, um array, uma string terminada em zero, memória opcional, memória mutável ou apenas uma view temporária. Em Zig, você escolhe um tipo mais preciso.

const std = @import("std");

fn somaSlice(nums: []const i32) i32 {
    var total: i32 = 0;
    for (nums) |n| total += n;
    return total;
}

pub fn main() void {
    const valores = [_]i32{ 10, 20, 30 };
    std.debug.print("total = {d}\n", .{somaSlice(valores[0..])});
}

[]const i32 é uma fatia: ponteiro + tamanho. Se a função precisa de exatamente um item, use *T. Se precisa de vários itens, prefira []T. Se precisa receber uma string C terminada em zero, use o tipo sentinel correto ([*:0]const u8) ou converta com cuidado.

Essa separação reduz bugs clássicos de C: passar o tamanho errado, iterar além do buffer ou tratar uma string binária como se fosse terminada em \0.

Allocators são dependências, não globals

Em C, a pergunta “quem libera isto?” costuma depender de convenção. Em Zig, a convenção aparece na assinatura.

const std = @import("std");

fn duplicaMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
    return std.fmt.allocPrint(allocator, "Olá, {s}!", .{nome});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();
    const msg = try duplicaMensagem(allocator, "Zig");
    defer allocator.free(msg);

    std.debug.print("{s}\n", .{msg});
}

A função recebe um allocator porque ela aloca. O chamador libera porque ele escolheu a política de memória. Em testes, você pode trocar por um arena allocator, fixed buffer allocator ou allocator instrumentado. Para aprofundar, veja gerenciamento de memória em Zig e o cheatsheet de allocators.

Erros explícitos substituem códigos mágicos

C costuma retornar -1, NULL, errno ou combinações de status. Zig usa error unions.

const std = @import("std");

fn abreConfig(path: []const u8) !std.fs.File {
    return std.fs.cwd().openFile(path, .{});
}

pub fn main() !void {
    const file = abreConfig("config.toml") catch |err| switch (err) {
        error.FileNotFound => {
            std.debug.print("config.toml não existe\n", .{});
            return;
        },
        else => return err,
    };
    defer file.close();
}

O tipo !std.fs.File diz: esta função retorna um arquivo ou um erro. try propaga, catch trata. Não existe exceção invisível nem status esquecido por acidente. Para quem vem de C, isso parece verboso no começo; depois vira documentação executável.

Optional não é ponteiro nulo disfarçado

Quando uma função pode não encontrar resultado, use optional.

fn encontraByte(buf: []const u8, alvo: u8) ?usize {
    for (buf, 0..) |b, i| {
        if (b == alvo) return i;
    }
    return null;
}

const pos = encontraByte("zig", 'g') orelse 0;

?usize força o chamador a lidar com ausência. Para ponteiros, ?*T deixa claro que o ponteiro pode ser nulo; *T não pode.

defer é o antídoto contra cleanup espalhado

C frequentemente usa goto cleanup para liberar recursos em múltiplos caminhos de erro. Zig usa defer e errdefer.

fn carregaArquivo(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();

    const stat = try file.stat();
    const data = try allocator.alloc(u8, stat.size);
    errdefer allocator.free(data);

    _ = try file.readAll(data);
    return data;
}

defer roda no fim do escopo. errdefer roda apenas quando a função sai por erro. Esse padrão mantém cleanup perto da alocação sem perder clareza.

comptime substitui macros sem perder performance

Macros de C são texto. comptime é execução de Zig em tempo de compilação. Isso permite gerar código, validar tipos e escrever funções genéricas com checagem real.

fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

const maior = max(i32, 10, 20);

O compilador conhece T, valida operações e especializa o código. Não há macro expandida com surpresa de precedência, avaliação dupla ou erro obscuro do pré-processador. Veja comptime em Zig para um mergulho mais fundo.

Interop C é caminho de migração, não detalhe

Uma vantagem enorme para equipes C é que Zig conversa com headers C diretamente.

const c = @cImport({
    @cInclude("stdio.h");
});

pub fn main() void {
    _ = c.printf("Chamando printf via @cImport\n");
}

Você pode começar pequeno:

  1. compilar um projeto C existente com zig cc;
  2. mover o build para build.zig;
  3. escrever novos módulos em Zig;
  4. expor funções Zig para C quando necessário;
  5. migrar partes críticas com testes de fronteira.

Para referência prática, mantenha à mão @cImport em Zig e o cheatsheet de interop com C.

Build.zig no lugar de Makefile frágil

build.zig é um programa Zig que descreve como compilar, testar e linkar. Isso é especialmente útil para quem mantém projetos C multi-plataforma.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "app",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.linkLibC();
    b.installArtifact(exe);
}

O mesmo arquivo aceita opções, targets, bibliotecas C, artefatos de teste e cross-compilation. Para o próximo passo, leia Build System do Zig e cross-compilation em Zig.

Armadilhas comuns para quem vem de C

1. Procurar malloc global em todo lugar

Em Zig idiomático, uma função que precisa alocar recebe allocator. Evite esconder alocação em helpers sem deixar isso claro na assinatura.

2. Usar ponteiro quando slice seria melhor

Se a função percorre N elementos, []T comunica melhor que *T. Ponteiros brutos ainda existem, mas slices são o padrão para buffers.

3. Tratar string como tipo mágico

String em Zig normalmente é []const u8. Ela pode conter bytes arbitrários e não precisa terminar em zero. Para APIs C, converta para sentinel quando necessário.

4. Ignorar modo de build

Debug e ReleaseSafe fazem checks que ajudam na migração. ReleaseFast remove proteções para performance. Durante a migração, rode testes em modo seguro antes de otimizar.

5. Recriar macros C sem pensar

Muitas macros viram funções inline, comptime ou constantes. Nem toda macro merece uma tradução 1:1.

Quando Zig substitui C bem

Zig é uma boa substituição ou complemento para C quando o projeto precisa de:

  • binário nativo pequeno e previsível;
  • cross-compilation simples;
  • integração com bibliotecas C existentes;
  • parsers, CLIs, ferramentas internas e agentes locais;
  • controle explícito de memória;
  • testes próximos ao código;
  • build reproduzível sem pilha grande de ferramentas externas.

Zig é menos indicado quando a equipe depende de ABI C estável para uma biblioteca pública já consolidada, usa toolchains certificados com requisitos rígidos ou precisa de um ecossistema de bibliotecas maduras que ainda não existe em Zig. Nesses casos, use Zig nas bordas: build, ferramentas, módulos novos e testes de integração.

Checklist de migração C → Zig

Antes de migrar uma parte real, responda:

  • Qual módulo tem fronteira clara e testes possíveis?
  • O contrato é arquivo, socket, função C, CLI ou biblioteca?
  • Quem aloca e quem libera cada buffer?
  • Quais erros hoje são errno, NULL ou código numérico?
  • Há macros que deveriam virar comptime?
  • O módulo precisa expor ABI C para o restante do sistema?
  • A validação roda em Debug/ReleaseSafe e no target final?

Se você não consegue responder essas perguntas, ainda não migre. Primeiro envolva a fronteira com testes.

Exemplo de plano de adoção gradual

Um plano seguro para uma codebase C existente:

  1. Semana 1: use zig cc para compilar um utilitário C pequeno e documente flags.
  2. Semana 2: crie build.zig para reproduzir o build local.
  3. Semana 3: escreva uma CLI auxiliar em Zig que consome arquivos do projeto.
  4. Semana 4: migre um parser, conversor ou validador com testes de fixtures.
  5. Depois: avalie módulos de performance, sempre mantendo ABI ou formato de dados claro.

Isso evita a armadilha da reescrita total. O ganho vem de reduzir risco e aumentar controle, não de trocar linguagem por vaidade.

Perguntas frequentes

Zig é C com sintaxe diferente?

Não. Zig preserva o controle de C, mas muda decisões centrais: erros são tipos, allocators são explícitos, macros viram comptime, imports substituem headers e o build é escrito em Zig.

Preciso abandonar C para usar Zig?

Não. O caminho mais pragmático é incremental. Use zig cc, @cImport, build.zig e módulos novos em Zig enquanto o C existente continua funcionando.

Zig tem garbage collector?

Não. A memória continua explícita. A diferença é que o allocator aparece como dependência e pode ser trocado, testado e auditado.

Zig é mais seguro que C?

Em muitos cenários, sim, especialmente em debug e release-safe, porque reduz null implícito, melhora checagem de bounds e explicita erros. Mas ainda é uma linguagem de sistemas: ponteiros, lifetime e ABI continuam exigindo disciplina.

Quando devo escolher Rust em vez de Zig?

Rust é forte quando o projeto precisa de garantias rígidas de ownership em tempo de compilação e ecossistema maduro. Zig é forte quando simplicidade, C interop, controle explícito e cross-compilation pesam mais. Compare com Zig vs Rust antes de decidir.

Próximo passo

Se você vem de C, o melhor exercício é pequeno: pegue uma função que manipula buffer, reescreva com []const u8, !T, defer e allocator explícito. Depois compile para dois targets. Essa experiência mostra mais sobre Zig do que qualquer debate abstrato.

Para continuar, siga esta trilha:

Para comparar com outras stacks de backend e sistemas, veja também a biblioteca padrão de Go e ownership e borrowing em Rust.

Continue aprendendo Zig

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