Como Detectar Vazamentos de Memória em Zig

Introdução

Vazamentos de memória (memory leaks) ocorrem quando seu programa aloca memória mas nunca a libera. Em Zig, o GeneralPurposeAllocator oferece detecção automática de vazamentos, reportando exatamente onde a memória foi alocada. Isso é uma das grandes vantagens do sistema de alocadores explícitos do Zig.

Nesta receita, você aprenderá a identificar, diagnosticar e corrigir vazamentos de memória.

Pré-requisitos

Detectar Vazamentos com GPA

O GPA detecta automaticamente memória não liberada ao chamar deinit():

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        switch (status) {
            .ok => std.debug.print("Sem vazamentos de memória!\n", .{}),
            .leak => std.debug.print("ALERTA: Vazamento de memória detectado!\n", .{}),
        }
    }
    const allocator = gpa.allocator();

    // Alocação 1: corretamente liberada
    const dados1 = try allocator.alloc(u8, 100);
    defer allocator.free(dados1);

    // Alocação 2: corretamente liberada
    const dados2 = try allocator.alloc(i32, 50);
    defer allocator.free(dados2);

    std.debug.print("Programa executado com sucesso\n", .{});
}

Saída esperada

Programa executado com sucesso
Sem vazamentos de memória!

Exemplo de Vazamento e Correção

Identifique o problema e veja a correção:

const std = @import("std");

fn funcaoComVazamento(allocator: std.mem.Allocator) ![]u8 {
    // ERRADO: alocação intermediária nunca liberada
    const temp = try allocator.alloc(u8, 50);
    @memset(temp, 'X');

    // O resultado é uma nova alocação, mas 'temp' foi esquecido
    const resultado = try allocator.dupe(u8, temp[0..10]);
    // Faltou: allocator.free(temp);

    return resultado;
}

fn funcaoCorrigida(allocator: std.mem.Allocator) ![]u8 {
    // CORRETO: liberar alocação intermediária
    const temp = try allocator.alloc(u8, 50);
    defer allocator.free(temp); // Libera temp ao sair da função
    @memset(temp, 'X');

    const resultado = try allocator.dupe(u8, temp[0..10]);
    return resultado;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("Vazamento detectado!\n", .{});
        } else {
            std.debug.print("Sem vazamentos!\n", .{});
        }
    }
    const allocator = gpa.allocator();

    // Usar a versão corrigida
    const resultado = try funcaoCorrigida(allocator);
    defer allocator.free(resultado);

    std.debug.print("Resultado: {s}\n", .{resultado});
}

Padrão errdefer para Evitar Vazamentos em Erros

const std = @import("std");

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

    pub fn init(allocator: std.mem.Allocator, nome: []const u8, tamanho: usize) !Recurso {
        const nome_copia = try allocator.dupe(u8, nome);
        errdefer allocator.free(nome_copia); // Libera se a próxima alocação falhar

        const dados = try allocator.alloc(u8, tamanho);
        errdefer allocator.free(dados); // Libera se algo mais falhar

        @memset(dados, 0);

        return Recurso{
            .nome = nome_copia,
            .dados = dados,
            .allocator = allocator,
        };
    }

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

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("Vazamento detectado!\n", .{});
        } else {
            std.debug.print("Sem vazamentos!\n", .{});
        }
    }
    const allocator = gpa.allocator();

    var recurso = try Recurso.init(allocator, "meu-recurso", 256);
    defer recurso.deinit();

    std.debug.print("Recurso '{s}' criado com {d} bytes\n", .{ recurso.nome, recurso.dados.len });
}

Checklist Anti-Vazamento

Padrões que ajudam a prevenir vazamentos:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Regra 1: defer imediato após alocação
    const a = try allocator.alloc(u8, 100);
    defer allocator.free(a);

    // Regra 2: errdefer para funções que podem falhar
    var lista = std.ArrayList(u8).init(allocator);
    defer lista.deinit();

    // Regra 3: deinit em structs que possuem memória
    var mapa = std.StringHashMap(u32).init(allocator);
    defer mapa.deinit();

    // Regra 4: liberar resultado de toOwnedSlice
    try lista.appendSlice("teste");
    const owned = try lista.toOwnedSlice();
    defer allocator.free(owned);

    // Regra 5: liberar resultado de allocPrint
    const msg = try std.fmt.allocPrint(allocator, "valor: {d}", .{42});
    defer allocator.free(msg);

    std.debug.print("Todas as alocações gerenciadas corretamente\n", .{});
    std.debug.print("  a: {d} bytes\n", .{a.len});
    std.debug.print("  owned: {s}\n", .{owned});
    std.debug.print("  msg: {s}\n", .{msg});
}

Teste Automatizado para Vazamentos

const std = @import("std");
const testing = std.testing;

fn processarDados(allocator: std.mem.Allocator) ![]u8 {
    var resultado = std.ArrayList(u8).init(allocator);
    errdefer resultado.deinit();

    try resultado.appendSlice("processado");
    return resultado.toOwnedSlice();
}

test "processarDados não vaza memória" {
    // testing.allocator é um GPA que falha o teste se houver vazamento
    const resultado = try processarDados(testing.allocator);
    defer testing.allocator.free(resultado);

    try testing.expectEqualStrings("processado", resultado);
}

test "ArrayList sem vazamento" {
    var lista = std.ArrayList(i32).init(testing.allocator);
    defer lista.deinit();

    try lista.append(1);
    try lista.append(2);
    try lista.append(3);

    try testing.expectEqual(@as(usize, 3), lista.items.len);
}

Execute os testes com zig test arquivo.zig. O testing.allocator automaticamente detecta vazamentos e faz o teste falhar.

Dicas e Boas Práticas

  1. defer imediato: Sempre coloque defer free() na linha seguinte à alocação.

  2. errdefer em funções falíveis: Se uma função pode retornar erro após alocar, use errdefer para limpar.

  3. testing.allocator nos testes: Ele detecta vazamentos automaticamente nos testes unitários.

  4. GPA em modo debug: Use GPA durante desenvolvimento e troque para page_allocator ou c_allocator em produção.

  5. ArenaAllocator simplifica: Quando muitas alocações têm o mesmo tempo de vida, use ArenaAllocator.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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