How do you make a package module that links against C libraries?

I’m writing my first Zig package, which I want to be usable as a package manager dependency for other Zig projects (including my own). From what I can tell, to be importable as a package, build.zig has to call b.addModule() to define the package name and root file. That part makes sense.

But I have two other requirements that are getting me confused:

  1. My package depends on C files, and links against external libraries.
  2. I want my package to include tests and example exe’s, which need to link against the package as any other project would.

For the first point, I can use b.addStaticLibrary() to be able to use the external includes and libraries. This builds fine. However, a library isn’t a module, as far as the package manager is concerned, right?

Is it possible to create a module which itself builds against external files, as a library would? Or do I have to split it into pieces, build the library, then include the library into the module? b.addModule() doesn’t appear to have a way to add libraries, so I’m a little lost there.

If I can get this working as a module, then I’m assuming I’ll be able to add the module to any tests and exe’s like a regular dependency. At the moment, using just the library, I have to include the library directly and manually include all external libraries and build requirements, which I’d really like to avoid.

Hey I recently encountered a similar problem when trying to learn how to make module, I don’t think that I’ve understood everything, and my solution might not be exactly what you are looking for, but my module is C files that needs to be linked to libraries so in any case I’m going to give you the link and you can look it up and see if that helps you.

zig project with a C module

The module is in the folder minilibx. and it’s called within the top level build.zig, I didn’t went beyond that but hopefully the syntax can maybe help you ?

1 Like

This may be helpful:

1 Like

Extremely helpful! I didn’t think all of those functions could be called off of a module rather than a library, so that is very illuminating, I’ll have to try that. Thanks!

1 Like

No problem I’d also suggest you to make extern function definitions yourself, for the library you are trying to use, it helps a lot for the LSP, and also avoid the use of the C translated types, I had a previous issue with this. Issue about interfacing C type with Zig type

1 Like

Interesting. Andrew appears to have taken the opposite path, and created both a static library and a module, and then linked the library into the module.

I remember trying that originally, and ran into issues where the final exe needed all of the same linker parameters that the library did, which is what I’m trying to avoid. I may just have to try it again. Thanks!

I’m still running into the issue of tests and executables needing to include the C headers and libraries that the library was built with. Does the library need to be built from a separate build.zig file to make this work right?

Here is my build.zig file I’m playing with to try to get this working:

const std = @import("std");

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

    const module = b.addModule("platform", .{
        .root_source_file = b.path("src/lib.zig"),
    });

    // Library
    const lib = b.addStaticLibrary(.{
        .name = "platform",
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });
    lib.addIncludePath(b.path("src/egl"));
    lib.addIncludePath(b.path("src/wayland"));
    lib.addIncludePath(b.path("src/x11"));
    lib.addCSourceFiles(.{
        .files = &protocol_sources,
    });
    lib.linkLibC();
    lib.linkSystemLibrary("wayland-client");
    lib.linkSystemLibrary("wayland-egl");
    lib.linkSystemLibrary("EGL");
    lib.linkSystemLibrary("xkbcommon");

    if (b.lazyDependency("zgl", .{
        .target = target,
        .optimize = optimize,
    })) |dep| {
        lib.root_module.addImport("zgl", dep.module("zgl"));
    }

    b.installArtifact(lib);

    // Tests
    {
        const test_lib = b.addTest(.{
            .root_source_file = b.path("src/lib.zig"),
            .target = target,
            .optimize = optimize,
        });
        test_lib.root_module.addImport("platform", module);

        const test_step = b.step("test", "Run unit tests");
        const test_step_run = b.addRunArtifact(test_lib);
        test_step.dependOn(&test_step_run.step);

        const testbin_step = b.step("test-bin", "Build unit tests into separate binary");
        const testbin_step_run = b.addInstallArtifact(test_lib, .{});
        testbin_step.dependOn(&testbin_step_run.step);
    }

    // Example
    {
        const example = b.addExecutable(.{
            .name = "example",
            .root_source_file = b.path("src/example.zig"),
            .target = target,
            .optimize = optimize,
        });
        example.root_module.addImport("platform", module);

        if (b.lazyDependency("zgl", .{
            .target = target,
            .optimize = optimize,
        })) |dep| {
            example.root_module.addImport("zgl", dep.module("zgl"));
        }

        const example_step = b.step("example", "Build example");
        const example_step_run = b.addInstallArtifact(example, .{});
        example_step.dependOn(&example_step_run.step);
    }
}

const protocol_sources = [_][]const u8{
    "src/wayland/wayland-protocol.c",
    "src/wayland/xdg-shell-protocol.c",
};

