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. 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.
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 {
// ...
}
Working directly with C pointers is a hassle, and unnecessary. We should know what type of pointer the C function expects and wrap it using a single-item pointer *T or a many-item pointer [*]T:
const c = @import("my-cool-translated-c.zig");
pub fn fizz(buzz: *c_int) c_int {
return c.fizz(buzz);
}
pub fn foo(size: c_int, bar: [*]c_int) c_int {
return c.foo(size, bar);
}
Working with Many-Item Pointers
Consider the following example, where I try to pass a slice to a function expecting a many-item pointer:
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);
}
The compiler dazzles us with the error:
error: expected type '[*]const u8', found '[]const u8'
expectManyItemPointer(u8, foo, 11);
^~~
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);
That’s ezpz. Pointers = solved. Except the reason I wrote this post was because I was troubled by passing the use case of passing a single-item pointer to a function expecting a many-item pointer.
For example, this Zig code will not compile because a *c_uint cannot coerce to a [*]c_uint:
var vbo: c_uint = undefined;
glGenBuffers(1, &vbo);
error: expected type '[*]c_uint', found '*c_uint'
glGenBuffers(1, &vbo);
^~~~
You could pass in an a pointer to an array of one item:
var vbo: [1]c_uint = undefined;
glGenBuffers(1, &vbo);
But when you have to access it with vbo[0] everywhere when you know it’s just one element and that’s gross and I hate it. Is there a cleaner, idiomatic solution to this? Is there a justified use case of @ptrCast–NO! 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]);
What on god’s green earth is (&vbo)[0..1]? With &vbo we’re taking the address of vbo, which is typed as a single-item pointer *c_uint. Then we slice it with the [start..len] syntax, which is enforced by the compiler to never be greater than one for a single-item pointer, and because both bounds of the slice are known at compile time the resulting type is a pointer to an array with a length of one, *[1]c_uint. Because pointers to arrays coerce to many-item pointers the code compiles and we’re off to the races.