I’ve been experimenting with object pools that allow you to mint arbitrary types, very much inspired by Zig’s InternPool. Like InternPool, the backing buffer is []u32 and the index is u32.
The first API I added was addUndefined, which takes a count argument:
pub const PackedData = struct {
entries: []u32,
capacity: u32,
index: u32,
pub fn addUndefined(
data: *PackedData,
allocator: std.mem.Allocator,
T: type,
count: u32
) !u32 {
// Ensure space for ⌈@sizeOf(T) * count / @sizeOf(u32)⌉
// Increment index by that much
// Return original index
}
};
That lets me mint variable-length data of varying integer size that can be safely reified into slices, which I fill in later. The way I’m doing that is by doing u32[] → std.mem.sliceAsBytes → std.mem.bytesAsSlice → u16[] or u8[]. I tested that this is safe:
test "bytes as slice larger than the bytes slice" {
var array: [2]u32 = undefined;
const slice: []u32 = &array;
_ = std.mem.bytesAsSlice(u128, std.mem.sliceAsBytes(slice)); // panics at 8 / 16
}
Next I wanted to mint structs. It looks like InternPool splits up struct fields and puts each one in its own u32 slot. I tried this, but it didn’t work out for me. And since entries already has a len, and @sizeOf(T) is known, shouldn’t std.mem.bytesAsValue be able to safety-check just like std.mem.bytesAsSlice does? It doesn’t currently, because bytesAsValue is implemented merely as a @ptrCast:
const Four = packed struct {a: u16, b: u16, c: u16, d: u16, ohno: u16};
test "bytes as struct larger than the bytes slice" {
var array: [2]u32 = undefined;
const slice: []u32 = &array;
var four = std.mem.bytesAsValue(Four, std.mem.sliceAsBytes(slice)); // no error!
four.ohno = 1; // traps, but is it safe to get that far?
}
So my questions are:
- Why doesn’t
mem.bytesAsValueadd a check likeif (@sizeOf(T) > bytes.len) unreachable;? Or is that not all that’s needed? - Are there better solutions to storing variable sized types together? Has anyone had to implement anything similar?