ABI
const std = @import("std");
const expect = std.testing.expect;
// `extern struct` guarantees C layout: declaration order, C padding rules.
const CPoint = extern struct {
x: i32,
y: i32,
};
// `packed struct` is bit-level and backed by an integer, with no padding.
const Flags = packed struct {
a: bool,
b: bool,
rest: u6,
};
// A plain struct may be reordered and padded however the compiler likes.
const Loose = struct {
small: u8,
big: u64,
};
test "extern struct follows C layout" {
try expect(@sizeOf(CPoint) == 8);
try expect(@offsetOf(CPoint, "x") == 0);
try expect(@offsetOf(CPoint, "y") == 4);
}
test "packed struct is exactly its bits" {
try expect(@bitSizeOf(Flags) == 8);
try expect(@sizeOf(Flags) == 1);
const f = Flags{ .a = true, .b = false, .rest = 0 };
// Packed structs convert to their backing integer.
try expect(@as(u8, @bitCast(f)) == 1);
}
test "plain structs make no layout promise" {
// Zig is free to order these for packing, so do not assume offsets.
try expect(@sizeOf(Loose) >= 9);
}
test "extern union and enum" {
const E = extern union { i: i32, f: f32 };
try expect(@sizeOf(E) == 4);
// An enum with a C ABI tag type.
const Colour = enum(c_int) { red, green, blue };
try expect(@intFromEnum(Colour.green) == 1);
}
// `callconv(.c)` makes a Zig function callable from C.
export fn addFromC(a: c_int, b: c_int) callconv(.c) c_int {
return a + b;
}
test "exported function is callable from Zig too" {
try expect(addFromC(2, 3) == 5);
}A plain Zig struct makes no layout promise. Fields may be reordered and
padded however the compiler prefers. That is a feature (it lets the optimiser
pack things), but it means you cannot hand one to C.
The three layouts
| Declaration | Layout | Use for |
|---|---|---|
struct | unspecified | ordinary Zig code |
extern struct | C ABI: declaration order, C padding | C interop |
packed struct | exact bits, no padding, integer-backed | wire formats, registers |
const CPoint = extern struct { x: i32, y: i32 }; // offsets 0 and 4
const Flags = packed struct { a: bool, b: bool, rest: u6 }; // exactly 8 bits
A packed struct converts to and from its backing integer with @bitCast,
which makes it the natural way to describe hardware registers and protocol
headers.
Verifying, not assuming
@sizeOf, @offsetOf, and @bitSizeOf are comptime values, so a layout
assumption can be asserted at compile time:
comptime {
std.debug.assert(@offsetOf(CPoint, "y") == 4);
}
Worth doing at any real C boundary: it turns a silent mismatch into a build failure.
Calling conventions
callconv(.c) gives a function the C calling convention, and export gives it
an unmangled symbol name:
export fn add(a: c_int, b: c_int) callconv(.c) c_int {
return a + b;
}
That is all it takes for C to call Zig. Going the other way, declare the C
function with extern:
extern "c" fn abs(n: c_int) c_int;
Do not extern everything
extern struct costs you the packing the optimiser would otherwise do. Use it
at the boundary, and use ordinary structs inside.