Nos artigos anteriores, exploramos os allocators da biblioteca padrão: tipos de allocators e arena allocator. Agora vamos dar o próximo passo: criar allocators personalizados que atendam exatamente às necessidades da sua aplicação.
Por Que Criar Custom Allocators?
Allocators personalizados são úteis quando:
- Você precisa rastrear o uso de memória (logging, métricas)
- Precisa impor limites de memória por componente
- Quer um pool allocator para objetos de tamanho fixo
- Precisa de estratégias especiais para sistemas embarcados ou real-time
A Interface Allocator de Zig
Para criar um allocator, você precisa implementar a vtable std.mem.Allocator.VTable:
const std = @import("std");
// A vtable requer estas duas funções:
// 1. alloc: fn (ctx, len, ptr_align, ret_addr) ?[*]u8
// 2. resize: fn (ctx, buf, buf_align, new_len, ret_addr) bool
// 3. free: fn (ctx, buf, buf_align, ret_addr) void
Vamos implementar três allocators progressivamente mais complexos.
Exemplo 1: Logging Allocator
Nosso primeiro custom allocator simplesmente registra todas as operações de memória. Perfeito para debugging e profiling.
const std = @import("std");
const LoggingAllocator = struct {
backing_allocator: std.mem.Allocator,
total_alocado: usize,
total_liberado: usize,
num_alocacoes: usize,
num_liberacoes: usize,
fn init(backing: std.mem.Allocator) LoggingAllocator {
return .{
.backing_allocator = backing,
.total_alocado = 0,
.total_liberado = 0,
.num_alocacoes = 0,
.num_liberacoes = 0,
};
}
fn allocator(self: *LoggingAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
const self: *LoggingAllocator = @ptrCast(@alignCast(ctx));
const result = self.backing_allocator.rawAlloc(len, ptr_align, ret_addr);
if (result != null) {
self.total_alocado += len;
self.num_alocacoes += 1;
std.debug.print("[ALLOC] {d} bytes (total: {d} bytes em {d} alocações)\n", .{
len, self.total_alocado, self.num_alocacoes,
});
} else {
std.debug.print("[ALLOC FALHOU] {d} bytes\n", .{len});
}
return result;
}
fn resize(ctx: *anyopaque, buf: [*]u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
const self: *LoggingAllocator = @ptrCast(@alignCast(ctx));
const old_len = @as(usize, @intFromPtr(buf)); // simplified
_ = old_len;
const result = self.backing_allocator.rawResize(buf, buf_align, new_len, ret_addr);
if (result) {
std.debug.print("[RESIZE] para {d} bytes\n", .{new_len});
}
return result;
}
fn free(ctx: *anyopaque, buf: [*]u8, buf_align: u8, ret_addr: usize) void {
const self: *LoggingAllocator = @ptrCast(@alignCast(ctx));
self.num_liberacoes += 1;
std.debug.print("[FREE] (total: {d} liberações)\n", .{self.num_liberacoes});
self.backing_allocator.rawFree(buf, buf_align, ret_addr);
}
fn imprimirRelatorio(self: *const LoggingAllocator) void {
std.debug.print("\n=== Relatório de Memória ===\n", .{});
std.debug.print("Total alocado: {d} bytes\n", .{self.total_alocado});
std.debug.print("Alocações: {d}\n", .{self.num_alocacoes});
std.debug.print("Liberações: {d}\n", .{self.num_liberacoes});
std.debug.print("Pendentes: {d}\n", .{self.num_alocacoes - self.num_liberacoes});
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var logger = LoggingAllocator.init(gpa.allocator());
const alloc = logger.allocator();
// Usar como qualquer allocator
const dados = try alloc.alloc(u8, 256);
const numeros = try alloc.alloc(i32, 10);
alloc.free(dados);
alloc.free(std.mem.sliceAsBytes(numeros));
logger.imprimirRelatorio();
}
Exemplo 2: Allocator com Limite de Memória
Um allocator que impõe um teto máximo de memória — essencial para evitar OOM em servidores.
const std = @import("std");
const LimitedAllocator = struct {
backing: std.mem.Allocator,
limite: usize,
usado: usize,
const Error = error{LimiteExcedido};
fn init(backing: std.mem.Allocator, limite: usize) LimitedAllocator {
return .{
.backing = backing,
.limite = limite,
.usado = 0,
};
}
fn allocator(self: *LimitedAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
if (self.usado + len > self.limite) {
std.debug.print("BLOQUEADO: {d} bytes excederia limite de {d} (usado: {d})\n", .{
len, self.limite, self.usado,
});
return null;
}
const result = self.backing.rawAlloc(len, ptr_align, ret_addr);
if (result != null) {
self.usado += len;
}
return result;
}
fn resize(ctx: *anyopaque, buf: [*]u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
return self.backing.rawResize(buf, buf_align, new_len, ret_addr);
}
fn free(ctx: *anyopaque, buf: [*]u8, buf_align: u8, ret_addr: usize) void {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
self.backing.rawFree(buf, buf_align, ret_addr);
}
fn usoAtual(self: *const LimitedAllocator) void {
const porcentagem = @as(f64, @floatFromInt(self.usado)) /
@as(f64, @floatFromInt(self.limite)) * 100.0;
std.debug.print("Uso: {d}/{d} bytes ({d:.1}%)\n", .{
self.usado, self.limite, porcentagem,
});
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// Limite de 1 KB
var limited = LimitedAllocator.init(gpa.allocator(), 1024);
const alloc = limited.allocator();
// Alocação dentro do limite
const a = try alloc.alloc(u8, 500);
limited.usoAtual();
const b = try alloc.alloc(u8, 400);
limited.usoAtual();
// Esta alocação será bloqueada
const c = alloc.alloc(u8, 200);
if (c) |dados| {
alloc.free(dados);
} else {
std.debug.print("Alocação recusada (esperado!)\n", .{});
}
alloc.free(a);
alloc.free(b);
}
Exemplo 3: Pool Allocator para Objetos de Tamanho Fixo
Pool allocators são extremamente eficientes quando você aloca e libera muitos objetos do mesmo tamanho:
const std = @import("std");
fn PoolAllocator(comptime T: type) type {
return struct {
const Self = @This();
const POOL_SIZE = 64;
const Node = struct {
dados: T,
proximo: ?*Node,
};
livre: ?*Node,
backing: std.mem.Allocator,
total_criados: usize,
total_em_uso: usize,
fn init(backing: std.mem.Allocator) Self {
return .{
.livre = null,
.backing = backing,
.total_criados = 0,
.total_em_uso = 0,
};
}
fn obter(self: *Self) !*T {
if (self.livre) |node| {
// Reutilizar nó da lista livre
self.livre = node.proximo;
self.total_em_uso += 1;
return &node.dados;
}
// Criar novo nó
const node = try self.backing.create(Node);
self.total_criados += 1;
self.total_em_uso += 1;
return &node.dados;
}
fn devolver(self: *Self, ptr: *T) void {
const node: *Node = @fieldParentPtr("dados", ptr);
node.proximo = self.livre;
self.livre = node;
self.total_em_uso -= 1;
}
fn status(self: *const Self) void {
std.debug.print("Pool<{s}>: {d} criados, {d} em uso, {d} livres\n", .{
@typeName(T),
self.total_criados,
self.total_em_uso,
self.total_criados - self.total_em_uso,
});
}
fn deinit(self: *Self) void {
// Liberar nós na lista livre
var atual = self.livre;
while (atual) |node| {
const proximo = node.proximo;
self.backing.destroy(node);
atual = proximo;
}
self.livre = null;
}
};
}
const Particula = struct {
x: f32,
y: f32,
vx: f32,
vy: f32,
vida: f32,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var pool = PoolAllocator(Particula).init(gpa.allocator());
defer pool.deinit();
// Criar partículas
var particulas: [10]*Particula = undefined;
for (&particulas, 0..) |*p, i| {
p.* = try pool.obter();
p.*.* = .{
.x = @floatFromInt(i * 10),
.y = 0.0,
.vx = 1.0,
.vy = -9.8,
.vida = 1.0,
};
}
pool.status();
// Devolver algumas ao pool
for (particulas[0..5]) |p| {
pool.devolver(p);
}
pool.status();
// Obter novamente — reutiliza do pool, sem nova alocação
for (0..5) |_| {
const p = try pool.obter();
p.* = .{ .x = 0, .y = 0, .vx = 0, .vy = 0, .vida = 1.0 };
}
pool.status();
// Devolver todas
for (&particulas) |p| {
pool.devolver(p);
}
pool.status();
}
Combinando Allocators: O Padrão Composto
Na prática, você frequentemente combina allocators para obter o melhor de cada um:
const std = @import("std");
const AppAllocators = struct {
// GPA como base — detecta leaks
gpa: std.heap.GeneralPurposeAllocator(.{}),
// Arena para dados temporários por requisição
arena: std.heap.ArenaAllocator,
fn init() AppAllocators {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
return .{
.gpa = gpa,
.arena = std.heap.ArenaAllocator.init(gpa.allocator()),
};
}
// Allocator para dados de longa duração
fn permanente(self: *AppAllocators) std.mem.Allocator {
return self.gpa.allocator();
}
// Allocator para dados temporários
fn temporario(self: *AppAllocators) std.mem.Allocator {
return self.arena.allocator();
}
fn resetTemporario(self: *AppAllocators) void {
_ = self.arena.reset(.retain_capacity);
}
fn deinit(self: *AppAllocators) void {
self.arena.deinit();
_ = self.gpa.deinit();
}
};
pub fn main() !void {
var allocs = AppAllocators.init();
defer allocs.deinit();
// Dados permanentes
const config = try allocs.permanente().alloc(u8, 100);
defer allocs.permanente().free(config);
@memcpy(config[0..11], "app_config!");
// Dados temporários por operação
for (0..3) |i| {
allocs.resetTemporario();
const temp = try std.fmt.allocPrint(
allocs.temporario(),
"Operação temporária {d}",
.{i},
);
std.debug.print("{s}\n", .{temp});
// Sem necessidade de free individual — reset cuida disso
}
std.debug.print("Config permanente: {s}\n", .{config[0..11]});
}
Diretrizes para Custom Allocators
- Sempre implemente a vtable completa — mesmo que
resizeapenas retornefalse - Teste com o GPA como backing — para detectar bugs no seu allocator
- Documente as garantias — thread-safety, alinhamento, limites
- Considere usar
@returnAddress()para stack traces em modo debug - Prefira composição — combine allocators existentes antes de criar do zero
Próximos Passos
No próximo e último artigo da série, vamos aprender técnicas de debugging de memória para encontrar e corrigir problemas como leaks, use-after-free e buffer overflows.
Referências
- Arena Allocator (Artigo 3) — Artigo anterior
- Builtins de Memória — Funções built-in relacionadas
- Testes Avançados — Testando custom allocators