Zig Memory Management Guide
Core Principle: Every allocation must have a corresponding deallocation. Use
deferfor normal cleanup,errdeferfor error path cleanup.
This skill ensures safe memory management in Zig, preventing memory leaks and use-after-free bugs.
Official Documentation:
- Memory Allocators: https://ziglang.org/documentation/0.15.2/#Memory
- std.mem: https://ziglang.org/documentation/0.15.2/std/#std.mem
Related Skills:
zig-0.15: API changes including ArrayList allocator parametersolana-sdk-zig: Solana-specific memory constraints (32KB heap)
References
Detailed allocator patterns and examples:
| Document | Path | Content |
|---|---|---|
| Allocator Patterns | references/allocator-patterns.md | GPA, Arena, FixedBuffer, Testing allocators, BPF allocator |
Resource Cleanup Pattern (Critical)
Always Use defer for Cleanup
// ❌ WRONG - No cleanup
fn process(allocator: Allocator) !void {
const buffer = try allocator.alloc(u8, 1024);
// ... use buffer ...
// Memory leaked!
}
// ✅ CORRECT - Immediate defer
fn process(allocator: Allocator) !void {
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer); // Always freed
// ... use buffer ...
}
Use errdefer for Error Path Cleanup
// ❌ WRONG - Leak on error
fn createResource(allocator: Allocator) !*Resource {
const res = try allocator.create(Resource);
res.data = try allocator.alloc(u8, 100); // If this fails, res leaks!
try res.initialize(); // If this fails, both leak!
return res;
}
// ✅ CORRECT - errdefer for each allocation
fn createResource(allocator: Allocator) !*Resource {
const res = try allocator.create(Resource);
errdefer allocator.destroy(res); // Freed only on error
res.data = try allocator.alloc(u8, 100);
errdefer allocator.free(res.data); // Freed only on error
try res.initialize(); // If this fails, errdefers run
return res; // Success - errdefers don't run
}
ArrayList Memory Management (Zig 0.15+)
Critical: In Zig 0.15, ArrayList methods require explicit allocator:
// ❌ WRONG (0.13/0.14 style)
var list = std.ArrayList(T).init(allocator);
defer list.deinit();
try list.append(item);
// ✅ CORRECT (0.15+ style)
var list = try std.ArrayList(T).initCapacity(allocator, 16);
defer list.deinit(allocator); // Allocator required!
try list.append(allocator, item); // Allocator required!
try list.appendSlice(allocator, items);
try list.ensureTotalCapacity(allocator, n);
const owned = try list.toOwnedSlice(allocator);
defer allocator.free(owned); // Caller owns the slice
ArrayList Method Reference (0.15+)
| Method | Allocator? | Notes |
|---|---|---|
initCapacity(alloc, n) | Yes | Preferred initialization |
deinit(alloc) | Yes | Changed in 0.15! |
append(alloc, item) | Yes | Changed in 0.15! |
appendSlice(alloc, items) | Yes | Changed in 0.15! |
addOne(alloc) | Yes | Returns pointer to new slot |
ensureTotalCapacity(alloc, n) | Yes | Pre-allocate capacity |
toOwnedSlice(alloc) | Yes | Caller must free result |
appendAssumeCapacity(item) | No | Assumes capacity exists |
items field | No | Read-only access |
HashMap Memory Management
Managed HashMap (Recommended)
// Managed - stores allocator internally
var map = std.StringHashMap(V).init(allocator);
defer map.deinit(); // No allocator needed
try map.put(key, value); // No allocator needed
Unmanaged HashMap
// Unmanaged - requires allocator for each operation
var umap = std.StringHashMapUnmanaged(V){};
defer umap.deinit(allocator); // Allocator required
try umap.put(allocator, key, value); // Allocator required
Which to Use?
| Type | When to Use |
|---|---|
Managed (StringHashMap) | General use, simpler API |
Unmanaged (StringHashMapUnmanaged) | When allocator changes, performance-critical |
Arena Allocator
Best for batch allocations freed together:
// Arena - single deallocation frees everything
var arena = std.heap.ArenaAllocator.init(backing_allocator);
defer arena.deinit(); // Frees ALL allocations
const temp = arena.allocator();
const str1 = try temp.alloc(u8, 100); // No individual free needed
const str2 = try temp.alloc(u8, 200); // No individual free needed
// arena.deinit() frees both
Arena Use Cases
| Use Case | Why Arena |
|---|---|
| Temporary computations | Free all at once |
| Request handling | Allocate per request, free at end |
| Parsing | Allocate AST nodes, free when done |
| Building strings | Accumulate, then transfer ownership |
Testing Allocator (Leak Detection)
std.testing.allocator automatically detects memory leaks:
test "no memory leak" {
const allocator = std.testing.allocator;
// If you forget to free, test FAILS with:
// "memory address 0x... was never freed"
const buffer = try allocator.alloc(u8, 100);
defer allocator.free(buffer); // MUST have this
// Test code...
}
Common Test Memory Issues
// ❌ WRONG - Memory leak
test "leaky test" {
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, 100);
// Forgot free → test fails: "memory leak detected"
}
// ✅ CORRECT - Proper cleanup
test "clean test" {
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
// Test code...
}
// ❌ WRONG - ArrayList leak
test "leaky arraylist" {
const allocator = std.testing.allocator;
var list = try std.ArrayList(u8).initCapacity(allocator, 16);
// Forgot deinit → memory leak
}
// ✅ CORRECT - ArrayList cleanup
test "clean arraylist" {
const allocator = std.testing.allocator;
var list = try std.ArrayList(u8).initCapacity(allocator, 16);
defer list.deinit(allocator);
// Test code...
}
Segfault Prevention
Null Pointer Dereference
// ❌ DANGEROUS - Segfault
var ptr: ?*u8 = null;
_ = ptr.?.*; // Dereference null → crash
// ✅ SAFE - Check null
var ptr: ?*u8 = null;
if (ptr) |p| {
_ = p.*;
}
Array Bounds
// ❌ DANGEROUS - Out of bounds
const arr = [_]u8{ 1, 2, 3 };
_ = arr[5]; // Index 5 > len 3 → undefined behavior
// ✅ SAFE - Bounds check
const arr = [_]u8{ 1, 2, 3 };
if (5 < arr.len) {
_ = arr[5];
}
Use After Free
// ❌ DANGEROUS - Use after free
const data = try allocator.alloc(u8, 100);
allocator.free(data);
data[0] = 42; // Use after free → undefined behavior
// ✅ SAFE - Set to undefined after free
const data = try allocator.alloc(u8, 100);
allocator.free(data);
// Don't use data after this point
String Ownership
Borrowed (Read-Only)
// Borrowed - caller keeps ownership
fn process(borrowed: []const u8) void {
// Read-only, cannot modify, cannot free
std.debug.print("{s}\n", .{borrowed});
}
Owned (Caller Must Free)
// Owned - caller takes ownership and must free
fn createMessage(allocator: Allocator, name: []const u8) ![]u8 {
return try std.fmt.allocPrint(allocator, "Hello, {s}!", .{name});
}
// Usage
const msg = try createMessage(allocator, "World");
defer allocator.free(msg); // Caller frees
Solana BPF Allocator
In Solana programs, use the BPF bump allocator:
const allocator = @import("solana_program_sdk").allocator.bpf_allocator;
// Limited to 32KB heap
const data = try allocator.alloc(u8, 1024);
// Note: BPF allocator does NOT support free()!
BPF Memory Constraints
| Constraint | Value |
|---|---|
| Total heap | 32KB |
| Free support | ❌ None |
| Stack size | 64KB (with 4KB frame limit) |
BPF Memory Tips
- Pre-calculate sizes when possible
- Use stack for small/fixed allocations
- Reuse buffers instead of reallocating
- Use
extern structfor zero-copy parsing
Common Error Messages
| Error | Cause | Fix |
|---|---|---|
memory leak detected | Forgot to free | Add defer allocator.free(...) |
expected 2 arguments, found 1 | ArrayList missing allocator | Add allocator to append, deinit |
use of undefined value | Use after free | Don't use data after freeing |
index out of bounds | Array access past length | Check bounds before access |
Pre-commit Checklist
- Every
allochas correspondingdefer free - Every
createhas correspondingdefer destroy - ArrayList uses
deinit(allocator)(0.15+) -
errdeferused for error path cleanup - Tests use
std.testing.allocator - No "memory leak detected" in test output
- No segfaults or crashes
- Solana programs respect 32KB limit
Quick Reference
| Pattern | When to Use |
|---|---|
defer allocator.free(x) | Single allocation cleanup |
errdefer allocator.free(x) | Cleanup only on error |
defer list.deinit(allocator) | ArrayList cleanup (0.15+) |
defer map.deinit() | Managed HashMap cleanup |
defer umap.deinit(allocator) | Unmanaged HashMap cleanup |
Arena + defer arena.deinit() | Many temporary allocations |
std.testing.allocator | Test memory leak detection |