A collection of memory allocators and utilities for Zig.
- Safe Allocator: Allocation tracking with leak detection, double-free prevention, and allocation statistics
- Guarded Allocator: Wrap allocators that don't segfault to detect buffer overflows using guard pages
- Stack Allocator: Fast bump allocator with LIFO freeing, supports heap-free operation with external buffers
- Debug Allocator: Full allocation tracking with leak detection, statistics, and source location resolution
- Pool Allocator: O(1) fixed-size object allocation with zero fragmentation
- Scoped Allocator: RAII-style allocation scopes with automatic cleanup
- Counting Allocator: Simple wrapper for tracking allocation counts and bytes
- Memory-Mapped File Allocator: Allocator backed by memory-mapped file
- Memory Utilities: Alignment helpers, byte formatting, and memory region tools
- Zero External Dependencies: Pure Zig implementation with no external dependencies
- Safe Pointers: Reference-counted pointers (Rc and Arc) for safe memory management
- Mutex: Simple mutex implementation for thread safety
Add to your build.zig.zon:
zig fetch --save git+https://github.com/captkirk88/zevy-memThen in your build.zig:
const zevy_mem = b.dependency("zevy_mem", .{});
exe.root_module.addImport("zevy_mem", zevy_mem.module("zevy_mem"));const mem = @import("zevy_mem");
// Create with external buffer (no heap)
var buffer: [4096]u8 = undefined;
var stack = mem.StackAllocator.initBuffer(&buffer);
const allocator = stack.allocator();
// Allocate memory
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
// Check usage
std.debug.print("Used: {} bytes, Remaining: {} bytes\n", .{
stack.bytesUsed(),
stack.bytesRemaining(),
});const mem = @import("zevy_mem");
var debug = mem.DebugAllocator(64).init(std.heap.page_allocator);
const allocator = debug.allocator();
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
// Intentionally leak 'a'
allocator.free(b);
// Check for leaks
if (debug.detectLeaks()) {
debug.dumpLeaks(); // Prints detailed leak report
}
// Get statistics
const stats = debug.getStats();
std.debug.print("Peak allocations: {}\n", .{stats.peak_active_allocations});const mem = @import("zevy_mem");
var safe = mem.SafeAllocator.init(std.heap.page_allocator, std.testing.allocator);
defer safe.deinit();
const allocator = safe.allocator();
const data = try allocator.alloc(u8, 100);
allocator.free(data);
// Attempt double-free (will panic)
allocator.free(data); // Panic: Double free detected
// defer safe.deinit() will check for leaks and panic if any remainconst mem = @import("zevy_mem");
const Entity = struct {
id: u32,
x: f32,
y: f32,
active: bool,
};
// Create pool with external buffer
var buffer: [100]mem.PoolAllocator(Entity).Slot = undefined;
var pool = mem.PoolAllocator(Entity).initBuffer(&buffer); // .initHeap(...) for heap allocation
// O(1) allocation
const entity = pool.create(.{
.id = 1,
.x = 0.0,
.y = 0.0,
.active = true,
}).?;
// O(1) deallocation
pool.free(entity);
std.debug.print("Pool: {}/{} slots used\n", .{pool.count(), pool.capacity()});const mem = @import("zevy_mem");
var buffer: [4096]u8 = undefined;
var stack = mem.StackAllocator.initBuffer(&buffer);
// Permanent allocation
const permanent = try stack.allocator().alloc(u8, 100);
// Scoped temporary allocations
{
var scope = mem.ScopedAllocator.begin(&stack); // Save current state
defer scope.end() catch {}; // Restore state, freeing all scope allocations
const temp1 = try scope.allocator().alloc(u8, 500);
const temp2 = try scope.allocator().alloc(u8, 300);
// Use temp1 and temp2...
_ = temp1;
_ = temp2;
}
// Only 'permanent' remains allocated
std.debug.print("Bytes used: {}\n", .{stack.bytesUsed()}); // 100
_ = permanent;const mem = @import("zevy_mem");
var buffer: [4096]u8 = undefined;
var stack = mem.StackAllocator.initBuffer(&buffer);
var nested = mem.NestedScope(8).init(&stack);
// Level 0
_ = try nested.allocator().alloc(u8, 100);
// Push level 1
try nested.push();
_ = try nested.allocator().alloc(u8, 200);
// Push level 2
try nested.push();
_ = try nested.allocator().alloc(u8, 300);
// Pop back to level 1 (frees 300 bytes from level 2)
try nested.pop();
std.debug.print("Depth: {}, Bytes: {}\n", .{nested.currentDepth(), stack.bytesUsed()});
// Pop back to level 0 (frees 200 bytes from level 1)
try nested.pop();const mem = @import("zevy_mem");
// Add guard pages around allocations to detect overflows
var guarded = try mem.GuardedAllocator.init(std.heap.smp_allocator, std.testing.allocator, 1);
defer guarded.deinit();
const allocator = guarded.allocator();
// Allocate with guard pages
const buf = try allocator.alloc(u8, 100);
defer allocator.free(buf);
// Overflowing beyond allocated size will cause segmentation fault
// buf[100] = 42; // Would segfault due to guard pageconst mem = @import("zevy_mem");
const FrameAllocator = struct {
stack: mem.StackAllocator,
buffer: [1024 * 1024]u8, // 1MB per frame
pub fn init() FrameAllocator {
var self = FrameAllocator{
.stack = undefined,
.buffer = undefined,
};
self.stack = mem.StackAllocator.initBuffer(&self.buffer);
return self;
}
pub fn allocator(self: *FrameAllocator) std.mem.Allocator {
return self.stack.allocator();
}
pub fn reset(self: *FrameAllocator) void {
self.stack.reset();
}
};
// Usage in game loop
var frame_alloc = FrameAllocator.init();
while (running) {
// All frame allocations automatically cleaned up
defer frame_alloc.reset();
const temp_data = try frame_alloc.allocator().alloc(u8, 1000);
// Use temp_data for this frame...
_ = temp_data;
}const mem = @import("zevy_mem");
const Position = struct { x: f32, y: f32, z: f32 };
const Velocity = struct { x: f32, y: f32, z: f32 };
const ComponentPools = struct {
positions: mem.PoolAllocator(Position),
velocities: mem.PoolAllocator(Velocity),
pos_buffer: [1000]mem.PoolAllocator(Position).Slot,
vel_buffer: [1000]mem.PoolAllocator(Velocity).Slot,
pub fn init() ComponentPools {
var self: ComponentPools = undefined;
self.positions = mem.PoolAllocator(Position).initBuffer(&self.pos_buffer);
self.velocities = mem.PoolAllocator(Velocity).initBuffer(&self.vel_buffer);
return self;
}
};
var pools = ComponentPools.init();
const pos = pools.positions.create(.{ .x = 0, .y = 0, .z = 0 }).?;
const vel = pools.velocities.create(.{ .x = 1, .y = 0, .z = 0 }).?;
_ = pos;
_ = vel;const mem = @import("zevy_mem");
pub fn runWithMemoryTracking() !void {
var buffer: [64 * 1024]u8 = undefined;
var debug = mem.DebugStackAllocator(256).initBuffer(&buffer);
defer {
if (debug.detectLeaks()) {
debug.dumpLeaks();
@panic("Memory leaks detected!");
}
}
const allocator = debug.allocator();
// Your code here...
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
// Print final stats
const stats = debug.getStats();
std.debug.print("Peak memory: {} bytes\n", .{stats.peak_bytes_used});
std.debug.print("Total allocations: {}\n", .{stats.total_allocations});
}| Allocator | Alloc | Free | Memory Overhead |
|---|---|---|---|
| SafeAllocator | O(1) | O(1) | ~32 bytes/allocation |
| GuardedAllocator | O(1) | O(1) | 2 * guard_pages * page_size per allocation |
| StackAllocator | O(1) | O(1)* | 0 bytes |
| DebugAllocator | O(1) | O(n)** | ~72 bytes/allocation |
| PoolAllocator | O(1) | O(1) | max(sizeof(T), 8) per slot*** |
| ScopedAllocator | O(1) | O(1) | 24 bytes per scope |
| NestedScope | O(1) | O(1) | 16 bytes per depth level |
| CountingAllocator | O(1) | O(1) | 24 bytes (wrapper state) |
| MmapAllocator | O(1) | O(1) | O(1) allocation/lookup with file-backed pages; working-set grows only as pages are touched |
* Only LIFO frees reclaim memory
** n = number of tracked allocations (linear search in findAllocation)
*** Slot size is @max(@sizeOf(T), @sizeOf(?*anyopaque)) to accommodate the free list pointer
The MmapAllocator runtime-vs-page allocator usage test measures working-set deltas and shows how the mmap-backed allocator touches far less resident memory than a straight heap allocation of the same size.
- LIFO Freeing: Stack allocators only reclaim memory for the most recent allocation
- Fixed Capacity: Pool and debug allocators have compile-time limits
- No Thread Safety: All allocators are single-threaded
- Buffer Ownership: Caller manages buffer lifetime
- SafeAllocator: Runtime checks with panics; not suitable for production error handling
- GuardedAllocator: High memory overhead from guard pages; segfaults may not provide detailed error info
Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request.
- zevy-ecs - Entity Component System framework
- zevy-reflect - Reflection and change detection