Zig Code Generation: Procedural Macros e Metaprogramação

Uma das superpotências do Zig é a metaprogramação em tempo de compilação. Enquanto outras linguagens usam macros textuais (C) ou proc-macros complexas (Rust), Zig oferece código Zig normal executado em comptime — permitindo gerar código boilerplate de forma type-safe e extremamente poderosa.

Neste guia, você aprenderá a criar:

  • Serializadores automáticos
  • Builders type-safe
  • Implementações de debug
  • Geração de código em geral

Conceitos Fundamentais

Reflexão em Tempo de Compilação

const std = @import("std");

const User = struct {
    name: []const u8,
    age: u32,
    email: []const u8,
};

// Inspeciona tipo em comptime
fn analyzeType(comptime T: type) void {
    std.debug.print("Tipo: {s}\n", .{@typeName(T)});
    std.debug.print("Tamanho: {d}\n", .{@sizeOf(T)});
    std.debug.print("Alinhamento: {d}\n", .{@alignOf(T)});
}

// Usa
comptime analyzeType(User);

Introspecção de Structs

fn printStructInfo(comptime T: type) void {
    std.debug.print("Struct: {s}\n", .{@typeName(T)});
    
    const info = @typeInfo(T);
    if (info != .Struct) return;
    
    inline for (info.Struct.fields) |field| {
        std.debug.print("  Campo: {s} ({s})\n", .{
            field.name,
            @typeName(field.type)
        });
    }
}

// Saída:
// Struct: User
//   Campo: name ([]const u8)
//   Campo: age (u32)
//   Campo: email ([]const u8)

Exemplo 1: Auto-Implementação de Debug

// Gera automaticamente função de debug format
fn AutoDebug(comptime T: type) type {
    return struct {
        pub fn format(
            self: T,
            comptime fmt: []const u8,
            options: std.fmt.FormatOptions,
            writer: anytype,
        ) !void {
            _ = fmt;
            _ = options;
            
            const info = @typeInfo(T);
            if (info != .Struct) {
                @compileError("AutoDebug só funciona com structs");
            }
            
            try writer.print("{s}{{ ", .{@typeName(T)});
            
            var first = true;
            inline for (info.Struct.fields) |field| {
                if (!first) try writer.writeAll(", ");
                first = false;
                
                try writer.print("{s}: ", .{field.name});
                
                // Obtém valor do campo
                const value = @field(self, field.name);
                
                // Formata baseado no tipo
                switch (@typeInfo(field.type)) {
                    .Int, .ComptimeInt => try writer.print("{d}", .{value}),
                    .Float, .ComptimeFloat => try writer.print("{d}", .{value}),
                    .Pointer => |ptr| {
                        if (ptr.size == .Slice and ptr.child == u8) {
                            try writer.print("\"{s}\"", .{value});
                        } else {
                            try writer.writeAll("...");
                        }
                    },
                    else => try writer.writeAll("?"),
                }
            }
            
            try writer.writeAll(" }");
        }
    };
}

// Uso
const User = struct {
    // Aplica mixin
    usingnamespace AutoDebug(@This());
    
    name: []const u8,
    age: u32,
};

pub fn main() !void {
    const user = User{
        .name = "Alice",
        .age = 30,
    };
    
    std.debug.print("{s}\n", .{user}); // User{ name: "Alice", age: 30 }
}

Exemplo 2: Serializador JSON Genérico

const std = @import("std");

// Serializa qualquer struct para JSON
fn serializeJson(
    allocator: std.mem.Allocator,
    value: anytype,
) ![]u8 {
    const T = @TypeOf(value);
    
    var list = std.ArrayList(u8).init(allocator);
    errdefer list.deinit();
    var writer = list.writer();
    
    try serializeValue(writer, value);
    
    return list.toOwnedSlice();
}

fn serializeValue(writer: anytype, value: anytype) !void {
    const T = @TypeOf(value);
    const info = @typeInfo(T);
    
    switch (info) {
        .Int, .ComptimeInt => {
            try writer.print("{d}", .{value});
        },
        .Float, .ComptimeFloat => {
            try writer.print("{d}", .{value});
        },
        .Bool => {
            try writer.print("{}", .{value});
        },
        .Null => {
            try writer.writeAll("null");
        },
        .Optional => {
            if (value) |v| {
                try serializeValue(writer, v);
            } else {
                try writer.writeAll("null");
            }
        },
        .Pointer => |ptr| {
            if (ptr.size == .Slice) {
                if (ptr.child == u8) {
                    // String
                    try writer.print("\"{s}\"", .{value});
                } else {
                    // Array
                    try writer.writeAll("[");
                    for (value, 0..) |item, i| {
                        if (i > 0) try writer.writeAll(", ");
                        try serializeValue(writer, item);
                    }
                    try writer.writeAll("]");
                }
            } else {
                try serializeValue(writer, value.*);
            }
        },
        .Struct => {
            try writer.writeAll("{");
            var first = true;
            inline for (info.Struct.fields) |field| {
                if (!first) try writer.writeAll(", ");
                first = false;
                
                try writer.print("\"{s}\": ", .{field.name});
                try serializeValue(writer, @field(value, field.name));
            }
            try writer.writeAll("}");
        },
        .Enum => {
            try writer.print("\"{s}\"", .{@tagName(value)});
        },
        .Union => {
            @compileError("Unions não suportadas ainda");
        },
        else => {
            @compileError("Tipo não suportado: " ++ @typeName(T));
        },
    }
}

