Memory Leak em Zig — O que é e Como Evitar

Memory Leak em Zig — O que é e Como Evitar

Definição

Um memory leak (vazamento de memória) ocorre quando um programa aloca memória no heap mas nunca a libera. Com o tempo, a memória disponível diminui, podendo levar a lentidão, OutOfMemory e, eventualmente, falha total do programa.

Em Zig, onde o gerenciamento de memória é manual, memory leaks são um risco real. Porém, a linguagem oferece ferramentas excelentes para detectá-los e preveni-los: o padrão defer e o GeneralPurposeAllocator com detecção integrada.

Por que Memory Leaks Importam

  1. Degradação progressiva: O programa fica cada vez mais lento até falhar.
  2. Servidores de longa duração: Leaks pequenos se acumulam ao longo de horas/dias.
  3. Recursos limitados: Em embarcados, qualquer leak é crítico.
  4. Difícil diagnóstico: Leaks muitas vezes só se manifestam em produção.

Exemplo Prático

Memory Leak Simples

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) @panic("LEAK DETECTADO!");
    }

    const allocator = gpa.allocator();

    // LEAK! Alocamos mas nunca liberamos
    const dados = try allocator.alloc(u8, 1024);
    _ = dados;

    // Ao sair, gpa.deinit() detecta o leak
}

Corrigido com Defer

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

    const allocator = gpa.allocator();

    const dados = try allocator.alloc(u8, 1024);
    defer allocator.free(dados); // Agora está correto!

    // Usar dados...
}

Leak em Estruturas Aninhadas

const std = @import("std");

const Usuario = struct {
    nome: []u8,
    email: []u8,
    allocator: std.mem.Allocator,

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

fn criarUsuario(allocator: std.mem.Allocator) !Usuario {
    const nome = try allocator.dupe(u8, "Maria Silva");
    errdefer allocator.free(nome); // Libera nome se email falhar

    const email = try allocator.dupe(u8, "maria@email.com");
    // Sem errdefer para email — se chegarmos aqui, tudo OK

    return Usuario{
        .nome = nome,
        .email = email,
        .allocator = allocator,
    };
}

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

    var usuario = try criarUsuario(gpa.allocator());
    defer usuario.deinit(); // Libera TODOS os campos internos
}

Detecção em Testes

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

test "função não vaza memória" {
    // testing.allocator detecta leaks automaticamente
    var lista = std.ArrayList(u8).init(testing.allocator);
    defer lista.deinit(); // Sem isto, o teste FALHA

    try lista.appendSlice("Zig Brasil");
    try testing.expectEqual(@as(usize, 10), lista.items.len);
}

Padrões de Prevenção

PadrãoDescrição
defer freeLiberar imediatamente após alocar
errdefer freeLiberar em caso de erro parcial
deinit() em structsMétodos que liberam todos os campos
Arena AllocatorLiberar tudo de uma vez
GPA em testesDetecção automática em cada teste

Causas Comuns de Leaks

// 1. Sobrescrever ponteiro sem liberar
var ptr = try allocator.alloc(u8, 100);
ptr = try allocator.alloc(u8, 200); // LEAK: primeiro alloc perdido!

// 2. Return antecipado sem cleanup
const dados = try allocator.alloc(u8, 100);
if (condicao) return error.Falha; // LEAK: dados não liberados
allocator.free(dados);

// 3. Exceção em lista de inicialização
// (Usar errdefer para proteger)

Usando o Arena Allocator para Eliminar Leaks

O ArenaAllocator é uma estratégia poderosa: você aloca livremente, e libera tudo de uma vez ao final. É ideal para processamento de requisições, parsing ou qualquer fluxo com tempo de vida bem definido:

const std = @import("std");

pub fn processarRequisicao(backing: std.mem.Allocator) !void {
    var arena = std.heap.ArenaAllocator.init(backing);
    defer arena.deinit(); // Libera TUDO que foi alocado dentro

    const allocator = arena.allocator();

    // Pode alocar à vontade — tudo vai ser liberado junto
    const nome = try allocator.dupe(u8, "Maria");
    const sobrenome = try allocator.dupe(u8, "Silva");
    const completo = try std.fmt.allocPrint(allocator, "{s} {s}", .{ nome, sobrenome });

    std.debug.print("Nome: {s}\n", .{completo});
    // Nenhum free individual necessário!
}

Com o Arena, a preocupação com leaks individuais desaparece — basta garantir o defer arena.deinit().

Boas Práticas

  • defer logo após a alocação: O defer allocator.free(dados) deve aparecer imediatamente na linha seguinte à alocação, antes de qualquer código que possa falhar.
  • deinit() para structs com estado interno: Todo tipo que aloca internamente (ArrayList, HashMap, StringHashMap) deve ter um deinit() correspondente chamado com defer.
  • Teste com testing.allocator: Em testes unitários, use std.testing.allocator — ele acusa qualquer leak ao final do teste, tornando a verificação automática.
  • Arena para escopos de curta duração: Para processamento de uma requisição ou parsing de um documento, um Arena elimina a necessidade de rastrear cada alocação individualmente.

Armadilhas Comuns

  • Confiar apenas em defer: Se a alocação e a liberação estão em funções diferentes, defer não resolve. Implemente deinit().
  • ArrayList/HashMap sem deinit: Coleções da std guardam memória internamente. Sempre chame deinit().
  • Leak em loops: Alocar dentro de um loop sem liberar a cada iteração acumula leaks.
  • Ignorar erros de alocação parcial: Sem errdefer, falhas no meio de inicialização vazam recursos já alocados.

Termos Relacionados

Tutoriais Relacionados

Continue aprendendo Zig

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