20 February 2025
One surprising fact about Zig is that the language has six different types for working with arrays. In this article, I want to go through each of them and explain what they’re for, how they work, and how you can convert between them.
We’ll start with the array, which is a fixed-length sequence of values in memory. Here’s an example of an array of five integers:
var array = [5]u64{ 1, 2, 3, 4, 5 };
At runtime, it’ll look like this:
Each box represents 8 bytes (64 bit); the whole array takes up 5 * 8 = 40 bytes.
Arrays are important because they’re the basis for all the other types, but having the length as part of the type makes them pretty inflexible. Zig has slices as a type for variable-length sequences:
var slice: []u64 = &array;
A slice contains a pointer to the first element and the number of elements:
This will look familiar if you’re coming from Go or Rust, except Go’s slices also have a “capacity.”
So arrays are fixed-length, slices are variable-length. What do we need the other types for? The main reason is compatibility with C APIs.
Arrays in C correspond to arrays in Zig, but C doesn’t have slices; instead you use a pointer to the first element to refer to an array in memory, and you use pointer arithmetic to access the other elements. In Zig, a many-item pointer lets you do the same thing:
var mip: [*]u64 = &array;
As you can see, the type in the code is [*]u64
-- it’s not *u64
, which
would be a pointer to a single value of type u64
, and wouldn’t allow indexing. At
runtime, a many-item pointer is just a pointer:
The downside of just passing pointers around is that you need some way to keep track of the length. For strings, C solves this by adding an extra NUL byte (a byte with all bits zero) to mark the end of the string, making it a “zero-terminated string.” The same method can work for other types, for example an array of pointers with a null pointer to mark the end.
In Zig, this is called a sentinel-terminated array. We can create a sentinel-terminated array with this syntax:
var sta = [5:0]u64{ 1, 2, 3, 4, 5 };
The type [5:0]u64
means array of 5 elements of type u64
, with a
0
value as the sentinel. The compiler will add the sentinel, so in the compiled
program this is actually six values:
The sentinel can be any value. If we feel like it, we can create a 99-terminated array:
var sta99 = [5:99]u64{ 1, 2, 3, 4, 5 };
A C-style pointer to a sentinel-terminated array is called a sentinel-terminated pointer.
var stp: [*:0]u64 = &sta;
Finally, the sentinel-terminated slice is a slice where the data is also terminated by a sentinel value:
var sts: [:0]u64 = &sta;
Having all these related types means we often need to convert from one to another.
In a nutshell, you use &array
to go from an array to a pointer or slice,
slice.ptr
to go from a slice to a pointer, and the standard-library function
std.mem.span
to go from sentinel-terminated pointer to (sentined-terminated) slice.
Converting a sentinel-terminated array to a regular array is done implicity; same for pointers and
slices.
This diagram gives an overview: