highly fashionable

Many-Item Pointers in Zig

Apr. 27th, 2026

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 :)

Raise the Bottom Line for Humanity
Thoughts on Other Peoples' thoughts on AI