Sentinel Termination
const std = @import("std");
const expect = std.testing.expect;
test "sentinel-terminated array" {
// `[3:0]u8` is three bytes plus a guaranteed 0 at index 3.
const array: [3:0]u8 = .{ 1, 2, 3 };
try expect(array.len == 3); // the sentinel is not counted
try expect(array[3] == 0); // but it is there
}
test "string literals are sentinel terminated" {
const literal = "hi";
try expect(@TypeOf(literal) == *const [2:0]u8);
try expect(literal.len == 2);
try expect(literal[2] == 0); // free C compatibility
}
test "span recovers the length from a sentinel" {
const c_string: [*:0]const u8 = "hello";
// `[*:0]T` has no length, but the sentinel makes it recoverable.
const slice = std.mem.span(c_string);
try expect(slice.len == 5);
try expect(std.mem.eql(u8, slice, "hello"));
}
test "sentinel slices coerce to ordinary ones" {
const sentinel_slice: [:0]const u8 = "abc";
const plain: []const u8 = sentinel_slice;
try expect(plain.len == 3);
}A sentinel-terminated type guarantees a specific value sits just past the end. The syntax puts it after a colon:
| Type | Meaning |
|---|---|
[3:0]u8 | 3 bytes, plus a guaranteed 0 at index 3 |
[:0]u8 | slice with a 0 after the last element |
[*:0]u8 | many-item pointer, terminated by 0 |
The sentinel is not counted in .len, but it is really there and you may
read it.
String literals get this for free
const literal = "hi"; // *const [2:0]u8
literal.len; // 2
literal[2]; // 0
So passing a Zig string literal to a C function that wants const char * needs
no conversion and no copy. That is the whole point: C compatibility without
paying for it in ordinary Zig code, where you still get the length.
Recovering a length
[*:0]T has no length, but the sentinel makes it discoverable:
const slice = std.mem.span(c_string); // scans to the sentinel
This is O(n), which is exactly the cost C pays for strlen. Do it once at the
boundary and work with slices from there.