I wrote this to better understand Zig’s many-item pointers, a type safety feature for working with C libraries.
Zig offers translate-c as a way to translate C source & header files into Zig code. Translating primitive types is trivial: C’s unsigned int becomes c_uint. However, pointer types are trickier. There’s no way to determine whether a pointer is referencing a single value or the start of an array, so all pointers are translated to Zig’s C pointer type [*c]T (a C pointer to one or more T).
C code before translation:
int fizz(int *buzz) {
// ...
}
int foo(int size, int *bar) {
// ...
}
Translated into Zig:
pub fn fizz(buzz: [*c]c_int) c_int {
// ...
}
pub fn foo(size: c_int, bar: [*c]c_int) c_int {
// ...
}
The Zig language reference recommends avoiding using C pointers directly. The idea is that we should know which pointer type a translated function expects and edit the generated Zig code to use the proper single-item pointer *T or a many-item pointer [*]T:
pub fn fizz(buzz: *c_int) c_int {
// ...
}
pub fn foo(size: c_int, bar: [*]c_int) c_int {
// ...
}
Trying to to pass a slice to a function expecting a many-item pointer results in a compilation error:
fn expectManyItemPointer(comptime T: type, s: [*]T, len: usize) void {
for (0..len) |i| {
_ = s[i];
}
}
test expectManyItemPointer {
const foo: []const u8 = "hello world";
expectManyItemPointer(u8, foo, 11);
}
error: expected type '[*]const u8', found '[]const u8'
expectManyItemPointer(u8, foo, 11);
^~~
It’s actually pretty easy to get around this, we can pass in the slice’s pointer instead, because slices are just fat pointers with a base address and length:
const foo: []const u8 = "hello world";
// before (compilation error)
expectManyItemPointer(u8, foo, 11);
// after (good, no error, amazing)
expectManyItemPointer(u8, foo.ptr, 11);
The original reason I wrote this post was because I was confused when trying to pass a single-item pointer to a function expecting a many-item pointer. This Zig code doesn’t compile because a *c_uint cannot coerce to a [*]c_uint:
var vbo: c_uint = undefined;
gl.GenBuffers(1, &vbo);
error: expected type '[*]c_uint', found '*c_uint'
gl.GenBuffers(1, &vbo);
^~~~
After smacking my head on the wall I eventually found that passing a pointer to a single-element array would work, but it’s ugly and I hate it:
var vbo: [1]c_uint = undefined;
gl.GenBuffers(1, &vbo);
I’d have to access it with vbo[0] everywhere even though I know it’s just one element, and that sucks. Technically, @ptrCast also works here, but the folks in #zig told me that would be uncouth, ungodly, and unwise. So instead I found a simple, idiomatic, non-@ptrCast way to do it, just by reading the docs. Can you imagine that? We can coerce a single-item pointer to a many-item pointer by slicing it with a length of one:
var vbo: c_uint = undefined;
gl.GenBuffers(1, (&vbo)[0..1]);
At first glance it wasn’t obvious what this snippet did, so I’ll break it down: With &vbo we’re taking the address of vbo, which yields a single-item pointer, *c_uint. Then we slice it with the [start..len] syntax. And because both bounds of the slice are known at compile time the resulting type is a pointer to an array with one element, *[1]c_uint. And because pointers to arrays coerce to many-item pointers, we can pass (&vbo)[0..1] to any function expecting a many-item pointer.
This little area of Zig was interesting to explore and learn more about its pointers and type coercion. I hope you learned something from this post, or find it helpful when running into the same problem I had. Thanks for reading :)