C interoperability in Zig is the ability to translate .h headers, link libraries, and call their functions. This allows reusing projects like SDL, Raylib, or SQLite without manually maintaining all their declarations.
Zig includes tools to compile C and translate its declarations into compatible types. The call preserves the C ABI, although the translation does not automatically convert the API into an idiomatic or safe Zig interface.
Translating Headers from build.zig
Since Zig 0.16, the recommended path is to create an addTranslateC step in build.zig. First, we group the includes into a single header.
// src/c.h
#include "my_library.h"Then we translate that header and expose the result as module c:
// build.zig
const translate_c = b.addTranslateC(.{
.root_source_file = b.path("src/c.h"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{.
.name = "c",
.module = translate_c.createModule(),
}},
}),
});
b.installArtifact(exe);The Zig code imports the module by the configured name:
const c = @import("c");
pub fn main() void {
c.my_function();
}@cImport still works in Zig 0.16, but it is deprecated and scheduled for removal. For new code, it is better to use the translation step of the build system.
Type Translation
One of the things you will see when using @cImport is that the types are renamed to reflect that they come from C. Zig tries to maintain exact binary compatibility.
| C Type | Zig Type (Imported) | Notes |
|---|---|---|
int | c_int | Usually i32, but depends on the platform. |
unsigned int | c_uint | Same as above. |
char | c_char | Its signedness depends on the target. |
void* | ?*anyopaque | Optional opaque pointer. |
struct Point | Generated identifier | The specific name depends on how the structure is declared. |
#define MAX 10 | c.MAX | Simple macros become constants. |
The C Pointer Type ([*c]T)
C is famous for its ambiguity with pointers. An int* in C can be:
- A pointer to a single integer.
- A pointer to a list of integers.
- It could be
NULL.
Since Zig cannot know the original intent just by reading the .h, it uses a special transitional type: [*c]T (C Pointer).
This compatibility pointer:
- Acts as a many-item pointer (
[*]). - Can represent the zero address and be converted to optional pointers.
- Supports pointer arithmetic.
When working with [*c]T, Zig does not know the length or the intent of the API. It is your responsibility to validate the address, length, lifetime, and active field as defined by the C contract.
Linking in build.zig
Translating the .h provides declarations, but you still need to link the binary library (.lib, .dll, .so, .a) or compile its .c files.
This is done in your build.zig.
Linking libc
Almost any C import requires libc.
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.link_libc = true,
}),
});Linking System Libraries
If you have the library installed on your system (e.g., with apt install libsdl2-dev or brew install sdl2).
translate_c.linkSystemLibrary("SDL2", .{});Compiling Local C Code
If you have .c files in your project and want to compile them alongside your Zig:
exe.root_module.addCSourceFile(.{
.file = b.path("src/my_library.c"),
.flags = &[_][]const u8{"-std=c99", "-O3"},
});
// And tell it where the .h files are
exe.root_module.addIncludePath(b.path("src/include"));Example with Raylib
Imagine using Raylib, a C game programming library, directly from Zig.
src/c.h:
#include <raylib.h>build.zig:
const translate_c = b.addTranslateC(.{
.root_source_file = b.path("src/c.h"),
.target = target,
.optimize = optimize,
});
translate_c.linkSystemLibrary("raylib", .{});
// Add translate_c.createModule() to the root_module with the name "c",
// as in the previous example.main.zig:
const c = @import("c");
pub fn main() void {
const width = 800;
const height = 450;
c.InitWindow(width, height, "Zig + Raylib");
defer c.CloseWindow(); // Using defer for Zig-style cleanup!
c.SetTargetFPS(60);
while (!c.WindowShouldClose()) {
c.BeginDrawing();
defer c.EndDrawing();
c.ClearBackground(c.RAYWHITE);
c.DrawText("Hello Zig!", 190, 200, 20, c.LIGHTGRAY);
}
}We can apply defer around C functions like CloseWindow and EndDrawing. This improves the structure of the Zig code but does not add checks to the original API.
Preprocessor Definitions
Sometimes a header changes based on the #define directives active before the include. With the Zig 0.16 flow, we can place them in the header we pass to addTranslateC.
// src/c.h
#define _GNU_SOURCE
#include <stdio.h>