Understanding "sentinels" (slices, arrays, pointers)

Dunno if the way I’m learning Zig is the right way, but I peek concepts from the reference documentation and try to grasp them.

When talking about slices sentinels I don’t get it.
I tend to considere that the only use case is for dealing with 0 terminated C strings

But, above this use case when should I use sentinel terminated slices ?

And second question : what is a sentinel terminated pointer ?

When I read the doc

The syntax [*:x]T describes a pointer that has a length determined by a sentinel value. This provides protection against buffer overflow and overreads.

Does it means that the compiler will generate code that prevents the pointer to be overwritten above its original size ?

1 Like

No. Having a sentinel value means that a program in a low level programming language (like C) can easily check when to terminate a loop.

See Null-terminated string - Wikipedia and Sentinel value - Wikipedia.

An alternative (in low level languages) is to pass both the pointer and the length to a function.

1 Like

The purpose of a sentinel (such as the 0 in a C string) is to mark the end of the sequence of elements. You obtain the length/end of the sequence by counting each element in a loop until you encounter the sentinel.

Slices carry both a length and a pointer with them at all times, so since you already have a length and know where the sequence ends, sentinel-terminated slices are more or less unnecessary in pure Zig code. But they are useful if you have a slice that will eventually be passed to a C API that expects a sentinel-terminated pointer (such as a 0-terminated string).

With a []const u8 slice there’s no guarantee that the string will have a terminating 0 byte, so you would need to make a copy of it and append a final 0 byte (usually with Allocator.dupeZ) if you wanted to pass it to a C API. But with [:0]const u8 you already know for sure that there’s a terminating 0 byte, so there’s no need for the copy.

The safety is enforced via compile errors. If you have a function with a parameter of type [*:0]const u8, passing a pointer of type [*]const u8 (note: no sentinel) to it is a compile error.

But, if you were to cast the [*]const u8 pointer to [*:0]const u8 using @ptrCast, the compiler won’t insert any code that checks that the pointer actually contains a 0 byte (you are more or less telling the compiler that “I know better”), so this could potentially be unsafe and result in a buffer overflow.

When using the slice syntax like buf[0..end :0], however, there is additional safety-checking code inserted which checks for the presence of a sentinel and protects against buffer overflows (in Debug and ReleaseSafe mode).

3 Likes

So seems obvious that I need to make better choices when electing a Zig topic to learn about :wink: This was a wrong shot

Make sense for C strings, and all other use cases are edge/rare cases related with C libraries calls that might expect a terminal (sentinel) value

For your first question, sentinel terminated slices are used in the std when parsing a Zig source file, as it simplifies the code:

I think this a great topic for learning Zig, even at the early stages. It should be emphasized how C does things, versus how Zig does things, with some clear guidance on interop. Null-terminated pointers are critical when working with C and character arrays, so even though it’s been de-emphasized in Zig, I think there is still a lot of value there for understanding how Zig works in general.

1 Like

Null terminated pointers use half the memory of a slice, so there are legitimate use cases for reducing memory usage.

1 Like

This argument while true theoretically is a very bad advice in practice. 50 years of C taught us the null terminated arrays are major cause of serious security bugs. Also, getting length of the null-terminated array is O(n) runtime versus O(1) for a slice. Last but not least, memory savings from not storing length field is rarely realized, for allocator’s alignment will pad null byte all way up to 4 or 8 bytes, the same size as the lenght field.

TL;DR: Use null terminated arryas (pointers, slices) only when you absolutely have to (interface to C). Use slices in all other cases.

1 Like

I disagree that it always bad. O(n) length check is only a problem for very large arrays and if you aren’t modifying the array after you create it then it’s almost impossible to introduce security bugs. Additionally, Zig’s sentinel system and various stdlib functions for working with sentinel terminated slices make it far safer than in C.

1 Like