Zig C interop
Contract
| Field | Bound contract |
|---|---|
| Trigger | The user calls a C function from Zig, exposes a Zig function to C, needs a struct that matches a C layout, wants to see how a C header translates, or builds a project that mixes C and Zig sources. |
| Authority | Read-only. The skill emits Zig declarations, build.zig lines, and translate-c commands to chat; the user writes them. Rollback is not needed. No remote mutation. |
| Side effect | Chat output; translate-c output goes where the user redirects it. |
| Done | The declarations and build lines for the request are reported, they compile against libc on the installed Zig in a scratch file, and every C type in the request has its Zig mapping. |
Inputs
- Zig version from
zig version: required. The samples ran on Zig 0.14.1. - The C header or function signatures involved: required.
- Direction: required. C called from Zig, Zig called from C, or both.
- Whether the C code lives in the project or in a system library: required; decides
addCSourceFileversuslinkSystemLibrary.
Procedure
Call C from Zig with
@cImport:const c = @cImport({ @cInclude("stdio.h"); @cDefine("MY_FEATURE", "1"); @cUndef("SOME_MACRO"); });then_ = c.printf("value: %d\n", @as(c_int, 42));. Variadic C functions such asprintfwork through the import. Single-file builds need-lc; inbuild.zigcallexe.linkLibC()andexe.addIncludePath(b.path("include")). Done when: the call compiles and links against libc.Inspect the translation when a declaration looks wrong:
zig translate-c -lc -I include -DFEATURE=1 mylib.h > mylib.zig, or with-target aarch64-linux-gnuto see another platform's layout. Read the output to learn the generated names and types, then keep using@cImportin code; the translated file is a reference, not a source to commit. Done when: the generated declaration for the symbol in question is read.Map C types from the table;
translate-coutput is the authority for anything not listed. Done when: every parameter and return type in the request is mapped.C Zig int,unsigned,long,unsigned long,long longc_int,c_uint,c_long,c_ulong,c_longlongsize_t,ssize_tusize,isizechar *(null-terminated)[*:0]u8;const char *is[*:0]const u8; generated code shows[*c]u8void **anyopaque; nullable?*anyopaqueNULLnullbool(C99)boolfloat,doublef32,f64T *returned from C?*Twhen it may be null;[*c]Tin generated codeZig string literals are already
[*:0]const u8. Build a dynamic C string withstd.fmt.bufPrintZ(&buf, "hello {d}", .{42})on a[N:0]u8buffer and pass.ptr. Done when: no string crosses the boundary without a terminator.Match C layouts with
extern struct, which uses the C ABI layout:const Point = extern struct { x: c_int, y: c_int };. Match bitfields and wire formats with a backed packed struct:const Flags = packed struct(u8) { mode: u4, kind: u4 };and convert with@bitCast. Model a forward-declared C type asconst FILE = opaque {};and declare its functions withextern fn fopen(path: [*:0]const u8, mode: [*:0]const u8) ?*FILE;. Done when: each C struct has one Zig counterpart with the same layout rule.Export Zig to C:
export fn zig_add(a: c_int, b: c_int) c_int { return a + b; }exports the symbol with the C calling convention; a non-exported C-callable function ispub fn f(x: u32) callconv(.c) u32; data isexport const VERSION: c_int = 42;. Write the matching C header by hand with the C types from the table (int zig_add(int, int); extern int VERSION;). Done when: the header's prototypes match the exported signatures.Build the mixed project. Zig library consumed by C: build with
b.addLibrary(.{ .linkage = .static, ... }), then a C executable withc_exe.addCSourceFile(.{ .file = b.path("src/main.c"), .flags = &.{"-std=c11"} }),c_exe.linkLibrary(lib),c_exe.linkLibC(). C consumed by Zig:exe.addCSourceFiles,exe.addIncludePath,exe.linkLibC(), orexe.linkSystemLibrary("name")for an installed library. Details of the build graph: use zig-build-system. Done when: the artifact links.Handle memory across the boundary: memory from C
mallocis freed withc.free;std.heap.c_allocatorgives Zig code an allocator backed by libc so buffers can cross either way. Model a C pointer that may beNULLas an optional and handle it withorelse. Done when: every allocation has one owner and every nullable pointer is checked.Confirm with a scratch file compiled by
zig build-exe scratch.zig -lcand, for exports, inspect the symbol table (use elf-inspection). Done when: the scratch build runs.
Failure and recovery
| Failure class | Behavior |
|---|---|
@cImport struct has no member name |
The header that declares it is not included or the macro guard hid it; add the @cInclude or @cDefine. |
translate-c fails on size_t or similar |
The header relies on an include it does not pull in; pass -lc and add the missing @cInclude or -I. |
| Calling convention mismatch | Non-exported callbacks passed to C need callconv(.c); the 0.14.1 spelling is lowercase .c. |
| Bitfield layout differs from C | Zig packed structs pack from the least significant bit; confirm against the C compiler's layout on the target before relying on it. |
| Build graph question | build.zig structure: use zig-build-system. |
Output
A chat report with the @cImport block or extern declarations, the type mapping for every signature in the request, the build.zig lines that link the pieces, and the scratch-build line confirming they compile on the installed Zig.