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_ulonglongFloating-point:
c_float
c_double
c_longdoubleThese follow the target C ABI.
F.6 C Strings
A C string is usually:
[*:0]const u8Meaning:
- 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_intThis is a special C pointer type.
Example:
extern fn strlen(s: [*:0]const u8) usize;Nullable pointer:
?*TF.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.hThis 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
errnoWrap 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