TL;DR
Call C from Zig with zero overhead — use @cImport to include C headers, link C libraries, and pass data between Zig and C seamlessly. Free tutorial with examples.
Key concepts
- zig c interop
- call c from zig
- zig cImport
- zig c library
- zig ffi
- zig c integration
- zig c header import
C Interop
Zig has first-class C interoperability. You can call C functions, use C headers with @cImport, and link against C libraries — all without any bindings generator or FFI overhead.
C Types in Zig
Zig provides built-in C type equivalents for seamless interop.
const std = @import("std");
pub fn main() !void {
// Zig's C-compatible types
const types = .{
.{ "c_char", @sizeOf(u8) },
.{ "c_short", @sizeOf(c_short) },
.{ "c_int", @sizeOf(c_int) },
.{ "c_long", @sizeOf(c_long) },
.{ "c_longlong", @sizeOf(c_longlong) },
.{ "c_uint", @sizeOf(c_uint) },
.{ "c_ulong", @sizeOf(c_ulong) },
};
std.debug.print("C Type Sizes (platform-dependent):\n\n", .{});
inline for (types) |t| {
std.debug.print(" {s:<14} = {} bytes\n", .{ t[0], t[1] });
}
// Pointer type mapping
std.debug.print("\nPointer Mapping:\n", .{});
std.debug.print(" C: const char* -> Zig: [*c]const u8\n", .{});
std.debug.print(" C: int* -> Zig: [*c]c_int\n", .{});
std.debug.print(" C: void* -> Zig: ?*anyopaque\n", .{});
std.debug.print(" C: NULL -> Zig: null\n", .{});
// C strings are null-terminated
const c_string: [*:0]const u8 = "Hello from Zig";
const zig_slice = std.mem.span(c_string);
std.debug.print("\nC string: {s} (len={})\n", .{ zig_slice, zig_slice.len });
}
Working with C Strings
Convert between Zig slices and C null-terminated strings.
const std = @import("std");
fn cStringLength(s: [*:0]const u8) usize {
var len: usize = 0;
while (s[len] != 0) : (len += 1) {}
return len;
}
fn compareCStrings(a: [*:0]const u8, b: [*:0]const u8) bool {
const a_slice = std.mem.span(a);
const b_slice = std.mem.span(b);
return std.mem.eql(u8, a_slice, b_slice);
}
pub fn main() !void {
// Zig string literals are null-terminated
const greeting: [*:0]const u8 = "Hello, World!";
// Get length (like C's strlen)
const len = cStringLength(greeting);
std.debug.print("String: {s}\n", .{std.mem.span(greeting)});
std.debug.print("Length: {}\n", .{len});
// Compare strings (like C's strcmp)
const a: [*:0]const u8 = "hello";
const b: [*:0]const u8 = "hello";
const c: [*:0]const u8 = "world";
std.debug.print("\n\"hello\" == \"hello\": {}\n", .{compareCStrings(a, b)});
std.debug.print("\"hello\" == \"world\": {}\n", .{compareCStrings(a, c)});
// Convert Zig slice to C string with allocator
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const zig_string = "Dynamic string";
const c_copy = try allocator.dupeZ(u8, zig_string);
defer allocator.free(c_copy);
std.debug.print("\nDuped C string: {s}\n", .{c_copy});
std.debug.print("Is null-terminated: {}\n", .{c_copy[c_copy.len] == 0});
}
Simulating C Library Calls
Zig can call C standard library functions directly.
const std = @import("std");
// Simulating what C interop looks like
// In real code: const c = @cImport(@cInclude("stdlib.h"));
// Manual implementations matching C signatures
fn c_abs(n: c_int) c_int {
return if (n < 0) -n else n;
}
fn c_atoi(str: [*:0]const u8) c_int {
const slice = std.mem.span(str);
var result: c_int = 0;
var negative = false;
var i: usize = 0;
if (i < slice.len and (slice[i] == '-' or slice[i] == '+')) {
negative = slice[i] == '-';
i += 1;
}
while (i < slice.len and slice[i] >= '0' and slice[i] <= '9') : (i += 1) {
result = result * 10 + @as(c_int, slice[i] - '0');
}
return if (negative) -result else result;
}
fn c_isalpha(ch: u8) bool {
return (ch >= 'A' and ch <= 'Z') or (ch >= 'a' and ch <= 'z');
}
fn c_toupper(ch: u8) u8 {
return if (ch >= 'a' and ch <= 'z') ch - 32 else ch;
}
pub fn main() !void {
// abs
std.debug.print("abs(-42) = {}\n", .{c_abs(-42)});
std.debug.print("abs(17) = {}\n", .{c_abs(17)});
// atoi
std.debug.print("\natoi(\"123\") = {}\n", .{c_atoi("123")});
std.debug.print("atoi(\"-456\") = {}\n", .{c_atoi("-456")});
std.debug.print("atoi(\"0\") = {}\n", .{c_atoi("0")});
// Character functions
const test_chars = "Hello, World! 123";
std.debug.print("\nCharacter analysis of \"{s}\":\n", .{test_chars});
var alpha_count: usize = 0;
std.debug.print(" Uppercase: ", .{});
for (test_chars) |ch| {
if (c_isalpha(ch)) alpha_count += 1;
std.debug.print("{c}", .{c_toupper(ch)});
}
std.debug.print("\n Alpha chars: {}\n", .{alpha_count});
// Real @cImport usage:
// const c = @cImport({
// @cInclude("stdio.h");
// @cInclude("stdlib.h");
// @cInclude("string.h");
// });
// const result = c.abs(-42);
// const len = c.strlen("hello");
}
Struct Layout Compatibility
Zig structs can match C struct layout with extern struct.
const std = @import("std");
// extern struct matches C layout exactly
const Point = extern struct {
x: f64,
y: f64,
};
const Color = extern struct {
r: u8,
g: u8,
b: u8,
a: u8,
};
const Rect = extern struct {
origin: Point,
size: Point,
color: Color,
};
fn distance(a: Point, b: Point) f64 {
const dx = b.x - a.x;
const dy = b.y - a.y;
return @sqrt(dx * dx + dy * dy);
}
fn rectArea(r: Rect) f64 {
return r.size.x * r.size.y;
}
pub fn main() !void {
// Struct sizes match C
std.debug.print("Struct sizes:\n", .{});
std.debug.print(" Point: {} bytes\n", .{@sizeOf(Point)});
std.debug.print(" Color: {} bytes\n", .{@sizeOf(Color)});
std.debug.print(" Rect: {} bytes\n", .{@sizeOf(Rect)});
// Use like C structs
const p1 = Point{ .x = 0, .y = 0 };
const p2 = Point{ .x = 3, .y = 4 };
std.debug.print("\nDistance: {d:.2}\n", .{distance(p1, p2)});
const rect = Rect{
.origin = .{ .x = 10, .y = 20 },
.size = .{ .x = 100, .y = 50 },
.color = .{ .r = 255, .g = 128, .b = 0, .a = 255 },
};
std.debug.print("Rect at ({d}, {d}), size {d}x{d}\n", .{
rect.origin.x, rect.origin.y, rect.size.x, rect.size.y,
});
std.debug.print("Area: {d}\n", .{rectArea(rect)});
std.debug.print("Color: rgb({}, {}, {})\n", .{ rect.color.r, rect.color.g, rect.color.b });
// packed struct for bit-level control
const Flags = packed struct {
readable: bool,
writable: bool,
executable: bool,
_padding: u5 = 0,
};
std.debug.print("\nFlags size: {} byte\n", .{@sizeOf(Flags)});
const flags = Flags{ .readable = true, .writable = true, .executable = false };
std.debug.print("r={}, w={}, x={}\n", .{ flags.readable, flags.writable, flags.executable });
}
Try It Yourself
const std = @import("std");
// Build a simple C-compatible string utility library
const CString = struct {
ptr: [*:0]const u8,
fn init(s: [*:0]const u8) CString {
return .{ .ptr = s };
}
fn len(self: CString) usize {
return std.mem.span(self.ptr).len;
}
fn slice(self: CString) []const u8 {
return std.mem.span(self.ptr);
}
fn startsWith(self: CString, prefix: []const u8) bool {
const s = self.slice();
if (prefix.len > s.len) return false;
return std.mem.eql(u8, s[0..prefix.len], prefix);
}
fn endsWith(self: CString, suffix: []const u8) bool {
const s = self.slice();
if (suffix.len > s.len) return false;
return std.mem.eql(u8, s[s.len - suffix.len ..], suffix);
}
fn contains(self: CString, needle: []const u8) bool {
return std.mem.indexOf(u8, self.slice(), needle) != null;
}
fn eql(self: CString, other: CString) bool {
return std.mem.eql(u8, self.slice(), other.slice());
}
};
pub fn main() !void {
const greeting = CString.init("Hello, World!");
const other = CString.init("Hello, World!");
const different = CString.init("Goodbye!");
std.debug.print("String: \"{s}\"\n", .{greeting.slice()});
std.debug.print("Length: {}\n", .{greeting.len()});
std.debug.print("Starts with 'Hello': {}\n", .{greeting.startsWith("Hello")});
std.debug.print("Ends with '!': {}\n", .{greeting.endsWith("!")});
std.debug.print("Contains 'World': {}\n", .{greeting.contains("World")});
std.debug.print("Contains 'Zig': {}\n", .{greeting.contains("Zig")});
std.debug.print("Equals other: {}\n", .{greeting.eql(other)});
std.debug.print("Equals different: {}\n", .{greeting.eql(different)});
}
Key Takeaways
- Zig can call C functions directly with
@cImportand@cInclude— no bindings needed - C types (
c_int,c_long, etc.) have platform-matching sizes [*:0]const u8is Zig's equivalent ofconst char*(null-terminated)- Use
std.mem.span()to convert C strings to Zig slices extern structmatches C struct layout;packed structcontrols bit layoutallocator.dupeZ()creates null-terminated copies for C APIs- Zig can link C libraries in
build.zigwithexe.linkSystemLibrary("libname")
Pro Tip: When wrapping a C library, create a thin Zig wrapper that converts C idioms to Zig idioms: turn error codes into Zig errors, turn null pointers into optionals, and turn C strings into slices. This gives callers a natural Zig API while keeping the C interop contained in one place.
Next Steps
You've now covered the core tools in Zig's toolkit. It's time to put everything together in a hands-on capstone project that combines error handling, memory management, file I/O, and the build system into a real command-line utility.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.