Skip to main content

C Interoperability

Zig provides first-class C interoperability, allowing seamless integration with existing C code and libraries.

Overview

Zig can:
  • Call C functions directly
  • Use C types and structs
  • Link against C libraries
  • Import C header files
  • Act as a C compiler (zig cc)
  • Translate C code to Zig

Using C Functions

Linking libc

--libc [file]    # Specify libc paths file
-lc              # Link against system libc
zig build-exe -lc main.zig

Importing C Headers

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

pub fn main() void {
    _ = c.printf("Hello from C!\n");
    const result = c.sqrt(16.0);
    _ = c.printf("sqrt(16) = %f\n", result);
}

With Defines and Include Paths

const c = @cImport({
    @cDefine("_GNU_SOURCE", {});
    @cDefine("DEBUG", "1");
    @cInclude("features.h");
});

C Types in Zig

Basic C Types

C TypeZig TypeDescription
intc_intC integer
unsigned intc_uintC unsigned integer
longc_longC long
unsigned longc_ulongC unsigned long
long longc_longlongC long long
shortc_shortC short
charc_charC char
floatf3232-bit float
doublef6464-bit float
void**anyopaqueOpaque pointer

Type Example

const std = @import("std");

extern fn add(a: c_int, b: c_int) c_int;

pub fn main() void {
    const x: c_int = 10;
    const y: c_int = 20;
    const result = add(x, y);
    std.debug.print("{d}\n", .{result});
}

Calling C Functions

Extern Functions

#include <math.h>

double calculate_distance(double x1, double y1, double x2, double y2) {
    double dx = x2 - x1;
    double dy = y2 - y1;
    return sqrt(dx * dx + dy * dy);
}

Variadic Functions

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

pub fn main() void {
    const name = "Alice";
    const age: c_int = 30;
    _ = c.printf("Name: %s, Age: %d\n", name.ptr, age);
}

C Structs

Using C Structs

typedef struct {
    double x;
    double y;
} Point;

Point make_point(double x, double y);
double point_distance(Point p1, Point p2);

Linking C Libraries

System Libraries

-l[lib]              # Link library
-L[dir]              # Add library search path
-I[dir]              # Add include search path
zig build-exe -lc -lm main.zig

Using build.zig

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

exe.linkLibC();
exe.linkSystemLibrary("ssl");
exe.linkSystemLibrary("crypto");

Compiling C Code

C Source Files

zig build-exe main.zig helper.c utils.c -lc

With Flags

-cflags [flags] --    # Set C compiler flags
zig build-exe main.zig file.c -lc -cflags -O2 -Wall -Wextra --

In build.zig

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

exe.linkLibC();

exe.addCSourceFile(.{
    .file = .{ .path = "src/helper.c" },
    .flags = &.{
        "-Wall",
        "-Wextra",
        "-O2",
        "-DDEBUG",
    },
});

Translating C to Zig

zig translate-c

Convert C code to Zig:
zig translate-c input.h > output.zig

Example Translation

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

typedef struct {
    int x;
    int y;
} Point;

Point make_point(int x, int y);

#endif
The generated Zig code may need manual cleanup for idiomatic Zig patterns.

Zig as a C Compiler

zig cc

Use Zig as a drop-in C compiler:
zig cc -o program main.c helper.c -lm

zig c++

Use Zig as a C++ compiler:
zig c++ -o program main.cpp -lstdc++

Benefits

Cross-Compilation

Easily cross-compile C/C++ code without separate toolchains

Bundled Toolchain

No need to install separate C compiler or libc

Target Support

Support for many targets out of the box

Consistent

Same compiler across all platforms

C String Handling

Null-Terminated Strings

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

pub fn main() void {
    const zig_string = "Hello, World!";
    const c_string: [*:0]const u8 = zig_string.ptr;
    _ = c.puts(c_string);
}

String Literals

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

pub fn main() void {
    const str1 = "Hello";
    const str2 = "World";
    
    const result = c.strcmp(str1.ptr, str2.ptr);
    std.debug.print("strcmp result: {d}\n", .{result});
}

Memory Management

Using C Allocators

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

pub fn main() void {
    const ptr = c.malloc(1024);
    defer c.free(ptr);
    
    // Use allocated memory
    const slice: [*]u8 = @ptrCast(ptr);
    slice[0] = 42;
}

Error Handling

C errno

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

pub fn main() void {
    const file = c.fopen("nonexistent.txt", "r");
    if (file == null) {
        const err = c.errno;
        std.debug.print("Error: {d}\n", .{err});
        return;
    }
    defer _ = c.fclose(file);
}

Wrapping C Errors

const std = @import("std");

const FileError = error{
    NotFound,
    PermissionDenied,
    Unknown,
};

extern fn c_open_file(path: [*:0]const u8) ?*anyopaque;

fn openFile(path: []const u8) FileError!*anyopaque {
    const c_path = path.ptr;
    return c_open_file(c_path) orelse return FileError.NotFound;
}

Callbacks

Function Pointers

typedef void (*callback_t)(int value);
void register_callback(callback_t cb);
void trigger_callback();
Callbacks must use .C calling convention: callconv(.C)

Complete Example

#ifndef MYLIB_H
#define MYLIB_H

typedef struct {
    int width;
    int height;
} Rectangle;

Rectangle make_rectangle(int w, int h);
int rectangle_area(Rectangle rect);
void print_rectangle(Rectangle rect);

#endif

Common Patterns

Opaque Pointers

const FileHandle = opaque {};

extern fn open_file(path: [*:0]const u8) ?*FileHandle;
extern fn close_file(handle: *FileHandle) void;
extern fn read_file(handle: *FileHandle, buffer: [*]u8, size: usize) isize;

pub fn main() !void {
    const handle = open_file("file.txt") orelse return error.FileNotFound;
    defer close_file(handle);
    
    var buffer: [1024]u8 = undefined;
    const bytes_read = read_file(handle, &buffer, buffer.len);
    // ...
}

Bitfields

const Flags = packed struct {
    read: bool,
    write: bool,
    execute: bool,
    _padding: u29 = 0,
};

extern fn set_permissions(flags: Flags) void;

Best Practices

  1. Use @cImport - Let Zig translate C headers automatically
  2. Link libc explicitly - Always add -lc when using C code
  3. Check C function returns - C functions don’t use Zig’s error model
  4. Use correct calling convention - Callbacks need callconv(.C)
  5. Handle null pointers - C pointers can be null
  6. Mind string encoding - C uses null-terminated strings
  7. Test thoroughly - C interop can have subtle issues
  8. Prefer Zig implementations - When possible, use native Zig code

Next Steps

CLI Reference

Complete CLI flag reference

Build System

Learn about build.zig

Build docs developers (and LLMs) love