Zig C Interop
Purpose
Guide agents through Zig's C interoperability: @cImport /@cInclude for calling C, translate-c for header inspection, extern struct and packed struct for ABI-compatible types, exporting Zig for C consumption, and zig cc for mixed C/Zig builds.
Triggers
-
"How do I call a C function from Zig?"
-
"How do I use @cImport and @cInclude?"
-
"How do I export Zig functions to be called from C?"
-
"How do I define a struct that matches a C struct?"
-
"What does translate-c do?"
-
"How do I build a mixed C and Zig project?"
Workflow
- Calling C from Zig with @cImport
const c = @cImport({ @cInclude("stdio.h"); @cInclude("string.h"); @cInclude("mylib.h"); @cDefine("MY_FEATURE", "1"); // Equivalent to -DMY_FEATURE=1 @cUndef("SOME_MACRO"); });
pub fn main() void { _ = c.printf("Hello from C: %d\n", @as(c_int, 42));
var buf: [256]u8 = undefined;
_ = c.snprintf(&buf, buf.len, "formatted: %d", @as(c_int, 100));
}
In build.zig :
exe.linkLibC(); // Required when using C functions exe.addIncludePath(b.path("include/"));
- translate-c — inspect C header translation
translate-c converts C headers to Zig declarations, letting you see exactly how Zig sees a C API:
Translate a header file
zig translate-c /usr/include/stdio.h > stdio.zig
Translate with defines/includes
zig translate-c -I include/ -DFEATURE=1 mylib.h > mylib.zig
Translate and inspect specific types
zig translate-c mylib.h | grep -A5 "struct MyStruct"
This is Zig's equivalent of bindgen — you use it to understand what Zig generates, then use @cImport directly in code.
- C type mapping
C type Zig type
int
c_int
unsigned int
c_uint
long
c_long
unsigned long
c_ulong
long long
c_longlong
size_t
usize
ssize_t
isize
char *
[*:0]u8 (null-terminated)
const char *
[*:0]const u8
void *
*anyopaque
NULL
null
bool
bool (C99) or c_int (older)
float
f32
double
f64
// Passing strings to C const str = "hello"; _ = c.puts(str); // Zig string literals are [*:0]const u8
// Dynamic strings — need null terminator var buf: [64:0]u8 = undefined; const len = std.fmt.bufPrint(buf[0..63], "hello {d}", .{42}) catch unreachable; buf[len] = 0; _ = c.puts(&buf);
- extern struct — ABI-compatible structs
Use extern struct to match a C struct's memory layout exactly:
// Matches: struct Point { int x; int y; }; const Point = extern struct { x: c_int, y: c_int, };
// Matches: struct Header { uint32_t magic; uint16_t version; uint16_t flags; }; const Header = extern struct { magic: u32, version: u16, flags: u16, };
// Use with C API var p = Point{ .x = 10, .y = 20 }; _ = c.draw_point(&p);
- packed struct — bit-level layout
// Matches C bitfield: struct { uint8_t flags : 4; uint8_t type : 4; }; const Flags = packed struct(u8) { mode: u4, kind: u4, };
// Packed struct for wire protocols const IpHeader = packed struct(u32) { ihl: u4, version: u4, tos: u8, total_length: u16, };
var h: IpHeader = @bitCast(@as(u32, raw_bytes));
- Exporting Zig to C
// Export a function callable from C export fn zig_add(a: c_int, b: c_int) c_int { return a + b; }
// Export with specific calling convention pub fn my_func(x: u32) callconv(.C) u32 { return x * 2; }
// Export a struct (use extern struct for C layout) export const VERSION: c_int = 42;
Generate a C header (manual or with tools):
/* mylib.h */ #ifndef MYLIB_H #define MYLIB_H #include <stdint.h>
int zig_add(int a, int b); uint32_t my_func(uint32_t x); extern int VERSION;
#endif
- Calling Zig from C in build.zig
// Build Zig as a C-compatible static library const lib = b.addStaticLibrary(.{ .name = "myzig", .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, });
// The C code that uses the Zig library const c_exe = b.addExecutable(.{ .name = "c_consumer", .target = target, .optimize = optimize, }); c_exe.addCSourceFile(.{ .file = b.path("src/main.c"), .flags = &.{"-std=c11"}, }); c_exe.linkLibrary(lib); c_exe.linkLibC(); b.installArtifact(c_exe);
- Opaque types and forward declarations
// Forward-declared C struct (opaque) const FILE = opaque {}; extern fn fopen(path: [:0]const u8, mode: [:0]const u8) ?*FILE; extern fn fclose(file: *FILE) c_int; extern fn fprintf(file: FILE, fmt: [:0]const u8, ...) c_int;
// Opaque handle pattern const MyHandle = opaque {}; extern fn lib_create() ?*MyHandle; extern fn lib_destroy(h: *MyHandle) void;
- Variadic functions
// Call variadic C functions using @call with variadic args const c = @cImport(@cInclude("stdio.h"));
// printf works directly through @cImport _ = c.printf("value: %d\n", @as(c_int, 42));
// For custom variadic C functions, use extern with ... extern fn my_log(level: c_int, fmt: [*:0]const u8, ...) void;
For translate-c output guide and C ABI types reference, see references/translate-c-guide.md.
Related skills
-
Use skills/zig/zig-compiler for zig cc C compilation and basic Zig builds
-
Use skills/zig/zig-build-system for build.zig with mixed C/Zig projects
-
Use skills/binaries/elf-inspection to verify symbol exports and ABI
-
Use skills/rust/rust-ffi for comparison with Rust's C FFI approach