Skip to content

Linking Libraries

Calling a C function is only half the job. The linker must also find the code for that function.

Calling a C function is only half the job. The linker must also find the code for that function.

A C declaration tells Zig what exists:

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

But the declaration does not contain the function body. The body must come from a C source file, an object file, a static library, a shared library, or the system C library.

The smallest case is a C source file.

int add(int a, int b) {
    return a + b;
}

Save it as add.c.

The Zig program:

const std = @import("std");

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

pub fn main() void {
    std.debug.print("{d}\n", .{add(3, 4)});
}

Build both files together:

zig build-exe main.zig add.c

Run the result:

./main

The output is:

7

Zig compiles the C file and links it into the executable.

A C file may also be compiled first:

zig cc -c add.c -o add.o

Then the object file can be linked:

zig build-exe main.zig add.o

This is useful when object files come from another build system.

A static library is an archive of object files. On Unix-like systems, its name often looks like this:

libmathlib.a

If the file is in the current directory, link it directly:

zig build-exe main.zig libmathlib.a

Or use a library search path and library name:

zig build-exe main.zig -L. -lmathlib

-L. adds the current directory to the library search path.

-lmathlib asks the linker to find a library named mathlib. On Unix-like systems, this usually means a file named libmathlib.a or libmathlib.so.

A shared library is loaded by the operating system at run time. On Unix-like systems it often has this form:

libmathlib.so

On macOS:

libmathlib.dylib

On Windows:

mathlib.dll

The build command is similar:

zig build-exe main.zig -L. -lmathlib

At run time, the operating system must also be able to find the shared library. That is separate from compiling and linking.

For functions from the C standard library, link libc:

zig build-exe main.zig -lc

For example:

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

pub fn main() void {
    _ = c.puts("hello");
}

Build it:

zig build-exe main.zig -lc

Without -lc, the declarations may still be known, but the final executable may fail to link because the implementation is missing.

When using build.zig, linking is written as part of the build graph.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.addCSourceFile(.{
        .file = b.path("add.c"),
        .flags = &.{},
    });

    b.installArtifact(exe);
}

Now build with:

zig build

To link libc in build.zig:

exe.linkLibC();

To link a system library:

exe.linkSystemLibrary("m");

To add an include path:

exe.addIncludePath(b.path("include"));

To add a library path:

exe.addLibraryPath(b.path("lib"));

Linking is target-dependent. The same Zig source may need different libraries or different names on Linux, macOS, Windows, or a freestanding target.

Good Zig code keeps this difference in the build file, not scattered through the program.

The source file should say what functions it calls. The build file should say where those functions come from.