Contact Info

sean [at] coreitpro [dot] com gpg key

Mastodon

sc68cal on Libera

Zig Serialization Followup

So, the critical piece that I forgot is that while Zig does have better memory management than C, you are still own your own when it comes to managing that memory and understanding the memory model.

As @cztomsik pointed out in my question I posted. This code doesn’t work

fn regionDetail(conn: zqlite.Conn, id: i64) !Region {
    const query = "select * from Regions where RegionID=?";
    if (try conn.row(query, .{id})) |row| {
        defer row.deinit();
        return Region{
            .RegionID = row.int(0),
            .RegionDescription = row.text(1),
        };
    }
    return error.NotFound;
}

Because

The problem is that you return a struct with a pointer, which is freed when you call the row.deinit(). Therefore, what you return, is invalid already.

Obviously calling alloc.dupe to copy the array is the correct way to fix this issue, and allows the row to continue to be deinit()‘d at the end of the function call.

--- a/src/main.zig
+++ b/src/main.zig
@@ -19,11 +19,7 @@ fn regionDetail(alloc: std.mem.Allocator, conn: zqlite.Conn, 
id: i64) !Region {
         defer row.deinit();
         return .{
             .RegionID = row.int(0),
-            .RegionDescription = row.text(1),
+            .RegionDescription = try alloc.dupe(u8, row.text(1)),

The downside is that now the caller has to be aware of this allocation, pass a std.mem.Allocator in, and then also manage deallocating.

Thankfully, tokamak already provides an easy way get an allocator passed as an argument to this function, via its dependency injection, and will set everything up correctly for your functions and also handle cleaning up anything you allocate via the allocator, at the end of the request.

This made me think a lot about how the assignment operator in different programming language have huge unspoken meanings.

In Zig, simply doing an = for the RegionID and the SQLite result row’s first column never caused an issue because it’s an i64 allocated on the stack, so setting .RegionID = row.int(0) works because the compiler just sets aside that memory for you and does a copy. RegionDescription being a []const u8 means it is memory allocated at runtime (and referenced via a pointer), so the = implies something far different.

It’s a big reminder, since I have been working with Python for a decade where the equals operator does all the memory management for you via reference counting and you can just blithely assign stuff to other stuff and it’s only when the reference count drops to zero, where you have the memory freed and everything “Just Works”.

Overall I am still enjoying Zig, since there is a lot of power that you can wield compared to an interpreted language like Python, but I’ve obviously just blown my foot off with my first couple of steps.