Substituir Macros C por Comptime Zig

Introdução

O preprocessador C é uma das fontes mais comuns de bugs sutis em projetos C. Macros operam por substituição textual, sem type checking, sem escopo, e com efeitos colaterais difíceis de prever. Zig substitui todo o preprocessador por comptime — código Zig normal executado em tempo de compilação.

Este guia converte os padrões mais comuns de macros C para equivalentes Zig. Para metaprogramação avançada, veja Comptime em Zig e Comptime e Reflection.

Pré-requisitos

Constantes #define

C

#define MAX_BUFFER 4096
#define PI 3.14159265358979
#define VERSAO "1.2.3"
#define DEBUG_MODE 1

Zig

const MAX_BUFFER: usize = 4096;
const PI: f64 = 3.14159265358979;
const VERSAO: []const u8 = "1.2.3";
const DEBUG_MODE: bool = true;

Vantagem: constantes Zig têm tipo, são verificadas pelo compilador, e aparecem no debugger.

Macros de Função

Macro simples

#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

// Bug clássico:
int resultado = MIN(i++, j++); // i++ ou j++ avaliado DUAS vezes!
fn min(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return if (a < b) a else b;
}

fn max(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return if (a > b) a else b;
}

fn abs(x: anytype) @TypeOf(x) {
    return if (x < 0) -x else x;
}

// Sem bug: argumentos avaliados UMA vez
var i: i32 = 5;
var j: i32 = 10;
const resultado = min(blk: { i += 1; break :blk i; }, blk: { j += 1; break :blk j; });

ARRAY_SIZE

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

int numeros[] = {1, 2, 3, 4, 5};
size_t n = ARRAY_SIZE(numeros); // 5

// Bug: não funciona com ponteiros
void processar(int arr[]) {
    size_t n = ARRAY_SIZE(arr); // BUG: retorna sizeof(ponteiro)!
}
// Em Zig, slices carregam o tamanho
const numeros = [_]i32{ 1, 2, 3, 4, 5 };
const n = numeros.len; // 5

fn processar(arr: []const i32) void {
    const n = arr.len; // Sempre correto
    _ = n;
}

STRINGIFY e CONCAT

#define STRINGIFY(x) #x
#define CONCAT(a, b) a ## b
// Zig: usar comptime para geração de nomes
fn stringify(comptime valor: anytype) []const u8 {
    return @typeName(@TypeOf(valor));
}

// Para concatenação de strings em comptime:
fn nomeCompleto(comptime prefixo: []const u8, comptime sufixo: []const u8) []const u8 {
    return prefixo ++ sufixo;
}
const NOME = nomeCompleto("meu_", "campo"); // "meu_campo"

Compilação Condicional

C

#ifdef DEBUG
    #define LOG(msg) printf("DEBUG: %s\n", msg)
#else
    #define LOG(msg) ((void)0)
#endif

#if defined(__linux__)
    #include <sys/epoll.h>
#elif defined(__APPLE__)
    #include <sys/event.h>
#elif defined(_WIN32)
    #include <winsock2.h>
#endif

#ifndef TIMEOUT
    #define TIMEOUT 30
#endif

Zig

const builtin = @import("builtin");
const std = @import("std");

fn log(comptime msg: []const u8) void {
    if (builtin.mode == .Debug) {
        std.debug.print("DEBUG: {s}\n", .{msg});
    }
    // Em release, esta função compila para noop
}

// Plataforma
const os_api = if (builtin.os.tag == .linux)
    @import("linux_epoll.zig")
else if (builtin.os.tag == .macos)
    @import("macos_kqueue.zig")
else if (builtin.os.tag == .windows)
    @import("windows_iocp.zig")
else
    @compileError("OS não suportado");

// Valor padrão (sem #ifndef)
const TIMEOUT: u32 = if (@hasDecl(@import("config.zig"), "timeout"))
    @import("config.zig").timeout
else
    30;

Header Guards

C

#ifndef MINHA_LIB_H
#define MINHA_LIB_H

typedef struct {
    int x, y;
} Ponto;

int somar(int a, int b);

#endif // MINHA_LIB_H

Zig

// Não necessário em Zig!
// Cada arquivo é um módulo — não existe inclusão dupla
// minha_lib.zig

pub const Ponto = struct {
    x: i32,
    y: i32,
};

pub fn somar(a: i32, b: i32) i32 {
    return a + b;
}

Zig elimina completamente a necessidade de header guards. O sistema de módulos garante que cada arquivo é processado uma vez.

Macros com Variadic Arguments

C

#define LOG_ERROR(fmt, ...) fprintf(stderr, "ERRO: " fmt "\n", ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) fprintf(stdout, "INFO: " fmt "\n", ##__VA_ARGS__)

LOG_ERROR("Conexão falhou: %s", strerror(errno));

Zig

fn logError(comptime fmt: []const u8, args: anytype) void {
    std.debug.print("ERRO: " ++ fmt ++ "\n", args);
}

fn logInfo(comptime fmt: []const u8, args: anytype) void {
    std.debug.print("INFO: " ++ fmt ++ "\n", args);
}

logError("Conexão falhou: {s}", .{mensagem_erro});

Veja Error Logging para padrões de log mais avançados.

Type-Generic Macros (_Generic)

C11

#define imprimir(x) _Generic((x), \
    int: imprimir_int, \
    float: imprimir_float, \
    char*: imprimir_string \
)(x)

Zig

fn imprimir(valor: anytype) void {
    const T = @TypeOf(valor);
    switch (@typeInfo(T)) {
        .int => std.debug.print("{}\n", .{valor}),
        .float => std.debug.print("{d:.2}\n", .{valor}),
        .pointer => |p| {
            if (p.child == u8) {
                std.debug.print("{s}\n", .{valor});
            }
        },
        else => std.debug.print("{any}\n", .{valor}),
    }
}

Macros para Geração de Código (X-Macros)

C

#define ERROS \
    X(OK, "Sucesso") \
    X(NOT_FOUND, "Não encontrado") \
    X(TIMEOUT, "Timeout") \
    X(PERMISSION, "Sem permissão")

// Gerar enum
typedef enum {
    #define X(nome, desc) ERRO_##nome,
    ERROS
    #undef X
} Erro;

// Gerar função de string
const char* erro_para_string(Erro e) {
    switch (e) {
        #define X(nome, desc) case ERRO_##nome: return desc;
        ERROS
        #undef X
    }
}

Zig

const ErroInfo = struct {
    nome: []const u8,
    descricao: []const u8,
};

const erros = [_]ErroInfo{
    .{ .nome = "OK", .descricao = "Sucesso" },
    .{ .nome = "NOT_FOUND", .descricao = "Não encontrado" },
    .{ .nome = "TIMEOUT", .descricao = "Timeout" },
    .{ .nome = "PERMISSION", .descricao = "Sem permissão" },
};

const Erro = enum {
    ok,
    not_found,
    timeout,
    permission,

    pub fn descricao(self: Erro) []const u8 {
        return erros[@intFromEnum(self)].descricao;
    }
};

Conclusão

O comptime de Zig substitui todo o preprocessador C com vantagens claras: tipagem, escopo, debugging, e sem efeitos colaterais surpresa. A maioria das macros C se traduz diretamente para funções Zig com anytype ou comptime, resultando em código mais seguro e mais legível.

Para mais sobre comptime, veja Comptime em Zig. Para a migração completa de C para Zig, consulte Guia de Migração: C para Zig e Como Migrar um Projeto C para Zig.

Continue aprendendo Zig

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