The library builds fine, but I get a compile error as soon as the test builds:

❯ zig build test-bin && ./zig-out/bin/test
test-bin
└─ install test
   └─ zig test Debug native 2 errors
src/wayland.zig:10:15: error: C import failed
pub const c = @cImport({
              ^~~~~~~~
src/wayland.zig:10:15: note: libc headers not available; compilation does not link against libc
referenced by:
    wayland__struct_2114: src/wayland.zig:22:15
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
/home/nairou/.persist/dev/projects/platform/.zig-cache/o/b48dcaa3d9447f6277086297ce68bdf5/cimport.h:1:10: error: 'wayland-client-core.h' file not found
#include <wayland-client-core.h>
         ^
error: the following command failed with 2 compilation errors:
/nix/store/a1iiismj2iw26m91l13kc3arr584ix44-zig-0.13.0/bin/zig test -ODebug --dep platform -Mroot=/home/nairou/.persist/dev/projects/platform/src/lib.zig -Mplatform=/home/nairou/.persist/dev/projects/platform/src/lib.zig --cache-dir /home/nairou/.persist/dev/projects/platform/.zig-cache --global-cache-dir /home/nairou/.cache/zig --name test --listen=-

Splitting the library into a separate build.zig file just to hide these details from the other builds feels like a hack, but I’m not sure what else to try here.

1 Like

Do you have an example of this project on github/other? I’d be happy to try and compile it for you on my end, but it’s hard to guess without actually seeing the project and I’d like to try a few things before making a recommendation.

2 Likes

Presumably you want to link the lib into your test/example program. Following the ffmpeg example from here, that would mean:

  • lib would only include the C files, so .root_source_file = null, (or omit the root_source_file field)
  • You’d want to call module.linkLibrary(lib)

The rest I think can remain the same, but am not fully sure about that.

2 Likes

don’t you need your test step to also link libc ? I know I had issues previously with forgetting to link libc inside other steps :slight_smile:

1 Like

Alright, I’ve pushed to a temporary repo here: GitHub - Nairou/platform: Temporary

I appreciate you looking at it!

My goal is to have a self-contained library package that other code can import via package manager, but I also want to be able to run the included tests and example program during development. The two needs feel like they clash.

Good catch on the linkLibrary() call. However lib is more than just some C files, it’s primarily a Zig codebase but uses some C files. That might be why I’m having difficulty getting it to play nicely.

If the static library already links libc, I would think any programs that use it wouldn’t have to? I can definitely get the tests to run if I include all of the same system libraries and C header files that the library includes, but that feels like it defeats the purpose.

Have you tried to set .link_libc = true on addTest like this:

const test_lib = b.addTest(.{
    .root_source_file = b.path("src/lib.zig"),
    .target = target,
    .optimize = optimize,
    .link_libc = true,
});

That removes the libc error, but the missing C header error remains.

Strangely, the example executable doesn’t give the libc error, even without that parameter. I assume because it’s root file is separate.

And I think also:

module.linkLibrary(lib);

like @squeek502 has suggested.

Those are my guesses from what I have read here and my own half-knowledge.

Actually maybe you want to set .link_libc = true on:

const module = b.addModule("platform", .{
    .root_source_file = b.path("src/lib.zig"),
    .link_libc = true,
});

I am not sure.

That actually does make more sense, and prevents the test libc error just as well. Strange that the explicit lib.linkLibC() doesn’t do the same thing.

Edit: To clarify, the C header errors remain. This just clears the libc error.

It isn’t strange the lib.zig uses linux.zig and that uses wayland.zig which contains the @cImport I think the cImport requires link_libc because else it doesn’t have access to the c headers. At least that is how I read the error message.

1 Like

To clarify, link_libc cleared the libc error, but didn’t change the C header include errors. The C headers it is complaining about are local files in the project, but should only be needed for the library.

How is this:

pub const c = @cImport({
    @cInclude("wayland-client-core.h");
    @cInclude("wayland-client-protocol.h");
    @cInclude("xdg-shell-client-protocol.h");
    @cInclude("xkbcommon/xkbcommon.h");
    @cInclude("wayland-egl.h");
    @cInclude("EGL/egl.h");
    //@cDefine("EGL_EGLEXT_PROTOTYPES", {});
    @cInclude("EGL/eglext.h");
});

supposed to work if you don’t add this to module too?:

lib.addIncludePath(b.path("src/wayland"));

cImport needs to read the header files to create the translation.

module.addIncludePath(b.path("src/wayland"));