Skip to content

Appendix G. C Interop Reference

Zig is designed to work closely with C. You can call C from Zig, call Zig from C, compile C code with Zig, and link Zig programs against existing C libraries.

Zig is designed to work closely with C. You can call C from Zig, call Zig from C, compile C code with Zig, and link Zig programs against existing C libraries.

This appendix gives the practical reference you need first.

G.1 Importing a C Header

Use @cImport with @cInclude.

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

Then call C functions through c.

pub fn main() void {
    _ = c.printf("Hello from C\n");
}

The _ = discards the return value intentionally.

G.2 Calling C Functions

C functions keep their C names.

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

pub fn main() void {
    const n = c.abs(-10);
    _ = n;
}

Zig checks the imported C declarations and gives you typed access to them.

G.3 C Strings

C strings are null-terminated byte pointers.

In Zig, a C string type often appears as:

[*c]const u8

A Zig string literal can usually be passed to C when it has a sentinel:

_ = c.printf("hello\n");

For dynamic strings, make sure the buffer is null-terminated before passing it to C.

G.4 Include Paths

If Zig cannot find a header, add an include path.

zig build-exe main.zig -I/usr/include

In build.zig, include paths are usually added to the executable or module.

exe.addIncludePath(.{ .cwd_relative = "include" });

G.5 Linking C Libraries

If a C function comes from a library, you must link that library.

Example with math library on Unix-like systems:

zig build-exe main.zig -lc -lm

In build.zig:

exe.linkLibC();
exe.linkSystemLibrary("m");

linkLibC() links the C standard library.

G.6 Compiling C with Zig

Zig can act as a C compiler.

zig cc hello.c -o hello

It can also cross-compile C programs.

zig cc hello.c -target x86_64-windows-gnu -o hello.exe

This is useful even in projects that contain no Zig code.

G.7 Mixing Zig and C Source Files

A Zig project can compile C files as part of the build.

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

Then import the C header from Zig:

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

G.8 Exporting Zig Functions to C

Use export to make a Zig function visible to C.

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

Use C-compatible types such as:

c_int
c_uint
c_char
c_long

These match the target platform’s C ABI.

G.9 Calling Zig from C

Suppose Zig exports this:

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

A C header might declare:

int add(int a, int b);

Then C code can call:

int x = add(2, 3);

The important rule: exported Zig functions must use a C-compatible ABI and C-compatible types.

G.10 Structs from C

C structs imported into Zig can be accessed through c.

C:

struct Point {
    int x;
    int y;
};

Zig:

const p = c.struct_Point{
    .x = 10,
    .y = 20,
};

C names may be translated slightly so they fit Zig’s namespace rules.

G.11 Passing Struct Pointers

Many C APIs take pointers to structs.

var p = c.struct_Point{
    .x = 1,
    .y = 2,
};

c.move_point(&p);

The &p passes the address of p.

G.12 C Pointers

C pointers are less strict than normal Zig pointers.

You may see:

[*c]T

This means a C pointer. It may behave like a nullable pointer or pointer-like value depending on context.

Prefer converting C data into safer Zig types when possible.

For example, if C gives you a pointer and a length, convert it to a slice:

const slice = ptr[0..len];

Then use the slice in Zig code.

G.13 Null from C

C often uses NULL.

In Zig, C pointers may need null checks.

const ptr = c.get_data();

if (ptr == null) {
    return error.NoData;
}

Do not dereference a C pointer before checking whether it is valid.

G.14 C Memory Allocation

C APIs may allocate memory using malloc.

const ptr = c.malloc(100);
defer c.free(ptr);

Memory allocated by C should usually be freed by C.

Memory allocated by Zig should usually be freed by the Zig allocator that created it.

Do not mix allocation systems unless the API explicitly allows it.

G.15 Zig Allocators and C APIs

Some C APIs let you provide custom allocation callbacks.

This is how you can connect Zig’s allocator model to C libraries.

The pattern is usually:

  1. Store a pointer to a Zig allocator or context.
  2. Provide C-compatible callback functions.
  3. Cast the user data pointer back to your Zig type.
  4. Allocate or free using the Zig allocator.

This is advanced. Start with normal C allocation rules first.

G.16 C Macros

Zig can import many C macros, but not all macros translate cleanly.

Simple constant macros often work:

#define MAX_SIZE 1024

Function-like macros may not work the way you expect:

#define SQUARE(x) ((x) * (x))

For complex macros, write a small C wrapper function.

C wrapper:

int square_int(int x) {
    return SQUARE(x);
}

Then call square_int from Zig.

G.17 C Enums

C enums usually import as integer-like values.

C:

enum Color {
    RED,
    GREEN,
    BLUE,
};

Zig:

const color = c.RED;

C enums are less strict than Zig enums. Treat them carefully when crossing the boundary.

G.18 C Header Translation

@cImport runs C translation.

That means Zig reads the header and creates Zig declarations from it.

This depends on:

InputWhy it matters
Header filesDefine functions, structs, macros
Include pathsLet Zig find headers
TargetAffects type sizes and ABI
C compiler flagsAffect conditional declarations
Linked librariesProvide implementation

If translation fails, check headers and include flags first.

G.19 Common C Interop Errors

ErrorUsual cause
Header not foundMissing -I include path
Undefined symbolLibrary not linked
Type mismatchWrong Zig type or pointer type
Invalid null pointerMissing null check
Macro missingMacro cannot be translated
ABI mismatchWrong calling convention or target

G.20 extern Declarations

You can declare C functions manually with extern.

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

Then call it:

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

Use @cImport when possible. Use extern when you need a small manual declaration.

G.21 Calling Convention

C ABI functions use the C calling convention.

fn callback(x: c_int) callconv(.c) void {
    _ = x;
}

Use this when passing a Zig function pointer to C.

G.22 Callbacks from C into Zig

C libraries often accept callbacks.

Zig callback:

fn onEvent(value: c_int) callconv(.c) void {
    _ = value;
}

Pass it to C:

c.set_callback(onEvent);

The callback must use the calling convention expected by the C library.

G.23 Opaque Types

Some C libraries hide struct definitions.

C:

typedef struct Database Database;

Zig treats this as an opaque type. You can pass pointers around, but you cannot access fields.

const db = c.db_open("file.db");
defer c.db_close(db);

This is common in C APIs. It is a good design for hiding implementation details.

G.24 Sentinels and C Strings

Zig has sentinel-terminated pointer and slice types.

[:0]const u8
[*:0]const u8

The 0 means the data ends with a zero byte.

This matches C strings.

Example:

const name: [:0]const u8 = "zig";

Use sentinel types when an API needs a null-terminated string.

G.25 Practical Rule

At the Zig-C boundary, be conservative.

Use C-compatible types.

Check nulls.

Match allocation and free functions.

Use the correct calling convention.

Wrap unsafe C APIs behind small Zig functions.

A good Zig wrapper turns a loose C API into a safer Zig API.