sgongle online

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. 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 @ptrCastNO! 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.

Raise the Bottom Line for Humanity