Skip to content

Instantly share code, notes, and snippets.

@jumpnbrownweasel
Last active September 24, 2022 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jumpnbrownweasel/5129912b13ce85b88fe9feb0bc029f32 to your computer and use it in GitHub Desktop.
Save jumpnbrownweasel/5129912b13ce85b88fe9feb0bc029f32 to your computer and use it in GitHub Desktop.
Alloc an arena in the arena itself to avoid common UB mistakes.
const std = @import("std");
const expectEqual = std.testing.expectEqual;
/// Called instead of ArenaAllocator.init to ensure that the arena allocator
/// does not become invalid when the arena struct is moved.
///
/// The ArenaAllocator is placed in the arena itself, and therefore the returned
/// pointer to the arena is stable. This means the allocator returned by
/// arena.allocator(), which contains a pointer to the arena struct, is also
/// stable.
///
/// This avoids a common error when calling ArenaAllocator.init, which returns
/// the arena struct. If arena.allocator() is called using the returned arena
/// struct, and then the arena struct is moved, the allocator that was returned
/// by arena.allocator() will be invalid since it contains a pointer to the old
/// location of the arena struct. A workaround for this is avoid saving the
/// allocator returned by arena.allocator(), and instead call arena.allocator()
/// each time it is needed. By allocating the arena struct in the arena itself,
/// this workaround is no longer needed.
///
/// The potential drawbacks of this approach are that an allocation must be
/// performed in the arena initially to hold the arena struct, and the arena
/// struct cannot be stored on the stack or elsewhere.
///
/// WARNING: When calling allocArena instead of ArenaAllocator.init, freeArena
/// must be called instead ArenaAllocator.deinit. Or, the arena struct must be
/// moved to the stack before calling ArenaAllocator.deinit, which is what
/// freeArena does.
///
pub fn allocArena(
backing: std.mem.Allocator,
) error{OutOfMemory}!*std.heap.ArenaAllocator {
//
// Allocate memory for the arena struct using the arena itself, and copy the
// arena struct into this allocated/stable memory.
//
var arena = std.heap.ArenaAllocator.init(backing);
var arena_ptr = try arena.allocator().create(std.heap.ArenaAllocator);
arena_ptr.* = arena;
return arena_ptr;
}
/// Called instead of ArenaAllocator.deinit when ArenaAllocator.init was used
/// to create the arena.
///
pub fn freeArena(arena: *std.heap.ArenaAllocator) void {
//
// Copy the arena to the stack so it isn't destroyed while freeing its own
// memory.
//
var arena_copy = arena.*;
arena_copy.deinit();
}
test "allocArena" {
var list_arena = try ArrayListArena(i32).init(std.testing.allocator);
defer list_arena.deinit();
try list_arena.list.append(123);
try expectEqual(@as(i32, 123), list_arena.list.items[0]);
}
pub fn ArrayListArena(comptime T: type) type {
return struct {
const Self = @This();
arena: *std.heap.ArenaAllocator,
list: *std.ArrayList(T),
pub fn init(backing: std.mem.Allocator) error{OutOfMemory}!Self {
const arena = try allocArena(backing);
errdefer freeArena(arena);
//
// Since we are using allocArena, the arena pointer is stable and
// therefore arena.allocator() is stable and can be given to the
// ArrayList safely. If we used an arena struct allocated on the
// stack here instead, allocations using arena.allocator() would
// cause UB.
//
const list = try arena.allocator().create(std.ArrayList(T));
list.* = std.ArrayList(T).init(arena.allocator());
//
// Note that allocating the array list struct in the arena isn't
// necessary -- it could also be an embedded struct field here if
// desired.
//
return Self{.arena = arena, .list = list};
}
pub fn deinit(self: *Self) void {
// Calling self.list.deinit() is unnecessary.
freeArena(self.arena);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment