Casting + aligning ?*anyopaque to zig string (done incorrectly?) results in corrupted data

Hi! I’m not very good around memory alignment and casting, so most likely I did something wrong.

I have an sqlite callback function which accepts a void* pointer which I then cast to the type I need:

var last_created_at: ?[]const u8 = null;
const cb = struct {
  fn callback(ctx: ?*anyopaque, rows: c_int, values: [*c][*c]u8, names: [*c][*c]u8) callconv(.C) c_int {
    // <snip> null checks redacted
    // values[0] here points to the sql TEXT column value
    // which holds an iso date-time string
    const created_at = std.mem.span(values[0]);
    std.debug.print("created_at1: {s}\n", .{created_at});
    var ptr = @as(?*?[]const u8, @ptrCast(@alignCast(ctx)));
    ptr.?.* = created_at;
    return 0;

sqlite3_exec(..., &cb.callback, &last_created_at, ...);
std.debug.print("created_at2 {?s}\n", .{last_created_at});

This prints something like:

created_at1: 2023-11-09T14:12:49.574Z
created_at2: (=09T14:12:49.574Z

Although the latter value seems to smile at me, it’s obvious that this is not a good outcome and the var points to the wrong portion of memory.

At first I thought that this can be caused by memory being somehow freed, this seems not to be the case (judging by sqlite docs).

So I think this may have something to do with that cast, but I’m not sure what would be the correct one.
Also, this kind of code works for me, when I query a long value and change everything to work with ?u64 instead of ?[]const u8

It seems like the values pointer becomes invalidated when the callback returns.

This probably works because u64 is passed by value rather than reference.
If this is the case, you should allocate some memory and, in the callback, memcpy the string into your buffer.

Hmm, so it’s still due to the freeing you think? I tried to do allocPrint to copy, but outside of the callback and that didn’t help.

I wanted to try to copy inside of the callback, but I don’t have access to the allocator there (due to it’s being across the boundary), and I can’t seem to allocate beforehand, because I don’t know the size of the string. Although I expect it to be an iso date-time, perhaps I can deduce something based on that…

Can the ctx be anything you want? If so, maybe you can pass in a struct with an allocator and a field to hold the copy. I’m totally guessing here, but something like this:

const Context = struct {
    allocator: std.mem.Allocator,
    last_created: []const u8,

    fn callback(ctx: ?*anyopaque, rows: c_int, values: [*c][*c]u8, names: [*c][*c]u8) callconv(.C) c_int {
        // <snip> null checks redacted
        const created_at = std.mem.span(values[0]);
        std.debug.print("created_at1: {s}\n", .{created_at});
        var ptr = @as(?*?Context, @ptrCast(@alignCast(ctx)));
        ptr.?.last_created = ptr.?.allocator.dupe(created) catch return 1;
        return 0;

var ctx = Context{
    .allocator = std.heap.c_allocator,
    .last_created = undefined,

sqlite3_exec(..., &Context.callback, &ctx, ...);

Oh, interesting idea, haven’t thought of that, thanks, I’ll try tomorrow!