Substituir malloc/free por Allocators Zig

Introdução

Uma das maiores diferenças entre C e Zig é o gerenciamento de memória. Em C, malloc e free são funções globais — qualquer código pode alocar memória de qualquer lugar. Em Zig, toda alocação é feita através de um allocator passado explicitamente como parâmetro.

Este guia mostra como substituir os padrões de malloc/free por allocators Zig. Para detalhes sobre cada allocator, consulte ArenaAllocator, GeneralPurposeAllocator e FixedBufferAllocator.

Pré-requisitos

O Problema com malloc/free

Em C, alocação de memória é invisível e global:

// Quem libera? Quando? Nenhuma informação no tipo.
char* ler_arquivo(const char* caminho) {
    FILE* f = fopen(caminho, "r");
    // ...
    char* conteudo = malloc(tamanho);
    fread(conteudo, 1, tamanho, f);
    fclose(f);
    return conteudo; // O chamador precisa saber que deve chamar free()
}

Problemas comuns:

  • Double free: Liberar a mesma memória duas vezes
  • Use-after-free: Usar memória já liberada
  • Memory leak: Esquecer de liberar
  • Invisibilidade: Não é claro quais funções alocam
  • Não testável: Não dá para substituir o alocador em testes

A Solução: Allocators Explícitos

Em Zig, o allocator é um parâmetro explícito:

fn lerArquivo(allocator: std.mem.Allocator, caminho: []const u8) ![]u8 {
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();
    return arquivo.readToEndAlloc(allocator, 1024 * 1024);
}

// O chamador SABE que memória foi alocada e é responsável por liberar:
const conteudo = try lerArquivo(allocator, "dados.txt");
defer allocator.free(conteudo);

Mapeamento malloc/free para Zig

Alocação Simples

// C
int* ptr = (int*)malloc(sizeof(int));
*ptr = 42;
free(ptr);
// Zig
const ptr = try allocator.create(i32);
defer allocator.destroy(ptr);
ptr.* = 42;

Alocação de Array

// C
int* arr = (int*)malloc(100 * sizeof(int));
memset(arr, 0, 100 * sizeof(int));
free(arr);
// Zig
const arr = try allocator.alloc(i32, 100);
defer allocator.free(arr);
@memset(arr, 0);

Realloc

// C
int* arr = malloc(10 * sizeof(int));
arr = realloc(arr, 20 * sizeof(int));
free(arr);
// Zig
var arr = try allocator.alloc(i32, 10);
arr = try allocator.realloc(arr, 20);
defer allocator.free(arr);

Strings Dinâmicas

// C
char* msg = malloc(256);
snprintf(msg, 256, "Olá, %s!", nome);
free(msg);
// Zig
const msg = try std.fmt.allocPrint(allocator, "Olá, {s}!", .{nome});
defer allocator.free(msg);

Escolhendo o Allocator Certo

GeneralPurposeAllocator (GPA)

Equivalente mais próximo ao malloc, com detecção de bugs em debug:

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
    const status = gpa.deinit();
    if (status == .leak) {
        std.debug.print("Vazamento de memória detectado!\n", .{});
    }
}
const allocator = gpa.allocator();

Use para: aplicações de uso geral, desenvolvimento, detecção de bugs.

ArenaAllocator

Aloca tudo e libera de uma vez. Ideal para processamento em fases:

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Libera TUDO de uma vez

const allocator = arena.allocator();
// Alocar livremente sem se preocupar com free individual
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
const c = try allocator.alloc(u8, 300);
// Tudo liberado no arena.deinit()

Use para: parsing, processamento de requisições, operações com lifecycle definido. Veja ArenaAllocator.

FixedBufferAllocator

Aloca de um buffer fixo na stack, sem heap:

var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();

const dados = try allocator.alloc(u8, 100);
// Sem heap allocation — tudo na stack

Use para: sistemas embarcados, hot paths, quando heap não é desejável. Veja FixedBufferAllocator.

page_allocator

Aloca diretamente do sistema operacional em páginas:

const allocator = std.heap.page_allocator;
const dados = try allocator.alloc(u8, 4096);
defer allocator.free(dados);

Use para: alocações grandes, backing allocator para arenas.

Padrão de Migração

Passo 1: Identificar Funções que Alocam

Em C, busque por malloc, calloc, realloc, strdup, e qualquer função que retorne ponteiros heap-alocados.

Passo 2: Adicionar Allocator como Parâmetro

// Antes (tradução direta de C)
fn processar() ![]u8 {
    return std.heap.c_allocator.alloc(u8, 100); // RUIM: alocador hardcoded
}

// Depois (idiomático Zig)
fn processar(allocator: std.mem.Allocator) ![]u8 {
    return allocator.alloc(u8, 100); // BOM: alocador injetado
}

Passo 3: Usar defer/errdefer

fn criarRecurso(allocator: std.mem.Allocator) !*Recurso {
    const r = try allocator.create(Recurso);
    errdefer allocator.destroy(r); // Libera se o resto falhar

    r.* = .{
        .dados = try allocator.alloc(u8, 1024),
        .estado = .inicializando,
    };
    errdefer allocator.free(r.dados);

    try r.inicializar();
    return r;
}

Veja Padrões Errdefer.

Passo 4: Testar com Allocators de Teste

test "processar não vaza memória" {
    // testing.allocator detecta leaks automaticamente
    const resultado = try processar(std.testing.allocator);
    defer std.testing.allocator.free(resultado);

    try std.testing.expect(resultado.len == 100);
}

Veja Testes com Allocator e Detectar Vazamentos.

Padrões Avançados

Arena por Requisição

fn processarRequisicao(req: Requisicao) !Resposta {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    // Todas as alocações desta requisição usam a arena
    const parsed = try parsearBody(alloc, req.body);
    const resultado = try executarLogica(alloc, parsed);
    return criarResposta(resultado);
}

Struct com Allocator

const MinhaStruct = struct {
    allocator: std.mem.Allocator,
    dados: []u8,

    pub fn init(allocator: std.mem.Allocator) !MinhaStruct {
        return .{
            .allocator = allocator,
            .dados = try allocator.alloc(u8, 256),
        };
    }

    pub fn deinit(self: *MinhaStruct) void {
        self.allocator.free(self.dados);
    }
};

Conclusão

Substituir malloc/free por allocators Zig melhora significativamente a segurança e testabilidade do código. O allocator como parâmetro explícito torna cada alocação visível e permite trocar estratégias de memória sem mudar a lógica do programa.

Para mais sobre gerenciamento de memória, veja Gerenciamento de Memória em Zig. Para migração de projeto, consulte Guia de Migração: C para Zig.

Continue aprendendo Zig

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