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.