How to use translate-c to replicate a simple c app in zig

I am using nix-shell to pull in glfw and wrote this small C program to test that everything is ok:

#include <GLFW/glfw3.h>
#include <stdio.h>

int main() {
    if (!glfwInit()) {
        printf("Failed to initialize GLFW\n");
        return -1;
    }
    
    printf("GLFW initialized successfully!\n");
    printf("GLFW version: %s\n", glfwGetVersionString());
    
    glfwTerminate();
    return 0;
}

I can compile it with clang -o test main.c $(pkg-config --cflags --libs glfw3) and everything seems to work ok.

Now I am trying to do the same in zig while using translate-c.

The first step I did:
zig translate-c $(pkg-config --cflags --libs glfw3) src/c.h > src/c.zig

And that seems to nicely create my c.zig file for me.

The next step would be to make this part of build.zig but I don’t know how add glfw3 include paths, ideally using pkg-config:

    const c_translate = b.addTranslateC(.{
        .link_libc = true,
        .target = target,
        .optimize = optimize,
        .root_source_file = b.addWriteFiles().add("c.h",
            \\#include <GLFW/glfw3.h>
        ),
    });

    // How to add glfw3 include path using pkg-config?

    exe_mod.addImport("c", c_translate.createModule());

Playing with it, I’ve reached this:

fn prepareC(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.TranslateC {
    const c_translate = b.addTranslateC(.{
        .link_libc = false,
        .target = target,
        .optimize = optimize,
        .root_source_file = b.addWriteFiles().add("c.h",
            \\#include <GLFW/glfw3.h>
        ),
    });

    const tool_run = b.addSystemCommand(&.{"pkg-config"});
    tool_run.addArgs(&.{
        "--cflags",
        "glfw3",
    });

    const pkg_config_result = b.run(&.{
        "pkg-config", "--cflags", "glfw3",
    });

    var flags = std.mem.splitSequence(u8, pkg_config_result, " ");
    while (flags.next()) |flag| {
        if (std.mem.startsWith(u8, flag, "-I")) {
            c_translate.addIncludePath(.{ .cwd_relative = flag[2..] });
        }
    }

    return c_translate;
}

But it seems to still fail:

/Users/romeo/personal/tmp/zig-opengl/.zig-cache/o/637063a83c3a9087ef4a8bbb34802b3b/c.h:1:10: error: 'GLFW/glfw3.h' file not found
#include <GLFW/glfw3.h>
         ^
error: the following command failed with 1 compilation errors:
/Users/romeo/soft/zig-0.14.1/zig translate-c --listen=- -I /nix/store/awkf1x5vqj6lgm938wrccnzdjy21jp2y-glfw-3.4/include
 /Users/romeo/personal/tmp/zig-opengl/.zig-cache/o/637063a83c3a9087ef4a8bbb34802b3b/c.h

Which is weird because running /Users/romeo/soft/zig-0.14.1/zig translate-c -I /nix/store/awkf1x5vqj6lgm938wrccnzdjy21jp2y-glfw-3.4/include /Users/romeo/personal/tmp/zig-opengl/.zig-cache/o/637063a83c3a9087ef4a8bbb34802b3b/c.h straight from the CLI works.

Did you add some logging to check if flag[2..] actually results in the expected path to the GLFW directory?

Instead of Nix I would probably use a Zig package to provide GLFW though, e.g. see: GitHub - tiawl/glfw.zig: @glfw packaged for @ziglang

Yup, it spits out /nix/store/awkf1x5vqj6lgm938wrccnzdjy21jp2y-glfw-3.4/include which is the actual path.

From the logs I can see that it tries to run

$ Users/romeo/soft/zig-0.14.1/zig translate-c --listen=- -I /nix/store/awkf1x5vqj6lgm938wrccnzdjy21jp2y-glfw-3.4/include
 /Users/romeo/personal/tmp/zig-opengl/.zig-cache/o/637063a83c3a9087ef4a8bbb34802b3b/c.h

Which is the correct command. If I run it from the CLI, it works.

Rather than relying on zig packages I’m trying to learn more about how to use the interoperability of zig and c so it’s less about the end product and more about process itself.

so it’s less about the end product and more about process itself

…you could compare against the Dear ImGui bindings build.zig I’m maintaining here:

Although that doesn’t require a header search path in the translateC step since the translated C header is consumed directly in the step without going through intermediate header.

Ok, so for the sake of other people trying to do the same thing, here is how I ended up solving this.

First I made this function that can search for the libs using pkg-config (you need to have pkg-config installed)

fn findPathWithPkgConfig(b: *std.Build, lib: []const u8) []const u8 {
    const tool_run = b.addSystemCommand(&.{"pkg-config"});
    tool_run.addArgs(&.{
        "--cflags",
        "glfw3",
    });

    const pkg_config_result = b.run(&.{
        "pkg-config", "--cflags", lib,
    });

    const flags = std.mem.trim(u8, pkg_config_result, " \n\t");
    var flag_iter = std.mem.splitSequence(u8, flags, " ");
    while (flag_iter.next()) |flag| {
        if (std.mem.startsWith(u8, flag, "-I")) {
            return flag[2..];
        }
    }

    @panic("Could not find path");
}

then you can use it to provide include paths to your translate C:

fn translateC(
    b: *std.Build,
    target: std.Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
) *std.Build.Module {
    const path = findPathWithPkgConfig(b, "glfw3");

    const translate_c = b.addTranslateC(.{
        .root_source_file = b.addWriteFiles().add("c.h",
            \\#include <GLFW/glfw3.h>
        ),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    translate_c.addIncludePath(.{ .cwd_relative = path });

    return translate_c.createModule();
}

and then you can easily use this to provide your zig code with a c module:

pub fn build(b: *std.Build) void {
    // ...

    const c = translateC(b, target, optimize);

    const exe = b.addExecutable(.{
        .name = "zig-opengl",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{ .name = "c", .module = c },
            },
        }),
    });

    // ...
}

It seems to do the trick.