Zig C Interop

Zig can call C functions and use C data structures directly, without a foreign function interface layer or code generators. Zig also compiles C source files as part of its own build. This makes Zig an ideal drop-in replacement for C in existing projects and lets you use the enormous ecosystem of C libraries from Zig code.

How Zig Sees C

  C Library (.h header + .a or .so binary)
         │
  @cImport → translates C types and declarations into Zig types
         │
  Your Zig code calls C functions as if they were Zig functions
         │
  Zig compiles and links everything together

Importing C Headers with @cImport

const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("string.h");
    @cInclude("math.h");
});

pub fn main() void {
    _ = c.printf("Hello from C's printf!\n");

    const len = c.strlen("Zig rocks");
    _ = c.printf("Length: %zu\n", len);

    const result = c.sqrt(144.0);
    _ = c.printf("sqrt(144) = %.1f\n", result);
}
  @cImport block:
  ┌───────────────────────────────────┐
  │ @cInclude("stdio.h")              │ → c.printf, c.scanf, c.fopen ...
  │ @cInclude("string.h")             │ → c.strlen, c.strcpy, c.memcpy ...
  │ @cInclude("math.h")               │ → c.sqrt, c.sin, c.pow ...
  └───────────────────────────────────┘
  Zig translates C declarations into Zig-compatible types.

C Types in Zig

  C type          │ Zig equivalent
  ────────────────┼──────────────────────────
  int             │ c_int
  long            │ c_long
  unsigned int    │ c_uint
  char            │ u8
  char*           │ [*:0]u8  (null-terminated)
  void*           │ *anyopaque
  size_t          │ usize
  NULL            │ null (for optional pointers)
  float           │ f32
  double          │ f64

Calling C with Strings

C strings are null-terminated byte arrays (char*). Zig string literals are []const u8 slices. Converting between them requires the sentinel-terminated type:

const c = @cImport(@cInclude("string.h"));

pub fn main() void {
    // Zig string literal already null-terminated
    const zig_str: [*:0]const u8 = "Hello from Zig";

    const length = c.strlen(zig_str);
    _ = c.printf("Length: %zu\n", length);
}

For dynamic strings, use std.fmt.bufPrintZ or std.fmt.allocPrintZ to produce a null-terminated Zig string suitable for passing to C.

Compiling C Source Files in Zig Build

Add C source files directly to your Zig executable in build.zig:

const exe = b.addExecutable(.{
    .name             = "mixed",
    .root_source_file = b.path("src/main.zig"),
    .target           = target,
    .optimize         = optimize,
});

exe.addCSourceFile(.{
    .file  = b.path("src/mylib.c"),
    .flags = &.{"-std=c11", "-Wall"},
});
exe.linkLibC();  // link the C standard library

b.installArtifact(exe);
  Project:
  src/
  ├── main.zig    ← Zig entry point, calls C functions
  └── mylib.c     ← C implementation file
  └── mylib.h     ← C header

  build.zig links them into one binary.
  No separate C compilation step needed.

Exposing Zig Functions to C

You can write Zig functions and call them from C code. Mark them with export and use the C calling convention:

// In Zig (exported for C):
export fn zig_add(a: c_int, b: c_int) c_int {
    return a + b;
}

export fn zig_greet(name: [*:0]const u8) void {
    std.debug.print("Hello from Zig, {s}!\n", .{name});
}
  // In C (calls Zig):
  extern int zig_add(int a, int b);
  extern void zig_greet(const char* name);

  int main() {
      printf("%d\n", zig_add(3, 4));   // 7
      zig_greet("World");
  }

Using a C Library — Example with libc

const std = @import("std");
const c   = @cImport({
    @cInclude("stdlib.h");
    @cInclude("time.h");
});

pub fn main() void {
    // Seed C random number generator
    c.srand(@intCast(c.time(null)));

    // Get 5 random numbers between 1 and 100
    for (0..5) |_| {
        const n = @rem(c.rand(), 100) + 1;
        std.debug.print("{d}\n", .{n});
    }
}

Translating C Code to Zig

Zig includes a tool to translate C headers and source files into Zig code automatically:

zig translate-c myfile.c
  myfile.c (C code)
       │
  zig translate-c
       │
  myfile.zig (Zig translation)
  ↑ Not always clean, but gives a starting point
    for replacing C with Zig incrementally.

Handling C Pointers Safely

  C returns a pointer that might be NULL:
  const ptr = c.malloc(100);  // returns ?*anyopaque

  Zig forces you to check for null:
  if (ptr) |p| {
      // p is *anyopaque, safely non-null
      defer c.free(p);
      // use p...
  } else {
      return error.OutOfMemory;
  }

C functions that return pointers return optional pointers in Zig (?*T). The Zig type system automatically wraps C null returns as optional, forcing you to handle the null case before using the pointer.

Link System Libraries

// In build.zig — link system libraries:
exe.linkSystemLibrary("curl");    // libcurl
exe.linkSystemLibrary("sqlite3"); // SQLite
exe.linkSystemLibrary("z");       // zlib
exe.linkLibC();                   // always needed for libc
  // In main.zig:
  const curl = @cImport(@cInclude("curl/curl.h"));
  // Now use curl.curl_easy_init(), curl.curl_easy_setopt(), etc.

Why Zig C Interop is Different

  Language   | C Interop method
  ───────────┼──────────────────────────────
  Python     | ctypes or cffi (runtime)
  Go         | cgo (separate compiler pass)
  Rust       | bindgen + unsafe blocks
  Java       | JNI (complex wrapper layer)
  Zig        | @cImport (compile-time, zero overhead, built-in)

Zig's C interop has no runtime overhead and requires no extra tools. The C headers are parsed at compile time, types are checked at compile time, and the resulting binary is exactly as efficient as if you had written pure C. This is why Zig is a practical choice for augmenting or gradually replacing large C codebases.

Leave a Comment

Your email address will not be published. Required fields are marked *