Skip to content

Instantly share code, notes, and snippets.

@rguiscard
Created October 22, 2024 08:05
Show Gist options
  • Save rguiscard/7d03bfb1def1ee582ca7a131c4114f9f to your computer and use it in GitHub Desktop.
Save rguiscard/7d03bfb1def1ee582ca7a131c4114f9f to your computer and use it in GitHub Desktop.
Make C library as zig package

It is quite often to include C library in Zig project. If it is a shared library, just link it in build.zig. But if it is not, it can be included as a dependency. Here I use tiny-regex-c as an example.

First, add the c libaray as a dependency.

$ zig fetch --save=tiny_regex_c https://github.com/shahar99s/tiny-regex-c/archive/refs/heads/master.zip

It will show up in the build.zig.zon file. Then include it in build.zig

    ...
    const tiny_regex_c = b.dependency("tiny_regex_c", .{
        .target = target,
        .optimize = optimize,
    });

    exe.addCSourceFiles(.{
        .root = tiny_regex_c.path(""),
        .files = &.{"re.c"},
    });

    exe.addIncludePath(tiny_regex_c.path(""));

    exe.linkLibC();
    ...

And use it in zig program:

const std = @import("std");
const tiny_regex = @cImport({
   @cInclude("re.h");
});

pub fn main() !void {
    var match_len:c_int = 0;
    const re_t = tiny_regex.re_compile("[a-zA-Z]*");
    if (re_t) |regex| {
        const result = tiny_regex.re_matchp(regex, "ABCD0123", &match_len);
        std.debug.print("result {d}, {d}\n", .{result, match_len});
    }
}

This is a very standard way to import C library.

What if I want to pack this C library as a zig package independent from the main program ? Some C libraries are wrapped in Zig as modules. But since Zig supports C quite well, Zig wrap may not be necessary for some simple C libraries. In such case, I can pack C library as a static library and included it in the main program.

$ mkdir tiny-regex-c
$ cd tiny-regex-c
$ zig init
$ rm src/*.zig # I am not making zig library or executable here
$ zig fetch --save=tiny_regex_c https://github.com/shahar99s/tiny-regex-c/archive/refs/heads/master.zip

Now add build.zig for C library like this

    ....
    const lib = b.addStaticLibrary(.{
        .name = "tiny_regex_lib",
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        // .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });

    const tiny_regex_c = b.dependency("tiny_regex_c", .{
        .target = target,
        .optimize = optimize,                                                                                                      });

    lib.addCSourceFiles(.{
        .root = tiny_regex_c.path(""),
        .files = &.{"re.c"},
    });

    lib.addIncludePath(tiny_regex_c.path(""));

    lib.installHeadersDirectory(tiny_regex_c.path(""), "", .{
        .include_extensions = &.{"re.h"},
    });

    lib.linkLibC();
    
    // This declares intent for the library to be installed into the standard
    // location when the user invokes the "install" step (the default step when
    // running `zig build`).
    b.installArtifact(lib);
    ...

After zig build, there will be re.h and libtiny_regex_lib.a under zig-out directory.

Go back to the main program, fetch this new package like this:

$ zig fetch --save=tiny-regex ../tiny-regex-c

Now the build.zig in the main program looks like this:

    ....
    const tiny_regex = b.dependency("tiny-regex", .{
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    
    exe.linkLibrary(tiny_regex.artifact("tiny_regex_lib"));
    ....

A good thing about this approach is that we can add unit test for this C library. We can test the library independently from the main program to avoid some issues. Now go back to the library. Add file src/test.zig:

const std = @import("std");
const tiny_regex = @cImport({
   @cInclude("re.h");
});

test "regex" {
    var re_t: ?*tiny_regex.regex_t = undefined;
    var match_len:c_int = 0;
    re_t = tiny_regex.re_compile("[a-zA-Z]*");
    if (re_t) |regex| {
        const result = tiny_regex.re_matchp(regex, "ABCD0123", &match_len);
        std.debug.print("result {d}, {d}\n", .{result, match_len});
        try std.testing.expectEqual(result, @as(c_int, 0));
        try std.testing.expectEqual(match_len, @as(c_int, 4));
    }
}

test "true" {
    try std.testing.expect(41 == 41);
}

test {
  std.testing.refAllDecls(@This());
}

It is quite simiar to the main program above. Add this as unit test in build.zig

    ....
    // Creates a step for unit testing. This only builds the test executable
    // but does not run it.
    const unit_tests = b.addTest(.{
        .root_source_file = b.path("src/test.zig"),
        .target = target,
        .optimize = optimize,
    });

    unit_tests.linkLibrary(lib);

    const run_unit_tests = b.addRunArtifact(unit_tests);

    // Similar to creating the run step earlier, this exposes a `test` step to
    // the `zig build --help` menu, providing a way for the user to request
    // running the unit tests.
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_unit_tests.step);

Now run the unit test:

$ zig build test

This may be an easier way to maintain some external libaries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment