Skip to content

Appendix F. C Interop Reference

One of Zig's central goals is direct interoperability with C. Zig can call C code, compile C code, link system libraries, export functions to C, and translate C headers.

One of Zig’s central goals is direct interoperability with C. Zig can call C code, compile C code, link system libraries, export functions to C, and translate C headers.

This appendix gives the common forms.

F.1 Calling a C Function

A C function declaration uses extern.

extern fn puts(s: [*:0]const u8) c_int;

Use it like an ordinary Zig function.

pub fn main() void {
    _ = puts("hello from c");
}

c_int is the Zig type matching C int.

F.2 @cImport

Most C libraries are imported with @cImport.

const c = @cImport({
    @cInclude("stdio.h");
});

Use C declarations through the namespace.

pub fn main() void {
    _ = c.printf("value = %d\n", 123);
}

F.3 Importing Multiple Headers

const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
    @cInclude("string.h");
});

F.4 Defining C Macros

const c = @cImport({
    @cDefine("VALUE", "123");
    @cInclude("config.h");
});

Remove a macro:

const c = @cImport({
    @cUndef("min");
    @cInclude("windows.h");
});

F.5 Zig and C Integer Types

C compatibility types:

c_char
c_short
c_ushort
c_int
c_uint
c_long
c_ulong
c_longlong
c_ulonglong

Floating-point:

c_float
c_double
c_longdouble

These follow the target C ABI.

F.6 C Strings

A C string is usually:

[*:0]const u8

Meaning:

  • many-item pointer
  • terminated by zero
  • immutable bytes

Example:

extern fn puts(s: [*:0]const u8) c_int;

A Zig string literal can be passed directly.

_ = puts("hello");

F.7 C Pointers

C pointer:

int *

Usually becomes:

[*c]c_int

This is a special C pointer type.

Example:

extern fn strlen(s: [*:0]const u8) usize;

Nullable pointer:

?*T

F.8 Struct Layout

Use extern struct for C ABI layout.

const Point = extern struct {
    x: c_int,
    y: c_int,
};

Without extern, Zig may choose a different layout.

F.9 Packed Layout

Use packed struct for exact bit layout.

const Flags = packed struct {
    a: bool,
    b: bool,
    c: u6,
};

Packed layout is for protocols, devices, binary formats, and hardware registers.

F.10 Linking libc

Link the system C library:

exe.linkLibC();

Many POSIX APIs require this.

F.11 Linking a System Library

exe.linkSystemLibrary("m");

Examples:

exe.linkSystemLibrary("SDL2");
exe.linkSystemLibrary("sqlite3");
exe.linkSystemLibrary("ssl");

F.12 Compiling C Source Files

exe.addCSourceFile(.{
    .file = b.path("src/add.c"),
    .flags = &.{ "-std=c99" },
});

Multiple files:

exe.addCSourceFiles(.{
    .root = b.path("csrc"),
    .files = &.{
        "a.c",
        "b.c",
    },
});

F.13 Include Paths

exe.addIncludePath(b.path("include"));

System include path:

exe.addSystemIncludePath(.{
    .cwd_relative = "/usr/include",
});

F.14 Exporting Zig Functions

Expose a Zig function to C:

export fn add(a: c_int, b: c_int) c_int {
    return a + b;
}

Compile as a shared library or static library.

F.15 Calling Convention

Specify a calling convention:

fn callback() callconv(.c) void {}

Most C APIs use .c.

F.16 Opaque Types

Opaque types are useful for C handles.

const SDL_Window = opaque {};

A pointer may exist:

var window: *SDL_Window = undefined;

But the contents are unknown to Zig.

F.17 Translating Headers

Translate a C header:

zig translate-c header.h

This produces Zig declarations from the C source.

The output is often useful as a reference even if it is not checked into a project.

F.18 Memory Ownership

C APIs often allocate memory.

Example pattern:

const p = c.malloc(1024);
defer c.free(p);

Ownership rules must be explicit:

  • who allocates
  • who frees
  • which allocator is used
  • whether null is possible

Do not hide ownership behind wrappers unless the wrapper improves correctness.

F.19 Error Handling

C often signals failure with:

NULL
-1
0
errno

Wrap these immediately.

fn openFile() !File {
    const fd = c.open(...);

    if (fd < 0) {
        return error.OpenFailed;
    }

    return File{ .fd = fd };
}

Translate weak C conventions into Zig errors near the boundary.

F.20 Volatile Memory

Memory-mapped hardware often requires volatile.

const reg: *volatile u32 = @ptrFromInt(0x40000000);

Ordinary memory should not use volatile.

F.21 Inline Assembly

Zig supports inline assembly.

asm volatile ("nop");

Output and input operands:

asm volatile (
    "add %[b], %[a]"
    : [a] "=r" (result)
    : [b] "r" (value),
);

Assembly is target-specific. Keep it isolated and documented.

F.22 A Small Example

C source:

int add(int a, int b) {
    return a + b;
}

Build script:

exe.addCSourceFile(.{
    .file = b.path("src/add.c"),
});

Zig source:

extern fn add(a: c_int, b: c_int) c_int;

const std = @import("std");

pub fn main() void {
    const n = add(10, 20);

    std.debug.print("{d}\n", .{n});
}

This is the ordinary model for Zig and C:

  • declare external symbols
  • compile or link C code
  • use C APIs directly
  • convert weak interfaces into explicit Zig types at the boundary