En la mayoría de los lenguajes modernos, la memoria es algo que ocurre en segundo plano. El GC la gestiona, el runtime la reserva y libera, y tú te olvidas del asunto hasta que hay una fuga o el sistema va lento. Zig toma la dirección contraria: la memoria es una decisión explícita que tomas en cada punto del código donde la necesitas.
El principio del allocator explícito
En Zig no existe una función global malloc ni un heap implícito. Cuando una función necesita memoria dinámica, recibe un parámetro de tipo std.mem.Allocator. Esta interfaz es la abstracción que Zig usa para desacoplar «quién pide la memoria» de «quién la gestiona».
El resultado práctico es que puedes cambiar el allocator de toda una biblioteca sin tocar su código. En tests usas std.testing.allocator, que detecta fugas. En producción usas el que mejor encaja con tu caso de uso.
const std = @import("std");
// Esta función no sabe de dónde viene la memoria, solo la usa
fn crearLista(allocator: std.mem.Allocator) !std.ArrayList(u32) {
var lista = std.ArrayList(u32).init(allocator);
try lista.append(1);
try lista.append(2);
try lista.append(3);
return lista;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var lista = try crearLista(gpa.allocator());
defer lista.deinit();
for (lista.items) |n| {
std.debug.print("{d}n", .{n});
}
}
Los allocators principales de la biblioteca estándar
GeneralPurposeAllocator
Es el allocator de uso general para desarrollo. En modo Debug y ReleaseSafe detecta dobles liberaciones y fugas de memoria, e imprime por dónde se asignó cada bloque perdido. En ReleaseFast y ReleaseSmall elimina esas comprobaciones y se comporta como page_allocator con algo de overhead adicional por los metadatos.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const result = gpa.deinit();
if (result == .leak) @panic("¡Hay fugas de memoria!");
}
const allocator = gpa.allocator();
ArenaAllocator
El arena allocator reserva memoria en bloques grandes y la libera toda de golpe al destruir el arena. Es ideal cuando tienes una fase de trabajo (parsear un fichero, construir una respuesta HTTP, procesar una petición) donde asignas mucha memoria temporal y la liberas al terminar. No hay que llamar a free por cada asignación individual.
const std = @import("std");
pub fn procesarPeticion(backing_allocator: std.mem.Allocator) !void {
var arena = std.heap.ArenaAllocator.init(backing_allocator);
defer arena.deinit(); // libera TODA la memoria del arena de golpe
const alloc = arena.allocator();
// Todas estas asignaciones se liberan con el defer anterior
const buffer = try alloc.alloc(u8, 4096);
const lista = try std.ArrayList([]const u8).initCapacity(alloc, 64);
_ = buffer;
_ = lista;
// ... procesar la petición ...
std.debug.print("Petición procesadan", .{});
}
El patrón arena es muy común en servidores web escritos en Zig: cada petición tiene su propio arena, y al terminar de responder se destruye entero sin contabilizar nada.
FixedBufferAllocator
Asigna memoria desde un buffer de tamaño fijo que tú proporcionas. Cuando se agota el buffer, devuelve error.OutOfMemory. Útil para sistemas embebidos o para partes del código donde quieres limitar estrictamente cuánta memoria se puede usar.
var buffer: [8192]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buffer); const allocator = fba.allocator(); // Solo puede usar hasta 8192 bytes const datos = try allocator.alloc(u8, 100); defer allocator.free(datos);
page_allocator
Llama directamente a mmap (o equivalente en Windows) para cada asignación. Es rápido para asignaciones grandes, pero muy ineficiente para muchas asignaciones pequeñas porque cada llamada tiene overhead de syscall. Úsalo como backing allocator para el GeneralPurposeAllocator o el ArenaAllocator, no directamente para asignaciones individuales pequeñas.
testing.allocator
Solo disponible en tests. Detecta cualquier fuga de memoria y hace fallar el test si la hay. Es la razón por la que en Zig es habitual que los tests detecten fugas sin ningún tooling adicional.
const std = @import("std");
const testing = std.testing;
test "sin fugas de memoria" {
const allocator = testing.allocator;
const lista = try std.ArrayList(u32).initCapacity(allocator, 10);
defer lista.deinit(); // sin este defer, el test falla
try testing.expect(lista.capacity == 10);
}
Componer allocators
Una de las cosas más interesantes del sistema es que puedes combinar allocators. Un ArenaAllocator necesita un «backing allocator» del que toma páginas; ese backing allocator puede ser un GeneralPurposeAllocator, el page_allocator u otro arena. Esta composición da mucha flexibilidad sin añadir ningún overhead en tiempo de compilación si el tipo es conocido.
// Arena respaldado por el GPA para desarrollo
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const alloc = arena.allocator();
Memoria en la pila vs. en el heap
Zig tiene tipos de datos que viven en la pila por defecto. Los arrays de tamaño fijo, los structs y los valores escalares se asignan en la pila automáticamente. Solo cuando el tamaño no se conoce en tiempo de compilación o cuando los datos deben sobrevivir al scope actual es necesario el heap.
// En la pila: array de tamaño conocido en tiempo de compilación var arr: [10]u32 = undefined; arr[0] = 42; // En el heap: slice de tamaño dinámico const arr_dyn = try allocator.alloc(u32, tamanyo_dinamico); defer allocator.free(arr_dyn);
Esta distinción explícita entre pila y heap, combinada con el sistema de allocators, hace que el comportamiento de memoria de un programa Zig sea completamente predecible leyendo el código fuente. No hay sorpresas del GC ni de un runtime que asigna a tus espaldas.
Errdefer: limpiar en caso de error
Cuando inicializas varios recursos y uno de ellos falla, necesitas limpiar los que ya inicializaste. errdefer ejecuta su bloque solo si la función termina con un error, lo que permite escribir código de limpieza sin anidar condicionales.
fn inicializar(allocator: std.mem.Allocator) !MiStruct {
const buffer = try allocator.alloc(u8, 1024);
errdefer allocator.free(buffer); // solo se ejecuta si hay error
const tabla = try allocator.alloc(u32, 256);
errdefer allocator.free(tabla); // ídem
// si llegamos aquí, todo fue bien; transferimos ownership al struct
return MiStruct{ .buffer = buffer, .tabla = tabla };
}
El patrón defer / errdefer junto con el sistema de allocators explícitos es la base de la gestión de memoria en Zig. Una vez que lo interiorizas, escribir código sin fugas es la norma, no la excepción. Si quieres ver cómo este sistema encaja con el resto del lenguaje, en el artículo de introducción a Zig tienes el contexto completo del lenguaje.
Imagen: Pexels / Valentine Tanasovich
