Importing a C header lets Zig understand a C API. Linking gives the final program the actual compiled code.
Importing a C header lets Zig understand a C API. Linking gives the final program the actual compiled code.
These are separate steps.
A header file says what exists:
// mathlib.h
int add(int a, int b);A source file or library provides the implementation:
// mathlib.c
#include "mathlib.h"
int add(int a, int b) {
return a + b;
}In Zig, this import only reads the declaration:
const c = @cImport({
@cInclude("mathlib.h");
});It does not include the body of add. If the final executable cannot find the compiled implementation, the linker fails.
The Build Has Two Jobs
When using C from Zig, your build usually needs to do two things.
First, it must tell Zig where the C headers are:
include path -> where .h files liveSecond, it must tell Zig where the C implementation is:
source file, object file, static library, or dynamic libraryThe header lets the compiler check your call.
The implementation lets the linker build the final executable.
Linking a C Source File
For small libraries, you may compile the C source file directly with your Zig program.
Project layout:
project/
build.zig
src/
main.zig
mathlib.c
mathlib.hC header:
// src/mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H
int add(int a, int b);
#endifC source:
// src/mathlib.c
#include "mathlib.h"
int add(int a, int b) {
return a + b;
}Zig code:
// src/main.zig
const std = @import("std");
const c = @cImport({
@cInclude("mathlib.h");
});
pub fn main() void {
const result = c.add(10, 20);
std.debug.print("result = {}\n", .{result});
}In build.zig, you add the include path and the C source file.
A simplified example:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
exe.addIncludePath(b.path("src"));
exe.addCSourceFile(.{
.file = b.path("src/mathlib.c"),
.flags = &.{},
});
b.installArtifact(exe);
}Now the build knows both parts:
src/mathlib.h -> available to @cImport
src/mathlib.c -> compiled and linked into the executableLinking a Static Library
A static library is usually a file such as:
libmathlib.aOn Windows, it may be:
mathlib.libA static library is copied into the final executable at link time. The result is usually more self-contained.
Project layout:
project/
build.zig
src/
main.zig
include/
mathlib.h
lib/
libmathlib.aZig imports the header:
const c = @cImport({
@cInclude("mathlib.h");
});The build must provide the include directory and the library search path:
exe.addIncludePath(b.path("include"));
exe.addLibraryPath(b.path("lib"));
exe.linkSystemLibrary("mathlib");The library name usually drops the lib prefix and .a suffix.
So this file:
libmathlib.ais linked as:
exe.linkSystemLibrary("mathlib");This naming convention comes from Unix-style linkers.
Linking a Dynamic Library
A dynamic library is loaded at runtime.
Common names:
| Platform | Dynamic library extension |
|---|---|
| Linux | .so |
| macOS | .dylib |
| Windows | .dll |
If you link dynamically, the executable depends on that library being available when the program runs.
For example, if your program links to SQLite dynamically, the compiled executable may still need libsqlite3.so, libsqlite3.dylib, or sqlite3.dll on the target machine.
The Zig build code may look similar:
exe.addIncludePath(b.path("include"));
exe.addLibraryPath(b.path("lib"));
exe.linkSystemLibrary("sqlite3");But the runtime behavior differs.
Static linking copies code into the executable.
Dynamic linking records a dependency on an external library.
Static vs Dynamic Linking
| Linking style | What happens | Main advantage | Main cost |
|---|---|---|---|
| Static | Library code is copied into the executable | Easier deployment | Larger executable |
| Dynamic | Executable loads library at runtime | Smaller executable, shared library updates | Runtime dependency |
For command-line tools, static linking can be convenient.
For desktop applications and system libraries, dynamic linking is common.
For plugins or libraries that must match system versions, dynamic linking may be required.
There is no universal best choice. It depends on deployment, licensing, platform rules, and library behavior.
Linking the C Standard Library
Some C libraries depend on libc.
In Zig build files, you may need:
exe.linkLibC();This tells Zig to link the C standard library.
You usually need this when your Zig executable calls C code that uses libc functions such as malloc, free, printf, fopen, or strlen.
Example:
exe.addCSourceFile(.{
.file = b.path("src/mathlib.c"),
.flags = &.{},
});
exe.linkLibC();For very small C files that do not use libc, this may not be necessary. But many real C libraries need it.
Header Found, Link Failed
A very common situation is:
The header was found.
The code compiled.
The linker failed.That usually means Zig knows the function declaration, but it cannot find the implementation.
Example error shape:
undefined symbol: addor:
undefined reference to `add`This means the final link step could not find compiled code for add.
The fix is usually one of these:
Add the C source file.
Add the object file.
Add the static library.
Add the dynamic library.
Add the correct library search path.
Use the correct library name.Do not try to fix this by changing @cImport. The import already did its job. The problem is linking.
Header Not Found
Another common error is:
'mathlib.h' file not foundThat is different.
It means the compiler cannot find the header file during @cImport.
The fix is to add the include path:
exe.addIncludePath(b.path("include"));Think of the two errors separately.
| Error | Meaning | Fix |
|---|---|---|
| Header not found | Compiler cannot find .h file | Add include path |
| Undefined symbol | Linker cannot find implementation | Link source file or library |
This distinction saves a lot of time.
Linking System Libraries
Some libraries are installed on the system.
For example, on many Unix-like systems, you may link math functions from libm.
In C, this often looks like:
cc main.c -lmIn Zig build code, it may look like:
exe.linkSystemLibrary("m");For pthreads:
exe.linkSystemLibrary("pthread");For SQLite:
exe.linkSystemLibrary("sqlite3");This asks the system linker to find a library with that name.
This only works if the library is installed and visible to the linker.
Library Search Paths
If a library is not in a standard system location, you need to tell the build where to look.
exe.addLibraryPath(b.path("vendor/sqlite/lib"));
exe.linkSystemLibrary("sqlite3");For headers:
exe.addIncludePath(b.path("vendor/sqlite/include"));These two paths solve different problems.
include path -> compile-time headers
library path -> link-time library filesA project that vendors a C dependency usually needs both.
Adding C Compiler Flags
Some C files require compile flags.
For example:
exe.addCSourceFile(.{
.file = b.path("src/library.c"),
.flags = &.{
"-std=c11",
"-Wall",
},
});Flags are passed to the C compiler when compiling that C file.
You may need flags for:
C standard version
warning settings
feature macros
include behavior
optimization options
platform-specific configurationKeep these flags close to the C source file in the build file. That makes the build easier to inspect.
Defining C Macros from the Build
Some C libraries use macros for configuration.
You can define macros inside @cImport with @cDefine, but you may also need to define them when compiling the C source.
For example:
exe.addCSourceFile(.{
.file = b.path("src/library.c"),
.flags = &.{
"-DMY_FEATURE=1",
},
});This affects the C source compilation.
If the header also depends on the macro, define it in the import too:
const c = @cImport({
@cDefine("MY_FEATURE", "1");
@cInclude("library.h");
});The C source and the Zig import should agree on configuration. If they use different macros, you can get mismatched declarations and implementations.
Object Files
Sometimes you already have a compiled object file:
mathlib.oAn object file is compiled code that has not yet been linked into the final executable.
You can link object files into your program.
Conceptually:
exe.addObjectFile(b.path("lib/mathlib.o"));Use this when a dependency ships object files instead of source files or static libraries.
For beginners, C source files and static libraries are more common.
Platform Differences
Linking is platform-sensitive.
Linux, macOS, and Windows use different library formats, default paths, system libraries, and runtime lookup rules.
Examples:
| Platform | Static library | Dynamic library | Common linker behavior |
|---|---|---|---|
| Linux | .a | .so | Uses system library paths and rpath rules |
| macOS | .a | .dylib | Uses frameworks and install names |
| Windows | .lib | .dll | Uses import libraries and DLL search paths |
This is why Zig’s build system is useful. You can express platform-specific rules in Zig code instead of maintaining many separate shell scripts.
Example shape:
if (target.result.os.tag == .windows) {
exe.linkSystemLibrary("ws2_32");
} else {
exe.linkSystemLibrary("m");
}The exact library names depend on the API you use.
C++ Is Different
This chapter is about C, not C++.
C++ linking is more complicated because of name mangling, constructors, destructors, templates, exceptions, RTTI, and the C++ standard library.
Zig can interact with C++ in some cases, but C interop is much simpler and more direct.
When possible, expose a C API from C++ code:
extern "C" int add(int a, int b);Then call that C-compatible API from Zig.
This keeps the boundary stable.
A Clean Mental Model
When linking C libraries, keep four things separate:
Header files
C source files
Library files
Runtime library loadingHeader files are for compilation.
C source files are compiled into object code.
Library files provide object code to the linker.
Dynamic libraries may also be needed at runtime.
If you keep these layers separate, most errors become easier to diagnose.
What to Remember
@cImport imports declarations.
Linking provides implementations.
Use addIncludePath for headers.
Use addCSourceFile for C source files.
Use addLibraryPath and linkSystemLibrary for libraries.
Use linkLibC when your C code depends on libc.
A “header not found” error is an include-path problem.
An “undefined symbol” error is a linking problem.
Static libraries make deployment simpler but produce larger binaries.
Dynamic libraries keep binaries smaller but add runtime dependencies.