Where is the memory stored when referencing an array literal?

Where does the memory for the slice contents come from?

const std = @import("std");

pub const MySliceHolder = struct {
    slice: []const u8,
    pub fn makeMySlice() MySliceHolder {
        return MySliceHolder{
            .slice = &.{ 0, 1, 2, 3 },
        };
    }
};

test {
    const expected = [_]u8{ 0, 1, 2, 3 };
    try std.testing.expectEqualSlices(
        u8,
        &expected,
        MySliceHolder.makeMySlice().slice,
    );
}

The test passes btw.

This gets statically allocated because it doesn’t depend on any runtime things and isn’t allowed to change at runtime, so the memory gets placed in the read only section of the executable.

3 Likes

Perfectly correct, and for posterity, I’ll add that this is not the answer to the title of this question.

Any comptime-known string value is put in .rodata, static memory, and can’t be altered at runtime. It technically isn’t a slice, either: if the literal string format is used, it’s a zero-terminated sentinel array of u8.

/// The type of this is [7:0]const u8
const rodata_string = ".rodata";

The example given in the question uses literal array syntax, so it would be a [4]const u8, without the null terminator. The distinction mainly makes a difference for C interop, if a string is heading to C-land, it’s often important that it be null terminated. A practical, if minor difference, is that it’s legal to read the sentinel, which is at a_string[a_string.len], which is one past the end of a normal array (and slice).

These can both be cast to a []const u8 fairly easily, for instance, by passing it to a function which expects one. But they’re of a different type: both the length of an array, and any sentinel terminator it has, are part of the type of that data. Any comptime-known value like this is in static memory and has the length as part of the type. u8 isn’t special here, except that when literal string syntax is used, it automatically becomes a null-sentinel array.

A slice is a pointer and a length, so “where is the memory” is actually two questions: where is the struct with the pointer and the length, and where is the memory that the pointer points at? The answer is not more specific than “wherever the memory is” in either case. The data in a []const u8 can be on the stack, the heap, or in static memory (the last happens when a string literal is cast to a slice), and this is true of the slice itself as well.

Edit: one final point: Arrays aren’t automatically in static memory either. They can be declared on the stack, or allocated on the heap, but the length (and the value of the sentinel when there is one) must be comptime-known. If the data in the array is also comptime-known, then it goes in .rodata.

2 Likes

It’s actually *const [7:0]u8, and that’s why you can coerce it to a slice; you got the pointer and the length right there.

1 Like

I’m going to adjust the title of this topic to reflect that it deals with const literals.

3 Likes

Good catch, I wrote that wrong. Should have been *const [7:0]u8. Tricky stuff :slight_smile:

There’s one way to always be sure:

test "whatami" {
    const a_string = ".rodata";
    std.debug.print("{}\n", .{@TypeOf(a_string)});
}

Yes. Another benefit of it being a pointer to constant data is that you can de-duplicate:

    const str_lit = ".rodata";
    std.debug.print("{}\n{*}\n{*}\n{*}\n", .{
        @TypeOf(str_lit),
        ".rodata",
        ".rodata",
        str_lit,
    });
*const [7:0]u8
[7:0]u8@104c3d864
[7:0]u8@104c3d864
[7:0]u8@104c3d864