// Uso
const Person = struct {
    name: []const u8,
    age: u32,
    email: ?[]const u8,
    hobbies: []const []const u8,
};

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    
    const person = Person{
        .name = "Bob",
        .age = 25,
        .email = "bob@example.com",
        .hobbies = &.{"coding", "gaming"},
    };
    
    const json = try serializeJson(allocator, person);
    defer allocator.free(json);
    
    std.debug.print("{s}\n", .{json});
    // {"name": "Bob", "age": 25, "email": "bob@example.com", "hobbies": ["coding", "gaming"]}
}

Exemplo 3: Builder Pattern Automático

// Gera builder type-safe automaticamente
fn AutoBuilder(comptime T: type) type {
    const info = @typeInfo(T);
    if (info != .Struct) {
        @compileError("AutoBuilder só suporta structs");
    }
    
    // Conta campos opcionais vs obrigatórios
    var optional_count: usize = 0;
    inline for (info.Struct.fields) |field| {
        if (@typeInfo(field.type) == .Optional) {
            optional_count += 1;
        }
    }
    
    return struct {
        // Gera campos do builder (todos opcionais inicialmente)
        const Builder = @This();
        
        # Comptime: gera campos
        # Nota: Zig não tem macros hygienic como Rust,
        # então usamos struct anônima
        
        pub fn init() Builder {
            return .{};
        }
        
        // Gera setters em comptime
        inline for (info.Struct.fields) |field| {
            const FieldType = field.type;
            const field_name = field.name;
            
            // Cria função setter com nome baseado no campo
            const setter = struct {
                pub fn set(b: Builder, value: FieldType) Builder {
                    var new = b;
                    @field(new, field_name) = value;
                    return new;
                }
            }.set;
            
            // Exporta com nome adequado
            @export(setter, .{ .name = field_name });
        }
        
        pub fn build(self: Builder) T {
            var result: T = undefined;
            inline for (info.Struct.fields) |field| {
                const has_value = !(@typeInfo(@TypeOf(@field(self, field.name))) == .Optional and 
                    @field(self, field.name) == null);
                
                if (!has_value and @typeInfo(field.type) != .Optional) {
                    @compileError("Campo obrigatório não setado: " ++ field.name);
                }
                
                @field(result, field.name) = @field(self, field.name) orelse continue;
            }
            return result;
        }
    };
}

// Solução mais simples com comptime string
fn MakeBuilder(comptime T: type) type {
    return struct {
        inner: Partial = .{},
        
        const Partial = blk: {
            var fields: [info.Struct.fields.len]std.builtin.Type.StructField = undefined;
            inline for (info.Struct.fields, 0..) |f, i| {
                fields[i] = .{
                    .name = f.name,
                    .type = ?f.type,
                    .default_value = &@as(?f.type, null),
                    .is_comptime = false,
                    .alignment = @alignOf(?f.type),
                };
            }
            break :blk @Type(.{
                .Struct = .{
                    .layout = .auto,
                    .fields = &fields,
                    .decls = &.{},
                    .is_tuple = false,
                },
            });
        };
        
        pub fn new() @This() {
            return .{ .inner = .{} };
        }
        
        pub fn build(self: @This()) T {
            var result: T = undefined;
            inline for (info.Struct.fields) |f| {
                const val = @field(self.inner, f.name);
                if (val == null) {
                    @compileError("Missing field: " ++ f.name);
                }
                @field(result, f.name) = val.?;
            }
            return result;
        }
    };
}

// Gera setters dinamicamente (versão simplificada demonstrativa)
fn createBuilder(comptime T: type) type {
    const info = @typeInfo(T);
    
    return struct {
        name: ?[]const u8 = null,
        age: ?u32 = null,
        
        const Self = @This();
        
        pub fn withName(self: Self, n: []const u8) Self {
            var s = self;
            s.name = n;
            return s;
        }
        
        pub fn withAge(self: Self, a: u32) Self {
            var s = self;
            s.age = a;
            return s;
        }
        
        pub fn build(self: Self) T {
            return .{
                .name = self.name orelse @compileError("name is required"),
                .age = self.age orelse @compileError("age is required"),
            };
        }
    };
}

// Uso
const Person = struct {
    name: []const u8,
    age: u32,
};

