I’ve been writing some Advent code for fun in Zig, and had the goal of doing it all with no heap usage. To support this I wrote a useful struct that stores all the lines in a file with std.BoundedArray
, but lets you access it via a slice of the lines ([]const []u8
). Note that there isn’t a second const
in the type, so modifying the bytes you received contained within this struct is considered fair game.
I think I’ve set up everything correctly so that there’s no unintentional references to dangling memory, but encountered some very odd behavior the code below demonstrates:
const std = @import("std");
pub fn BoundedArrayBackedLines(max_line_width: usize, max_num_lines: usize) type {
return struct {
_internal_buffer: OverallBuffer,
_outer_slice_mem: [max_num_lines][]u8,
const Self = @This();
pub const BufferPerLine = std.BoundedArray(u8, max_line_width);
pub const OverallBuffer = std.BoundedArray(BufferPerLine, max_num_lines);
fn fromFile(fp: []const u8) !Self {
const fh = try std.fs.cwd().openFile(fp, .{});
defer fh.close();
var ret: Self = undefined;
ret._internal_buffer = OverallBuffer.init(0) catch unreachable;
var line_idx: usize = 0;
while (true) {
var line_buffer = try std.BoundedArray(u8, max_line_width).init(0);
fh.reader().streamUntilDelimiter(line_buffer.writer(), '\n', null) catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
const item = try ret._internal_buffer.addOne();
item.* = try BufferPerLine.init(0);
try item.*.appendSlice(line_buffer.constSlice());
ret._outer_slice_mem[line_idx] = item.*.slice();
line_idx += 1;
}
return ret;
}
pub fn slice(self: *Self) []const []u8 {
return self._outer_slice_mem[0..self._internal_buffer.len];
}
};
}
fn someFunction(slice: []const []u8) usize {
std.debug.print("This is also fine:\n", .{});
for (slice) |ln| {
std.debug.print("{s}\n", .{ln});
}
// Huge variable chucked on the stack seems to nullify the bytes of the input slice!
var big_var = std.BoundedArray(u8, 100000).init(0) catch unreachable;
big_var.append(10) catch unreachable;
big_var.append(12) catch unreachable;
std.debug.print("This isn't (raw bytes):\n", .{});
for (slice) |ln| {
std.debug.print("{any}\n", .{ln});
}
return big_var.slice()[0] + big_var.slice()[1];
}
pub fn main() !void {
var lines = try BoundedArrayBackedLines(100, 100).fromFile("test_file.txt");
std.debug.print("This is fine:\n", .{});
for (lines.slice()) |ln| {
std.debug.print("{s}\n", .{ln});
}
std.debug.print("But this function works...? {d}\n", .{someFunction(lines.slice())});
}
The code more or less explains it, but my slice is getting “zeroed out” unintentionally when I throw a huge variable on the stack in a function that uses this slice. This makes me think I’m somehow referencing temporary memory, but I’ve double and triple checked and I can’t see where it would be happening… The output of this code for me (Zig version 0.13.0
, x86 Linux platform) is as follows:
This is fine:
hello
from
test
file
This is also fine:
hello
from
test
file
This isn't (raw bytes):
{ 0, 0, 0, 0, 0 }
{ 0, 0, 0, 0 }
{ 0, 0, 0, 0 }
{ 0, 0, 0, 0 }
But this function works...? 22
The contents of test_file.txt
is:
hello
from
test
file
Any help is greatly appreciated!