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ção | Uso |
|---|---|---|
.Type | O próprio tipo | Metaprogramação recursiva |
.Void | void | Funções sem retorno |
.Bool | bool | Booleanos |
.Int | Inteiros | @typeInfo(T).Int.signed |
.Float | Floats | @typeInfo(T).Float.bits |
.Pointer | Ponteiros | .Pointer.child, .Pointer.size |
.Array | Arrays | .Array.len, .Array.child |
.Struct | Structs | .Struct.fields |
.Optional | ?T | .Optional.child |
.Enum | Enums | .Enum.fields |
.Union | Unions | .Union.fields |
.Fn | Funções | .Fn.params, .Fn.return_type |
Próximos Passos
- 🔗 Comptime em Zig — Fundamentos da metaprogramação
- ⚡ SIMD em Zig — Geração de código vetorial
- 🧠 Zig para WebAssembly — Compile-time code generation para WASM
- 📦 Zig Package Registry — Publique sua biblioteca de macros
Recursos
Está usando metaprogramação Zig para eliminar boilerplate? Compartilhe seu caso de uso!