Skip to content

captkirk88/zevy-mem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zevy-mem

A collection of memory allocators and utilities for Zig.

Zig Version

Features

  • 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

Installation

Add to your build.zig.zon:

zig fetch --save git+https://github.com/captkirk88/zevy-mem

Then in your build.zig:

const zevy_mem = b.dependency("zevy_mem", .{});
exe.root_module.addImport("zevy_mem", zevy_mem.module("zevy_mem"));

Quick Start

Stack Allocator

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(),
});

Debug Allocator (Leak Detection)

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});

Safe Allocator (Leak Detection & Double-Free Prevention)

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 remain

Pool Allocator

const 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()});

Scoped Allocator

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;

Nested Scopes

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();

Guarded Allocator (Buffer Overflow Detection)

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 page

Examples

Game Frame Allocator Pattern

const 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;
}

Component Pool for ECS

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;

Debug Memory Tracking

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});
}

Performance

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.

Limitations

  • 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

Contributing

Contributions, issues, and feature requests are welcome! Please open an issue or submit a pull request.

Related Projects

About

A bucket of different Zig allocators for different uses. Also, Rc, Arc, and Mutex types.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages