Como Migrar um Projeto C para Zig

Introdução

Migrar um projeto C existente para Zig não precisa ser um esforço de “tudo ou nada”. A interoperabilidade bidirecional entre Zig e C permite uma migração incremental — arquivo por arquivo, função por função — sem nunca quebrar o build. Este artigo descreve uma estratégia prática para essa migração.

Para um guia técnico detalhado, veja Guia de Migração: C para Zig. Para entender as diferenças entre as linguagens, consulte Zig vs C Moderno (C23).

Por Que Migrar?

Antes de migrar, é importante entender os benefícios concretos:

  1. Segurança: Bounds checking, detecção de uso após liberação, null safety
  2. Manutenibilidade: Código mais claro sem preprocessador, com error handling integrado
  3. Produtividade: Sistema de build integrado, cross-compilation trivial, testes embutidos
  4. Performance igual ou melhor: Mesmo backend LLVM, com otimizações extras via comptime

E os riscos:

  1. Zig é pré-1.0: API pode mudar entre versões
  2. Curva de aprendizado: A equipe precisa aprender Zig
  3. Ecossistema menor: Algumas ferramentas C não têm equivalente em Zig

Estratégia de Migração em 5 Fases

Fase 1: Substituir o Sistema de Build

O primeiro passo é trocar Make/CMake por build.zig, sem alterar nenhuma linha de código C:

const std = @import("std");

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

    const exe = b.addExecutable(.{
        .name = "meu-projeto",
        .target = target,
        .optimize = optimize,
    });

    // Compilar todos os arquivos C existentes
    exe.addCSourceFiles(.{
        .files = &.{
            "src/main.c",
            "src/utils.c",
            "src/parser.c",
        },
        .flags = &.{
            "-std=c11",
            "-Wall",
            "-Wextra",
        },
    });

    exe.linkLibC();
    exe.addIncludePath(b.path("include"));

    b.installArtifact(exe);
}

Veja Migrar de Makefile para build.zig e Migrar de CMake para build.zig para guias detalhados.

Fase 2: Adicionar Código Zig Novo

Comece escrevendo código novo em Zig. Novos módulos, novas funcionalidades — tudo em Zig, chamando código C existente quando necessário:

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

pub fn processarComValidacao(dados: []const u8) !Resultado {
    // Lógica nova em Zig
    if (dados.len == 0) return error.DadosVazios;

    // Chamar código C existente
    const resultado_c = c.parse(dados.ptr, dados.len);
    if (resultado_c == null) return error.ParseFalhou;

    // Pós-processamento em Zig
    return Resultado.fromC(resultado_c);
}

Veja Chamar Funções C de Zig e Converter Ponteiros C para Zig.

Fase 3: Migrar Módulos Isolados

Identifique módulos C com poucas dependências e reescreva-os em Zig. Comece pelos mais simples — utilidades, parsers, algoritmos isolados:

Antes (C):

// utils.c
#include <string.h>
#include <ctype.h>

int contar_palavras(const char* texto) {
    int count = 0;
    int em_palavra = 0;
    while (*texto) {
        if (isspace(*texto)) {
            em_palavra = 0;
        } else if (!em_palavra) {
            em_palavra = 1;
            count++;
        }
        texto++;
    }
    return count;
}

Depois (Zig):

// utils.zig
const std = @import("std");

pub fn contarPalavras(texto: []const u8) usize {
    var count: usize = 0;
    var em_palavra = false;

    for (texto) |c| {
        if (std.ascii.isWhitespace(c)) {
            em_palavra = false;
        } else if (!em_palavra) {
            em_palavra = true;
            count += 1;
        }
    }

    return count;
}

// Exportar para código C que ainda não foi migrado
export fn contar_palavras(texto: [*:0]const u8) c_int {
    const slice = std.mem.span(texto);
    return @intCast(contarPalavras(slice));
}

test "contar palavras" {
    try std.testing.expectEqual(@as(usize, 3), contarPalavras("Zig é incrível"));
    try std.testing.expectEqual(@as(usize, 0), contarPalavras(""));
}

Fase 4: Substituir Padrões C por Padrões Zig

Conforme módulos são migrados, substitua padrões C por equivalentes Zig melhores:

Padrão CPadrão Zig
malloc/freeAllocators explícitos
Macros #definecomptime
errno/retorno de erroError unions
NULL checksOptional ?T
Preprocessador condicionalcomptime if
goto cleanupdefer/errdefer

Veja Substituir malloc/free por Allocators, Substituir Macros C por Comptime e Padrões Errdefer.

Fase 5: Remover Dependências C

Na fase final, substitua as últimas chamadas C e remova os arquivos .c e .h:

  1. Substituir #include <stdio.h> por std.io e std.fs
  2. Substituir #include <string.h> por std.mem
  3. Substituir #include <stdlib.h> por allocators
  4. Substituir bibliotecas C externas por equivalentes Zig (quando disponíveis) ou manter via @cImport

Armadilhas Comuns

1. Tradução Literal

Não traduza C para Zig linha por linha. Aproveite os padrões idiomáticos de Zig:

// RUIM: tradução literal de C
var i: usize = 0;
while (i < array.len) : (i += 1) {
    processar(array[i]);
}

// BOM: idiomático Zig
for (array) |item| {
    processar(item);
}

2. Ignorar o Sistema de Allocators

Não use std.heap.c_allocator em todo lugar. Use allocators apropriados:

// Melhor: arena para operações com lifecycle definido
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

3. Tentar Migrar Tudo de Uma Vez

A migração incremental é essencial. Um projeto de 100.000 linhas de C não será migrado em uma semana. Migre módulo por módulo, mantendo o build funcional a cada passo.

Ferramentas Úteis

  • zig translate-c: Traduz automaticamente código C para Zig (resultado precisa de revisão manual)
  • zig cc: Use como drop-in replacement para gcc/clang durante a transição
  • ZLS: Language server para suporte em editores
  • zig test: Testes integrados para validar cada módulo migrado

Conclusão

A migração de C para Zig é viável e traz benefícios reais de segurança, manutenibilidade e produtividade. A chave é a abordagem incremental: comece pelo build system, adicione código novo em Zig, migre módulos isolados, e gradualmente elimine o código C.

A interoperabilidade transparente de Zig com C significa que você nunca precisa “parar tudo” para migrar. O projeto continua funcional em cada etapa.

Para detalhes técnicos, consulte Guia de Migração: C para Zig e Portar uma Biblioteca C para Zig.

Continue aprendendo Zig

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