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 globais | std.mem.Allocator passado explicitamente | Quem aloca também decide política e tempo de vida |
| ponteiro + tamanho separado | slice []T | O tamanho viaja junto com o ponteiro |
NULL | optional ?T | Ausência precisa ser tratada explicitamente |
código de erro int | error union !T | A assinatura mostra que a função falha |
#define e macros | comptime, inline, generics por tipo | Metaprogramação com a própria linguagem |
headers .h | imports de módulos e pub | API pública fica no arquivo Zig |
| Make/CMake | build.zig | Build é código Zig tipado |
| UB silencioso | checks em debug/release-safe | Muitos 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:
- compilar um projeto C existente com
zig cc; - mover o build para
build.zig; - escrever novos módulos em Zig;
- expor funções Zig para C quando necessário;
- 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,NULLou 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:
- Semana 1: use
zig ccpara compilar um utilitário C pequeno e documente flags. - Semana 2: crie
build.zigpara reproduzir o build local. - Semana 3: escreva uma CLI auxiliar em Zig que consome arquivos do projeto.
- Semana 4: migre um parser, conversor ou validador com testes de fixtures.
- 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:
- Como instalar Zig
- Zig para desenvolvedores
- Gerenciamento de memória em Zig
- Interoperabilidade Zig e C
- Build System do Zig
Para comparar com outras stacks de backend e sistemas, veja também a biblioteca padrão de Go e ownership e borrowing em Rust.