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

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

DeclarationLayoutUse for
structunspecifiedordinary Zig code
extern structC ABI: declaration order, C paddingC interop
packed structexact bits, no padding, integer-backedwire 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.