Figuring out how to use translateC

I’m trying to wrap my head around integrating C dependencies in a project and compile them with zig. Thanks to this topic I have a working prototype that uses @cImport. It looks like this:

// build.zig
const std = @import("std");

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

    const lib_mod = b.createModule(.{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe_mod.addImport("apollonius_lib", lib_mod);

    // load miniaudio dependency
    const miniaudio_dep = b.dependency("miniaudio", .{});

    // compile as a static library
    const miniaudio = b.addLibrary(.{
        .linkage = .static,
        .name = "miniaudio",
        .root_module = b.createModule(.{
            .root_source_file = null,
            .optimize = .ReleaseFast,
            .target = target,
        }),
    });
    miniaudio.root_module.addCSourceFile(.{ .file = miniaudio_dep.path("miniaudio.c") });
    miniaudio.linkSystemLibrary("pthread");

    // link the static library into the executable
    exe_mod.linkLibrary(miniaudio);
    exe_mod.addIncludePath(miniaudio_dep.path("."));

    const exe = b.addExecutable(.{
        .name = "apollonius",
        .root_module = exe_mod,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const lib_unit_tests = b.addTest(.{
        .root_module = lib_mod,
    });

    const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);

    const exe_unit_tests = b.addTest(.{
        .root_module = exe_mod,
    });

    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_lib_unit_tests.step);
    test_step.dependOn(&run_exe_unit_tests.step);
}

Apparently, it is necessary to compile miniaudio in ReleaseFast mode to avoid tripping ubsan.

// main.zig
const std = @import("std");
const lib = @import("apollonius_lib");

const c = @cImport(@cInclude("miniaudio.h"));

pub fn main() !void {
    var result: c.ma_result = undefined;
    var engine: c.ma_engine = undefined;

    result = c.ma_engine_init(null, &engine);
    if (result != c.MA_SUCCESS) {
        return error.FailedToInit;
    }
    defer c.ma_engine_uninit(&engine);

    result = c.ma_engine_play_sound(&engine, "./amenbreak.wav", null);
    if (result != c.MA_SUCCESS) {
        return error.FailedToPlaySound;
    }

    std.time.sleep(10 * std.time.ns_per_s);
}

Now, I would like to expose miniaudio as a module. Taking inspiration from this post, I modified my build.zig to compile miniaudio like this:

    const miniaudio_mod = blk: {
        const dep = b.dependency("miniaudio", .{});
        const tc = b.addTranslateC(.{
            .root_source_file = dep.path("miniaudio.h"),
            .target = b.graph.host,
            .optimize = .ReleaseFast,
        });
        const mod = tc.createModule();
        mod.addCSourceFile(.{
            .file = dep.path("miniaudio.c"),
        });
        mod.linkSystemLibrary("pthread", .{});
        break :blk mod;
    };

    exe_mod.addImport("miniaudio", miniaudio_mod);

It builds, but crashes at runtime. I’m not completely sure of what I’m doing here, so here are a few questions:

  • is my code doing something equivalent to what the @cImport is doing in the first version?
  • compilation is slower: 35 seconds for the initial version, 79 seconds for the second version (both with .zig-cache deleted before of course). If @cImport calls translate-c behind the scene, why the difference?
  • why .target = b.graph.host and not .target = target?
  • the runtime crash is the same as the one when you compile in debug mode. If I compile everything with ``–release=fastit works. It looks like the.optimize = .ReleaseFastpassed toaddTranslateC` is ignored?

Anyway, thanks everyone, it is tough to find up to date information about the build system, and this community is gold. Also, big shoutout to @kristoff - your recent video tutorial on the build system helped a lot.

You should use .target = target here, .target = b.graph.host in my post was only necessary because the library was going to be used by the build process.

This shouldn’t be the case:

Try building with zig build --verbose to see whether zig translate-c and the relevant part of zig build-exe is invoked with -OReleaseFast or not.

You can look at dcimgui (GitHub - floooh/dcimgui: All in one Dear ImGui source distro for C++, C and Zig. - basically a ‘Dear ImGui souce distribution’ which can also be used in Zig projects) as example.

Here Dear ImGui and its C bindings are built into a library:

Here the translateC step is defined which translates the cimgui.h C header into a Zig file:

…and here this Zig file is declared as a module (which is the actual module that’s used by upstream projects):

…and finally here, the C library is added as static linker dependency to the Zig module so that any upstream user of the Zig module automatically also links with the C library:

…a project which uses this package first declares it in the build.zig.zon:

…in the build.zig gets the dependency:

…and uses it like this:

…ignore anything in the build.zig files that mention WASM or Emscripten, since this requires a lot of special-case-code tha’s not needed for regular native builds.