
Zig Cinterop
- 297 installs
- 155 repo stars
- Updated June 27, 2026
- mohitmishra786/low-level-dev-skills
Bind C libraries from Zig with correct calling conventions, struct layouts, and build.zig link steps for native extensions and systems tooling.
About
Explains Zig-to-C interoperability: importing headers, modeling C types with comptime checks, configuring build.zig link flags, and wrapping foreign calls with Zig error handling for native CLI and API tooling.
- @cImport and header binding
- C ABI struct alignment
- build.zig linkSystemLibrary
- error union around errno
- cross-compile libc targets
Zig Cinterop by the numbers
- 297 all-time installs (skills.sh)
- +21 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,358 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mohitmishra786/low-level-dev-skills --skill zig-cinteropAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 297 |
|---|---|
| repo stars | ★ 155 |
| Last updated | June 27, 2026 |
| Repository | mohitmishra786/low-level-dev-skills ↗ |
What it does
Bind C libraries from Zig with correct calling conventions, struct layouts, and build.zig link steps for native extensions and systems tooling.
Files
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
1. 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/"));2. 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.
3. 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);4. 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);5. 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));6. 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;
#endif7. 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);8. 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;9. 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-compilerforzig ccC compilation and basic Zig builds - Use
skills/zig/zig-build-systemforbuild.zigwith mixed C/Zig projects - Use
skills/binaries/elf-inspectionto verify symbol exports and ABI - Use
skills/rust/rust-ffifor comparison with Rust's C FFI approach
translate-c Guide and C ABI Types Reference
Using translate-c
Basic usage
# Translate a system header
zig translate-c /usr/include/fcntl.h 2>/dev/null | head -100
# Translate with include paths and defines
zig translate-c \
-I /usr/include \
-I ./vendor/mylib/include \
-DLINUX \
-D_GNU_SOURCE \
./vendor/mylib/include/mylib.h > mylib_translated.zig
# Cross-platform translation
zig translate-c \
-target aarch64-linux-gnu \
/usr/include/sys/socket.h > socket_arm.zigWhat translate-c produces
Given C:
typedef struct {
int x;
int y;
float z;
} Vec3;
int process(Vec3 *v, size_t count, const char *name);
#define MAX_ITEMS 1024translate-c generates:
pub const Vec3 = extern struct {
x: c_int = @import("std").mem.zeroes(c_int),
y: c_int = @import("std").mem.zeroes(c_int),
z: f32 = @import("std").mem.zeroes(f32),
};
pub extern fn process(v: ?*Vec3, count: usize, name: ?[*:0]const u8) c_int;
pub const MAX_ITEMS = @as(c_int, 1024);Workflow: translate-c → @cImport
1. Run translate-c to see Zig's interpretation of the C API 2. Identify type mappings, function signatures, pointer nullability 3. Use @cImport in actual code (handles the translation automatically) 4. Only use the translated output as a manual reference, not as source
C ABI Type Mapping Reference
Integer types
| C | Zig builtin | Notes |
|---|---|---|
char | u8 / i8 | Sign is implementation-defined |
signed char | i8 | Always signed |
unsigned char | u8 | Always unsigned |
short | c_short | Platform-dependent size |
unsigned short | c_ushort | |
int | c_int | Typically 32-bit |
unsigned int | c_uint | |
long | c_long | 32-bit on Windows, 64-bit on Linux/macOS |
unsigned long | c_ulong | |
long long | c_longlong | 64-bit |
unsigned long long | c_ulonglong | |
size_t | usize | |
ssize_t | isize | |
ptrdiff_t | isize | |
intptr_t | isize | |
uintptr_t | usize | |
int8_t | i8 | |
uint8_t | u8 | |
int16_t | i16 | |
uint16_t | u16 | |
int32_t | i32 | |
uint32_t | u32 | |
int64_t | i64 | |
uint64_t | u64 |
Pointer types
| C | Zig |
|---|---|
void * | *anyopaque |
const void * | *const anyopaque |
char * (null-terminated) | [*:0]u8 |
const char * (null-terminated) | [*:0]const u8 |
char * (known length) | [*]u8 or []u8 |
T * (nullable) | ?*T |
T * (non-null) | *T |
T ** | *?*T or **T |
void (*fn)(int) | *const fn (c_int) callconv(.C) void |
Enum types
// C enum
typedef enum {
STATUS_OK = 0,
STATUS_ERR = 1,
STATUS_BUSY = 2,
} Status;// Zig equivalent with C ABI
const Status = enum(c_int) {
ok = 0,
err = 1,
busy = 2,
};Common Patterns
Callback functions (function pointers)
// C API with callback
typedef void (*callback_t)(void *ctx, int event);
void register_callback(callback_t cb, void *ctx);const c = @cImport(@cInclude("mylib.h"));
fn my_callback(ctx: ?*anyopaque, event: c_int) callconv(.C) void {
const self = @as(*MyType, @ptrCast(@alignCast(ctx)));
self.handle(event);
}
// Register
c.register_callback(my_callback, @ptrCast(my_obj));Error handling (C errno pattern)
const c = @cImport({
@cInclude("errno.h");
@cInclude("string.h");
});
fn open_file(path: [*:0]const u8) !void {
const fd = c.open(path, c.O_RDONLY);
if (fd < 0) {
const err = c.__errno_location().*; // Linux
const msg = c.strerror(err);
std.log.err("open failed: {s}", .{msg});
return error.OpenFailed;
}
defer _ = c.close(fd);
}Memory management with C allocator
const c = @cImport({
@cInclude("stdlib.h");
@cInclude("string.h");
});
// Allocate C memory (must free with c.free)
const buf = c.malloc(1024) orelse return error.OutOfMemory;
defer c.free(buf);
const typed: [*]u8 = @ptrCast(buf);
// Use Zig allocator as C allocator via std.heap.c_allocator
const allocator = std.heap.c_allocator;
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);Null Safety
Zig's pointer model forces explicit null handling:
// C: char *result = get_name(id); // might return NULL
// Zig: ?[*:0]const u8
const result: ?[*:0]const u8 = c.get_name(id);
if (result) |name| {
std.debug.print("name: {s}\n", .{name});
} else {
std.debug.print("not found\n", .{});
}
// Or with orelse
const name = c.get_name(id) orelse {
return error.NotFound;
};