Why do I need to dupe this array literal?

I was recently debugging a segfault that I was able to trace back to my construction of a struct with deeply nested slices, in the end I needed to add a dupe:

Why is this dupe required (sorry it is a wall of code)?
Is there a way for me to more easily avoid problems like this?

I am constructing a deeply nested struct with a nested slices using some for loops, ArrayList, and toOwnedSlice(). The function is part of a larger memory allocation strategy that uses an arena, which is why leaking is ok (and why the function is called Leaky and accepts an arena).

pub fn initFromENILeaky(arena: std.mem.Allocator, eni: gcat.ENI, options: Options) error{OutOfMemory}!Zenoh {
    var subdevices: std.ArrayList(Zenoh.ENI.Subdevice) = .empty;
    for (eni.subdevices, 0..) |eni_subdevice, subdevice_index| {
        var inputs: std.ArrayList(Zenoh.ENI.Subdevice.PDO) = .empty;
        for (eni_subdevice.inputs) |eni_input| {
            var entries: std.ArrayList(Zenoh.ENI.Subdevice.PDO.Entry) = .empty;
            for (eni_input.entries) |eni_entry| {
                if (eni_entry.isGap()) continue;
                const substitutions: ProcessVariableSubstitutions = .{
                    .subdevice_index = zenohSanitize(try std.fmt.allocPrint(arena, "{}", .{subdevice_index})),
                    .subdevice_name = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_subdevice.name})),
                    .pdo_direction = "input",
                    .pdo_name = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_input.name})),
                    .pdo_index_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_input.index})),
                    .pdo_entry_index_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_entry.index})),
                    .pdo_entry_subindex_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_entry.subindex})),
                    .pdo_entry_description = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_entry.description})),
                };
                try entries.append(
                    arena,
                    Zenoh.ENI.Subdevice.PDO.Entry{
                        .index = eni_entry.index,
                        .subindex = eni_entry.subindex,
 //////////////////////////////////////////// DUPE HERE ////////////////////////////////////////////////////
                        .publishers = if (options.pdo_input_publisher_key_format) |pdo_input_publisher_key_format| try arena.dupe(
                            Zenoh.ENI.PubSub,
                            &.{
                                .{
                                    .key_expr = try processVaribleNameSentinelLeaky(arena, pdo_input_publisher_key_format, substitutions, 0),
                                },
                            },
                        ) else &.{},
                        .subscribers = &.{},
                    },
                );
            }
            try inputs.append(arena, Zenoh.ENI.Subdevice.PDO{
                .index = eni_input.index,
                .entries = try entries.toOwnedSlice(arena),
            });
        }

        var outputs: std.ArrayList(Zenoh.ENI.Subdevice.PDO) = .empty;
        for (eni_subdevice.outputs) |eni_output| {
            var entries: std.ArrayList(Zenoh.ENI.Subdevice.PDO.Entry) = .empty;
            for (eni_output.entries) |eni_entry| {
                if (eni_entry.isGap()) continue;
                const substitutions: ProcessVariableSubstitutions = .{
                    .subdevice_index = zenohSanitize(try std.fmt.allocPrint(arena, "{}", .{subdevice_index})),
                    .subdevice_name = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_subdevice.name})),
                    .pdo_direction = "output",
                    .pdo_name = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_output.name})),
                    .pdo_index_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_output.index})),
                    .pdo_entry_index_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_entry.index})),
                    .pdo_entry_subindex_hex = zenohSanitize(try std.fmt.allocPrint(arena, "{x}", .{eni_entry.subindex})),
                    .pdo_entry_description = zenohSanitize(try std.fmt.allocPrint(arena, "{?s}", .{eni_entry.description})),
                };

                try entries.append(arena, Zenoh.ENI.Subdevice.PDO.Entry{
                    .index = eni_entry.index,
                    .subindex = eni_entry.subindex,
                    .publishers = if (options.pdo_output_publisher_key_format) |pdo_output_publisher_key_format| try arena.dupe(
                        Zenoh.ENI.PubSub,
                        &.{
                            .{
                                .key_expr = try processVaribleNameSentinelLeaky(arena, pdo_output_publisher_key_format, substitutions, 0),
                            },
                        },
                    ) else &.{},
                    .subscribers = if (options.pdo_output_subscriber_key_format) |pdo_output_subscriber_key_format| try arena.dupe(
                        Zenoh.ENI.PubSub,
                        &.{
                            .{
                                .key_expr = try processVaribleNameSentinelLeaky(arena, pdo_output_subscriber_key_format, substitutions, 0),
                            },
                        },
                    ) else &.{},
                });
            }
            try outputs.append(arena, Zenoh.ENI.Subdevice.PDO{
                .index = eni_output.index,
                .entries = try entries.toOwnedSlice(arena),
            });
        }
        try subdevices.append(arena, Zenoh.ENI.Subdevice{
            .inputs = try inputs.toOwnedSlice(arena),
            .outputs = try outputs.toOwnedSlice(arena),
        });
    }

    return Zenoh{ .eni = .{ .subdevices = try subdevices.toOwnedSlice(arena) } };
}

Breaking the code in question into multiple lines with explicit types might make the problem more obvious:

const publishers_array = [_]Zenoh.ENI.PubSub{
    .{ .key_expr = try processVaribleNameSentinelLeaky(arena, pdo_input_publisher_key_format, substitutions, 0) },
};
const publishers_slice: []const Zenoh.ENI.PubSub = &publishers_array;

try entries.append(
    arena,
    Zenoh.ENI.Subdevice.PDO.Entry{
        .index = eni_entry.index,
        .subindex = eni_entry.subindex,
        .publishers = publishers_slice,
        .subscribers = &.{},
    },
);

The memory pointed to by the slice has to live beyond the return of the function, and without dupe you’re taking a pointer to a stack-allocated array.

2 Likes

To clarify, it’s stack allocated because its contents are not known at compile time. If they were, then this code would work.

1 Like