⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

C Primitive Types

const std = @import("std");
const expect = std.testing.expect;

test "c types are separate from fixed-width ones" {
    // `c_int` is whatever `int` is on this target. Use it only at the
    // C boundary, and use i32/u64/etc everywhere else.
    try expect(@sizeOf(c_int) == 4);
    try expect(@sizeOf(c_char) == 1);
    try expect(c_int != i32); // distinct types, even at the same size
}

test "c_long varies by target" {
    // 4 bytes on wasm32 and Windows, 8 on 64-bit Linux. This is exactly
    // why the type exists rather than hardcoding i64.
    try expect(@sizeOf(c_long) == 4); // wasm32
}

test "converting at the boundary" {
    const n: i32 = 42;
    const c: c_int = @intCast(n);
    try expect(c == 42);
}

test "the C ABI integer for pointers" {
    try expect(@sizeOf(usize) == @sizeOf(*anyopaque));
}

Zig provides c_char, c_short, c_int, c_long, c_longlong, their unsigned counterparts, and c_longdouble. Their sizes are whatever the target’s C ABI says.

Why not just use i32?

Because long is not one size. It is 8 bytes on 64-bit Linux and macOS, 4 bytes on Windows and on wasm32. Code that hardcodes i64 for a C long is correct on one platform and silently wrong on another, the kind of bug that appears only after a port.

c_int and i32 are distinct types even when they are the same size, so the compiler will not let you conflate them by accident.

Keep them at the boundary

Convert at the edge and use fixed-width types inside:

export fn process(count: c_int) callconv(.c) void {
    const n: u32 = @intCast(count);   // now in Zig's world
    ...
}

Threading c_int through your whole program spreads a target-dependent assumption everywhere.

anyopaque

anyopaque is Zig’s void in the C sense: the pointee type of void*. So C’s void *ptr becomes *anyopaque, and C’s void f(void) returns Zig’s void. The two meanings of C’s void are separated.

For handles you control, prefer a distinct opaque {} type over *anyopaque: it keeps different handle kinds from being interchangeable.