An API for allocating multiple dependent buffers as one single contiguous allocation, so that all the buffers can also be freed as one single operation, would be very useful, especially for copying data from awkward internal/external APIs when you don’t want to expose references to internal data structures.
(For a simple example of what I mean by “dependent buffers”, consider a slice of strings [][]u8, which needs one buffer for the character data and another to store the strings (the pointer to the first character + length for each string).)
I played around with the idea for a bit and arrived at the following interface:
pub fn groupAlloc(
allocator: std.mem.Allocator,
comptime Types: anytype, // tuple of types
lengths: anytype, // tuple of usizes
) std.mem.Allocator.Error!AllocatedGroup(Types); // tuple of slices
pub fn groupFree(
allocator: std.mem.Allocator,
comptime Types: anytype, // tuple of types
first_group: []const Types[0],
) void;
Example usage:
pub const Server = struct {
id: u32,
name: []u8,
users: []User,
}
pub const User = struct {
id: u32,
name: []u8,
};
/// Returns details about all servers.
/// Pass the result to `freeServers()` to free the allocated memory.
pub fn getServers(allocator: std.mem.Allocator) !*Server {
// calculate object counts and buffer lengths...
const servers: []Server //
const users: []User, //
const strings: [][]u8, //
const string_bytes_buf: []u8 //
= try groupAlloc(
allocator,
.{ Server, User, []u8, u8 },
.{ num_servers, num_users, num_strings, string_bytes_buf_required_len },
);
errdefer comptime unreachable;
// initialize, populate and link up the buffers...
return servers;
}
/// Frees server details previously allocated by `getServers()`.
pub fn freeServers(servers: []const Server, allocator: std.mem.Allocator) void {
groupFree(allocator, .{ Server, User, []const u8, u8 }, servers);
}
Implementation (not thoroughly tested but seems to work):
pub fn groupAlloc(
allocator: std.mem.Allocator,
comptime Types: anytype,
lengths: anytype,
) std.mem.Allocator.Error!AllocatedGroup(Types) {
comptime std.debug.assert(Types.len != 0);
var allocation_len: usize = @sizeOf(usize);
comptime var alignment: usize = @alignOf(usize);
inline for (Types, lengths) |T, len| {
if (len != 0) {
allocation_len = std.mem.alignForward(usize, allocation_len, @alignOf(T));
}
allocation_len += @sizeOf(T) * len;
alignment = @max(alignment, @alignOf(T));
}
const allocation = try allocator.alignedAlloc(u8, .fromByteUnits(alignment), allocation_len);
errdefer comptime unreachable;
@as(*usize, @ptrCast(allocation.ptr)).* = allocation_len;
var offset: usize = @sizeOf(usize);
var result: AllocatedGroup(Types) = undefined;
inline for (&result, Types, lengths) |*slice, T, len| {
if (len != 0) {
offset = std.mem.alignForward(usize, offset, @alignOf(T));
}
slice.ptr = @as([*]T, @ptrCast(@alignCast(allocation.ptr + offset)));
slice.len = len;
offset += @sizeOf(T) * len;
}
std.debug.assert(offset == allocation_len);
return result;
}
pub fn groupFree(
allocator: std.mem.Allocator,
comptime Types: anytype,
first_group: []const Types[0],
) void {
comptime var alignment: usize = @alignOf(usize);
comptime {
for (Types) |T| {
alignment = @max(alignment, @alignOf(T));
}
}
const offset = @intFromPtr(first_group.ptr) - std.mem.alignBackward(usize, @intFromPtr(first_group.ptr) - 1, alignment);
const allocation_ptr: [*]align(alignment) const u8 = @alignCast(@as([*]const u8, @ptrCast(first_group.ptr)) - offset);
const allocation_len = @as(*const usize, @ptrCast(@alignCast(allocation_ptr))).*;
allocator.free(allocation_ptr[0..allocation_len]);
}
fn AllocatedGroup(comptime Types: anytype) type {
std.debug.assert(Types.len != 0);
var fields: [Types.len]std.builtin.Type.StructField = undefined;
for (&fields, Types, 0..) |*field, T, i| {
field.* = .{
.name = std.fmt.comptimePrint("{}", .{i}),
.type = []T,
.default_value_ptr = null,
.is_comptime = false,
.alignment = 0,
};
}
return @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &fields,
.decls = &.{},
.is_tuple = true,
} });
}
Given groupAlloc(allocator, .{ Foo, []u8, u8 }, .{ 2, 4, 256 }), this will allocate a region of memory equivalent to:
extern struct {
allocation_len: usize = @sizeOf(@This()),
@"0": [2]Foo,
@"1": [4][]u8,
@"2": [256]u8,
}
When the allocation is later freed by groupFree(allocator, .{ Foo, []u8, u8 }, foos), it uses the type information as well as the allocation_len value stored immediately before &foos[0] to reconstruct the size of the original allocation and pass it to allocator.free().
You could probably extend this idea further to support alignments and sentinels like all the regular allocation functions.