I recently attempted to write a project in Zig, but was thwarted by this issue in zig-sqlite (I didn’t try very hard).
I ended up writing it in Go (see here), which I think was better for my sanity, but I’d like to at least partially make a Zig version for Edutainment purposes.
As a GC boy, I am new to memory management, and my question relates to whether or not deinit() functions are code clutter when using an Arena Allocator.
The project is a simple, short-lived CLI program that at most has to:
- Scan some folders
- Decrypt a sqlite db file.
- Load the sqlite db.
- Perform a handful of basic CRUD operations
- Save the db to disk
- Re-encrypt
- Shutdown
As I was handling some JSON conversion for a field, I ended up writing a few tests which required that I de-allocate memory properly, and it occurred to me that putting deinit() methods on my structs might be a waste of time.
IIUC, when using an arena allocator, not needing to manage lifetimes of structs inside that arena is one of the benefits, and all the calls you make to free are largely just slowing you down (at least in this context).
The way I see it, I have three choices:
- Put
deinit()methods on everything that needs it, even though they’ll never be called in production. This clutters the codebase with things that aren’t strictly needed, but might make writing tests easier. - Add comments documenting that these structs have memory that they aren’t cleaning up, but it’s okay because we expect them to be used with an arena allocator.
- Do nothing and have streamlined code that arguably has a flaw in it, but isn’t relevent to the actual use case.
Which should I go with? Are there any I missed?
Cheers ![]()
Here’s the relevant code, any/all code review is welcome!
/// These are rows in the table, each one represents a backup
/// of an on-disk .env file
pub const EnvFile = struct {
path: []const u8,
// Storing the full path and the full dir is
// a waste of space, but its a WIP.
dir: []const u8,
remotes: JsonStringSlice,
sha256: []const u8,
contents: []const u8,
pub fn deinit(self: EnvFile, alloc: std.mem.Allocator) void {
self.remotes.deinit(alloc);
}
};
/// A list of strings, JSON serialized for zig-sqlite
pub const JsonStringSlice = struct {
data: [][]const u8,
pub const BaseType = []const u8;
// AI Generated
pub fn bindField(self: JsonStringSlice, alloc: std.mem.Allocator) !BaseType {
var string = std.ArrayList(u8).init(alloc);
try std.json.stringify(self.data, .{}, string.writer());
return string.toOwnedSlice();
}
pub fn readField(alloc: std.mem.Allocator, value: BaseType) !JsonStringSlice {
const parsed = try std.json.parseFromSlice(
[][]const u8,
alloc,
value,
.{},
);
defer parsed.deinit();
// AI Generated
var owned_data = try alloc.alloc([]const u8, parsed.value.len);
for (parsed.value, 0..) |str, i| {
owned_data[i] = try alloc.dupe(u8, str);
}
return .{ .data = owned_data };
}
pub fn deinit(self: JsonStringSlice, alloc: std.mem.Allocator) void {
for (self.data) |str| {
alloc.free(str);
}
alloc.free(self.data);
}
};
test "JsonStringSlice parses" {
const gpa = std.testing.allocator;
const input =
\\["foo", "bar"]
;
const slice = try JsonStringSlice.readField(gpa, input);
defer slice.deinit(gpa);
try std.testing.expectEqualStrings(slice.data[0], "foo");
try std.testing.expectEqualStrings(slice.data[1], "bar");
}
test "EnvFile doesn't leak" {
const gpa = std.testing.allocator;
const input =
\\["foo", "bar"]
;
const slice = try JsonStringSlice.readField(gpa, input);
const contents =
\\APP_ENV=prod
;
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
hasher.update(contents);
const hash = hasher.finalResult();
const f = EnvFile{
.path = "/home/user/project/.env",
.dir = "/home/user/project/",
.remotes = slice,
.sha256 = &hash,
.contents = contents,
};
defer f.deinit(gpa);
}
const std = @import("std");
const sqlite = @import("sqlite");