Build.zig for static library in C/C++

I am creating a new small SDK from scratch in C/C++ and want to use zig as the build system (0.12.0), but this newbie brain doesn’t understand the zig build system, and the Ziglang Build System docs are useless for my scenario. I’ve looked at the “maintain it in zig” blogs and such, but they’re obsolete now. I haven’t found simple examples for how to do this in 0.12.0, only .11.

Apparently there are “modules” now which are entirely undocumented, and the term is not even defined.

structure:

src/em-lib/** // C/C++ library source and headers
src/main.zig // etc. – otherwise a normal zig project

The main.zig is an exe that should link with the static library. I know the zig could just add the C/C++ files directly, but since I’m building an SDK, it should consume a built static .lib, because that’s my work product.

So what should my build.zig look like (precisely, with example code)?

I think where I’m having the most trouble is knowing what build system object I need to create and add source files to, and the working syntax for that, since the intuitive “hand it a path” from .11 is gone.

I’ll also need to export an artifact with the header and built lib, but I think that’s straightforward once I have the basic entities building and the exe running.

Yep, documentation is still a little light, but I think I got you! To follow along, run zig init in a blank directory, and then add the following lines to your build.zig:


// Creating an all C/C++ "em-lib" Static Library
const em_lib = b.addStaticLibrary(.{
    .name = "emlib",
    .target = target,
    .optimize = optimize,
});
em_lib.addCSourceFiles(.{ .files = &.{"em-lib/emlib.c"} });
em_lib.addIncludePath(b.path("em-lib"));
em_lib.linkLibC();
em_lib.linkLibCpp();
em_lib.installHeader(b.path("em-lib/emlib.h"), "em-lib/emlib.h");
b.installArtifact(em_lib);

// Linking in "emlib" to our main executable + including the header path
exe.linkLibrary(em_lib);

I created a folder em-lib which has:

emlib.c:

#include "emlib.h"

int doSomething(int a)
{
    return a + 5;
}

emlib.h:

#pragma once

int doSomething(int a);

And modified main.zig to look like:

const std = @import("std");
const emlib = @cImport({
    @cInclude("em-lib/emlib.h");
});

pub fn main() !void {
    // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
    std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
    
    // Testing "emlib"!
    std.debug.print("Calling a C function: {d}\n", .{emlib.doSomething(10)});

    // stdout is for the actual output of your application, for example if you
    // are implementing gzip, then only the compressed bytes should be sent to
    // stdout, not any debugging messages.
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try stdout.print("Run `zig build test` to run the tests.\n", .{});

    try bw.flush(); // don't forget to flush!
}

test "simple test" {
    var list = std.ArrayList(i32).init(std.testing.allocator);
    defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
    try list.append(42);
    try std.testing.expectEqual(@as(i32, 42), list.pop());
}

When I run this I now get:

All your codebase are belong to us.
Calling a C function: 15
Run zig build test to run the tests.

Does this hopefully give you enough to go on/get started? Also check out this post if you haven’t yet:

1 Like

Ok, figured it out.

    const em_lib = b.addStaticLibrary(.{
        .name = "em-lib",
        .target = target,
        .optimize = optimize,
        .version = .{ .major = 0, .minor = 1, .patch = 0 },
        .link_libc = true,
    });
    em_lib.addIncludePath(b.path("src"));
    em_lib.addCSourceFiles(.{
        .root = b.path("src/em-lib"),
        .files = &.{
            "midi-message.cpp",
            "midi-file.cpp",
        },
    });
...
    exe.linkLibrary(em_lib);

Next is what’s required is to make a folder under zig-out for the library, and get the headers and the lib copied there. Bonus to make it a zip.

At the moment clueless how to create a directory structure in the install location and get the files there.

you probably want b.installArtifact

Just edited my original answer, in terms of a “header” directory structure for your library you can use:

em_lib.installHeader(b.path("em-lib/emlib.h"), "em-lib/emlib.h");

This basically just tells Zig’s build system to copy the emlib.h header over to zig-out/include/em-lib/emlib.h. I modified main.zig to now include it via:

const emlib = @cImport({
    @cInclude("em-lib/emlib.h");
});

I got rid of the line that manually adds the include directory to exe as this is no longer needed (and wasn’t exactly the “right” way to do it). The directory you copied your header file over to automatically gets added as an include directory when you link against em_lib.

Generally speaking, that’s the only folder structure I would personally care about when creating a C library (so users import it in C code with the qualified name, eg. <somelibrary/lib_header.h>. If you want even more control over where the actual .a file is getting put in zig-out look into:
b.addInstallArtifact()

If you look at the source code for b.installArtifact() it’s just a convenience function that wraps b.addInstallArtifact() with some defaults. IMO the .a file ending up in zig-out/lib/libemlib.a doesn’t seem important since exe.linkLibrary(em_lib) knows where to find it automatically.

1 Like

Seems like this is working. I hadn’t intended there to be an include directory in the output, but that’s ok.

    const em_lib = b.addStaticLibrary(.{
        .name = "em-lib",
        .target = target,
        .optimize = optimize,
        .version = .{ .major = 0, .minor = 1, .patch = 0 },
        .link_libc = true,
    });
    em_lib.addIncludePath(b.path("src"));
    // install the header
    em_lib.installHeader(b.path("src/em-lib/em-lib.h"), "em-lib/emlib.h");
    em_lib.addCSourceFiles(.{
        .root = b.path("src"),
        .files = &.{
            "em-lib/midi-message.cpp",
            "em-lib/midi-file.cpp",
        },
    });
    // install the lib
    b.installArtifact(em_lib); 

    const exe = b.addExecutable(.{
        .name = "em-tool",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.addIncludePath(b.path("src"));
    exe.linkLibrary(em_lib);

I get this folder structure in the result.

zig-out\include\em-lib\em-lib.h
zig-out\lib\em-lib.lib

This is not something I declared, and I’ll need to dig around to see where (or if) this can be changed. For the moment, it’s fine, and I may rearrange the source to match.

Thank you Andrew, and haydenridd for the help!

1 Like