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 u8A 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/includeIn 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 -lmIn 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 helloIt can also cross-compile C programs.
zig cc hello.c -target x86_64-windows-gnu -o hello.exeThis 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_longThese 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]TThis 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:
- Store a pointer to a Zig allocator or context.
- Provide C-compatible callback functions.
- Cast the user data pointer back to your Zig type.
- 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 1024Function-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:
| Input | Why it matters |
|---|---|
| Header files | Define functions, structs, macros |
| Include paths | Let Zig find headers |
| Target | Affects type sizes and ABI |
| C compiler flags | Affect conditional declarations |
| Linked libraries | Provide implementation |
If translation fails, check headers and include flags first.
G.19 Common C Interop Errors
| Error | Usual cause |
|---|---|
| Header not found | Missing -I include path |
| Undefined symbol | Library not linked |
| Type mismatch | Wrong Zig type or pointer type |
| Invalid null pointer | Missing null check |
| Macro missing | Macro cannot be translated |
| ABI mismatch | Wrong 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 u8The 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.