pub fn main() !void {
    const Builder = createBuilder(Person);
    
    const person = Builder{}
        .withName("Alice")
        .withAge(30)
        .build();
    
    std.debug.print("{s}, {d}\n", .{ person.name, person.age });
}

Exemplo 4: Enum para String Automático

// Gera funções de conversão Enum ↔ String
fn EnumStringMap(comptime E: type) type {
    const info = @typeInfo(E);
    if (info != .Enum) {
        @compileError("EnumStringMap requer enum");
    }
    
    return struct {
        // Converte enum para string
        pub fn toString(e: E) []const u8 {
            return switch (e) {
                inline else => |tag| @tagName(tag),
            };
        }
        
        // Converte string para enum (comptime)
        pub fn fromString(comptime s: []const u8) E {
            inline for (info.Enum.fields) |field| {
                if (std.mem.eql(u8, s, field.name)) {
                    return @field(E, field.name);
                }
            }
            @compileError("Enum variant não encontrada: " ++ s);
        }
        
        // Runtime parse (retorna null se não encontrar)
        pub fn parse(s: []const u8) ?E {
            inline for (info.Enum.fields) |field| {
                if (std.mem.eql(u8, s, field.name)) {
                    return @field(E, field.name);
                }
            }
            return null;
        }
    };
}

// Uso
const Status = enum {
    pending,
    active,
    completed,
    cancelled,
};

const StatusMap = EnumStringMap(Status);

pub fn main() !void {
    const status = Status.active;
    
    // Enum → String
    const s = StatusMap.toString(status);
    std.debug.print("Status: {s}\n", .{s}); // "active"
    
    // String → Enum (runtime)
    const parsed = StatusMap.parse("completed");
    std.debug.print("Parsed: {any}\n", .{parsed}); // .completed
}

Exemplo 5: Geração de Equals e Hash

// Gera função de igualdade profunda
fn DeepEqual(comptime T: type) type {
    return struct {
        pub fn eql(a: T, b: T) bool {
            const info = @typeInfo(T);
            
            switch (info) {
                .Struct => |s| {
                    inline for (s.fields) |field| {
                        const fa = @field(a, field.name);
                        const fb = @field(b, field.name);
                        
                        const FieldEql = DeepEqual(field.type);
                        if (!FieldEql.eql(fa, fb)) return false;
                    }
                    return true;
                },
                .Array => |arr| {
                    for (a, b) |xa, xb| {
                        const ElemEql = DeepEqual(arr.child);
                        if (!ElemEql.eql(xa, xb)) return false;
                    }
                    return true;
                },
                .Pointer => |ptr| {
                    if (ptr.size == .Slice) {
                        if (a.len != b.len) return false;
                        for (a, b) |xa, xb| {
                            const ElemEql = DeepEqual(ptr.child);
                            if (!ElemEql.eql(xa, xb)) return false;
                        }
                        return true;
                    }
                    return a == b;
                },
                .Optional => {
                    if (a == null and b == null) return true;
                    if (a == null or b == null) return false;
                    const ChildEql = DeepEqual(info.Optional.child);
                    return ChildEql.eql(a.?, b.?);
                },
                else => {
                    return a == b;
                },
            }
        }
    };
}

// Uso
const Point = struct {
    x: f64,
    y: f64,
};

const Line = struct {
    start: Point,
    end: Point,
};

pub fn main() !void {
    const LineEql = DeepEqual(Line);
    
    const l1 = Line{
        .start = .{ .x = 0, .y = 0 },
        .end = .{ .x = 1, .y = 1 },
    };
    
    const l2 = Line{
        .start = .{ .x = 0, .y = 0 },
        .end = .{ .x = 1, .y = 1 },
    };
    
    std.debug.print("Iguais: {}\n", .{LineEql.eql(l1, l2)}); // true
}

Tabela de TypeInfo

@typeInfo()DescriçãoUso
.TypeO próprio tipoMetaprogramação recursiva
.VoidvoidFunções sem retorno
.BoolboolBooleanos
.IntInteiros@typeInfo(T).Int.signed
.FloatFloats@typeInfo(T).Float.bits
.PointerPonteiros.Pointer.child, .Pointer.size
.ArrayArrays.Array.len, .Array.child
.StructStructs.Struct.fields
.Optional?T.Optional.child
.EnumEnums.Enum.fields
.UnionUnions.Union.fields
.FnFunções.Fn.params, .Fn.return_type

Próximos Passos

  1. 🔗 Comptime em Zig — Fundamentos da metaprogramação
  2. SIMD em Zig — Geração de código vetorial
  3. 🧠 Zig para WebAssembly — Compile-time code generation para WASM
  4. 📦 Zig Package Registry — Publique sua biblioteca de macros

Recursos


Está usando metaprogramação Zig para eliminar boilerplate? Compartilhe seu caso de uso!

Continue aprendendo Zig

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