Zig Allocator Failure Modes
An allocator failure mode is the behavior a program shows when memory runs out. Good programs handle the OutOfMemory error instead of crashing.
Counting down to failure
The allocator allows two allocations. The third allocation reports OutOfMemory.
const std = @import("std");
const FailingAllocator = struct {
inner: std.mem.Allocator,
remaining: usize,
fn allocFn(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
if (self.remaining == 0) return null;
self.remaining -= 1;
return self.inner.rawAlloc(len, alignment, ret_addr);
}
fn resizeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawResize(memory, alignment, new_len, ret_addr);
}
fn remapFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawRemap(memory, alignment, new_len, ret_addr);
}
fn freeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
self.inner.rawFree(memory, alignment, ret_addr);
}
fn allocator(self: *FailingAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{ .alloc = allocFn, .resize = resizeFn, .remap = remapFn, .free = freeFn } };
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var failing = FailingAllocator{ .inner = gpa.allocator(), .remaining = 2 };
const alloc = failing.allocator();
const a = try alloc.alloc(u8, 4);
defer alloc.free(a);
const b = try alloc.alloc(u8, 4);
defer alloc.free(b);
if (alloc.alloc(u8, 4)) |extra| {
alloc.free(extra);
try out.print("allocated\n", .{});
} else |err| {
try out.print("{}\n", .{err});
}
}
Degrading to a smaller plan
The 100-byte request cannot be served. The program falls back to a 10-byte fixed buffer.
const std = @import("std");
const FailingAllocator = struct {
inner: std.mem.Allocator,
remaining: usize,
fn allocFn(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
if (self.remaining == 0) return null;
self.remaining -= 1;
return self.inner.rawAlloc(len, alignment, ret_addr);
}
fn resizeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawResize(memory, alignment, new_len, ret_addr);
}
fn remapFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawRemap(memory, alignment, new_len, ret_addr);
}
fn freeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
self.inner.rawFree(memory, alignment, ret_addr);
}
fn allocator(self: *FailingAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{ .alloc = allocFn, .resize = resizeFn, .remap = remapFn, .free = freeFn } };
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var failing = FailingAllocator{ .inner = gpa.allocator(), .remaining = 0 };
const alloc = failing.allocator();
const small: [10]u8 = undefined;
if (alloc.alloc(u8, 100)) |big| {
defer alloc.free(big);
try out.print("{d}\n", .{big.len});
} else |_| {
try out.print("{d}\n", .{small.len});
}
}
Propagating the error
The helper returns the allocation error to main. Main catches it and reports the failure.
const std = @import("std");
const FailingAllocator = struct {
inner: std.mem.Allocator,
remaining: usize,
fn allocFn(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
if (self.remaining == 0) return null;
self.remaining -= 1;
return self.inner.rawAlloc(len, alignment, ret_addr);
}
fn resizeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawResize(memory, alignment, new_len, ret_addr);
}
fn remapFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawRemap(memory, alignment, new_len, ret_addr);
}
fn freeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
self.inner.rawFree(memory, alignment, ret_addr);
}
fn allocator(self: *FailingAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{ .alloc = allocFn, .resize = resizeFn, .remap = remapFn, .free = freeFn } };
}
};
fn load(alloc: std.mem.Allocator, n: usize) ![]u8 {
return alloc.alloc(u8, n);
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var failing = FailingAllocator{ .inner = gpa.allocator(), .remaining = 0 };
const alloc = failing.allocator();
const data = load(alloc, 8) catch {
try out.print("load failed\n", .{});
return;
};
defer alloc.free(data);
try out.print("{d}\n", .{data.len});
}
Example
const std = @import("std");
const FailingAllocator = struct {
inner: std.mem.Allocator,
remaining: usize,
fn allocFn(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
if (self.remaining == 0) return null;
self.remaining -= 1;
return self.inner.rawAlloc(len, alignment, ret_addr);
}
fn resizeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawResize(memory, alignment, new_len, ret_addr);
}
fn remapFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
return self.inner.rawRemap(memory, alignment, new_len, ret_addr);
}
fn freeFn(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void {
const self: *FailingAllocator = @ptrCast(@alignCast(ctx));
self.inner.rawFree(memory, alignment, ret_addr);
}
fn allocator(self: *FailingAllocator) std.mem.Allocator {
return .{ .ptr = self, .vtable = &.{ .alloc = allocFn, .resize = resizeFn, .remap = remapFn, .free = freeFn } };
}
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var failing = FailingAllocator{ .inner = gpa.allocator(), .remaining = 2 };
const alloc = failing.allocator();
const header = try alloc.alloc(u8, 4);
defer alloc.free(header);
header[0] = 1;
const body = try alloc.alloc(u8, 4);
defer alloc.free(body);
body[0] = 2;
var parts: usize = 2;
if (alloc.alloc(u8, 4)) |footer| {
alloc.free(footer);
parts = 3;
} else |_| {}
try out.print("{d} {d} {d}\n", .{ header[0], body[0], parts });
}
Key points
- Failing allocator: a failing allocator is an allocator that returns OutOfMemory after a set number of successful allocations.
- A countdown allocator is useful for testing how a program handles running out of memory.
- Catching OutOfMemory lets the program degrade to a smaller plan.
- A fixed buffer is a safe smaller plan when heap memory is gone.
- Errors should travel up through helper functions to the caller.
- Cleanup code must still run on the failure path.
The allocator allows two allocations. The header and body succeed. The footer allocation fails. The program continues with two parts instead of stopping.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A program's third allocation returns OutOfMemory. What is the graceful response?