Trying to serialize a struct to JSON leads to a Segmentation fault

Zig version: 0.17

Hi there, I am learning Zig and my first serious project is to implement a small app that gets a payload from the job queue and posts it to Slack API. The API works with the concept of “blocks”, each block can represent text, image, or more complex composite types. Blocks can be nicely represented with tagged unions. But I have to deal with somewhat generic structs: Parent and Child.

However, I am stuck with the simplest task - initializing a struct that can be properly serialized. With the current implementation I am getting a Segmentation fault.

I have reduced my code to this simplest failing snippet.

const std = @import("std");

const Child = struct {
    text: []const u8,
};

const Parent = struct {
    type: []const u8,
    elements: []const Child,
};

const Msg = struct {
    elements: []const Parent,

    pub fn init(v: []const u8) Msg {
        const children = &[_]Child{.{
            .text = v,
        }};

        const parent: Parent = .{
            .type = "parent-type",
            .elements = children,
        };

        return Msg{
            .elements = &[_]Parent{parent},
        };
    }
};

pub fn main(init: std.process.Init) !void {
    const allocator: std.mem.Allocator = init.arena.allocator();

    const msg = Msg.init("test-value");

    const out = try std.json.Stringify.valueAlloc(allocator, msg, .{});
    defer allocator.free(out);

    std.debug.print("JSON: {s}\n", .{out});
}

Output:

Segmentation fault at address 0x0
/usr/local/lib/zig/std/unicode.zig:243:43: 0xea12f86edb8 in utf8ValidateSliceImpl__anon_33805 (main)
const chunk: Chunk = remaining[0..chunk_len].*;
^
/usr/local/lib/zig/std/unicode.zig:232:33: 0xea12f86eca4 in utf8ValidateSlice (main)
return utf8ValidateSliceImpl(input, .cannot_encode_surrogate_half);
^
/usr/local/lib/zig/std/json/Stringify.zig:506:95: 0xea12f86bde2 in write__anon_33493 (main)
if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) {
^
/usr/local/lib/zig/std/json/Stringify.zig:477:35: 0xea12f86b088 in write__anon_33471 (main)
try self.write(@field(v, Field.name));
^
/usr/local/lib/zig/std/json/Stringify.zig:513:35: 0xea12f86ad9a in write__anon_33434 (main)
try self.write(x);
^
/usr/local/lib/zig/std/json/Stringify.zig:477:35: 0xea12f86a7c2 in write__anon_33380 (main)
try self.write(@field(v, Field.name));
^
/usr/local/lib/zig/std/json/Stringify.zig:575:16: 0xea12f86a5d4 in value__anon_33336 (main)
try s.write(v);
^
/usr/local/lib/zig/std/json/Stringify.zig:621:10: 0xea12f86a37b in valueAlloc__anon_30459 (main)
value(v, options, &aw.writer) catch return error.OutOfMemory;
^
/home/me/projects/repr/src/main.zig:36:50: 0xea12f8659f0 in main (main)
const out = try std.json.Stringify.valueAlloc(allocator, msg, .{});
^
/usr/local/lib/zig/std/start.zig:737:30: 0xea12f866170 in callMain (main)
return wrapMain(root.main(.{
^
???:?:?: 0xea12f74e53a in ??? (/home/me/.cache/zig/o/1c066c708c3cff05d1d3c930e4312205/main)
Abort trap (core dumped)

I want to use the init function, because in practice a Slack message will contain many blocks with multiple default field values and I need a concise way to initialize them.

If I go the explicit way, then I get the expected JSON:

pub fn main(init: std.process.Init) !void {
    const allocator: std.mem.Allocator = init.arena.allocator();

    //const msg = Msg.init("test-value");

    const msg: Msg = .{
        .elements = &[_]Parent{.{
            .type = "parent-type",
            .elements = &[_]Child{.{ .text = "expected-text" }},
        }},
    };

    const out = try std.json.Stringify.valueAlloc(allocator, msg, .{});
    defer allocator.free(out);

    std.debug.print("JSON: {s}\n", .{out});
}

Output:

$ zig run main.zig
JSON: {“elements”:[{“type”:“parent-type”,“elements”:[{“text”:“expected-text”}]}]}

I would appreciate if you can point me in a direction where I can read more about it.

This really needs some links to the stack&the heap articles, but I don’t remember one off the top of my head.

But the general thing is: Zig is a language with manual memory management. You have to care about not only the abstract logic of the program, but also about physical representation. Where data lives is a key question.

In this case, the key question to ask is “where are the bytes pointed by []const Child?” .

In your example,

        const children = &[_]Child{.{
            .text = v,
        }};

the relevant bytes are on the stack frame of the init function. So, when the init returns, those bytes get overwritten by stuff private to valueAlloc, so the data becomes completely invalid and the program crashes.

I don’t think there’s a simple straightforward “fix” here, this really comes down to the overacting memory management strategy, which is a vast vast topic, but, hopefully, this explanation at least makes sense in terms of what actually went wrong!

Thanks! This is exactly the answer I was hoping to get. It provides direction, without stealing the joy of discovery. Cheers.