Incrementally adding zig code to a C project

For my own edutainment, I was interested in using zig to write some mods for quake 2, using the yamagi quake 2 fork. I had the general idea of just getting zig to compile the c code (into a game.dll that the engine then loads) and then incrementally adjusting the output to use zig functions instead. first wrappers, then more complex behaviors.

My knowledge of dlls, linking, and building in general is limited, and I’m not sure if this is even something that can be done inside of a build.zig file.

the include/ directory contains some files pulled directly from the yquake2 source.

I’m sufficiently out of my depth that this could be completely off-base and not even the right question to be asking, so I welcome any such corrections!

here is the contents of my build.zig file, currently outputting a bunch of (expected?) linker errors:

const std = @import("std");

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

    // NOTE: https://ziggit.dev/t/link-against-static-c-library-with-zig-0-16/15234/7
    // this was originally for the game.h file when i just wanted types/declarations
    // I first tried changing it to the g_main.c to see if that would work but it does not.
    const q2_game = b.addTranslateC(.{
        .root_source_file = b.path("include/game/g_main.c"),
        .optimize = optimize,
        .target = target,
    });

    const q2_mod = q2_game.createModule();

    const mod = b.addModule("sportsball", .{
        .root_source_file = b.path("src/root.zig"),
        .target = target,
        .optimize = optimize,
        .imports = &.{
            .{ .name = "q2", .module = q2_mod },
        },
    });

    mod.addCSourceFiles(.{
        .files = &.{
            "include/game/g_main.c",
            "include/game/g_ai.c",
            "include/game/g_chase.c",
            "include/game/g_cmds.c",
            "include/game/g_combat.c",
            "include/game/g_func.c",
            "include/game/g_items.c",
            "include/game/g_misc.c",
            "include/game/g_monster.c",
            "include/game/g_phys.c",
            "include/game/g_spawn.c",
            "include/game/g_svcmds.c",
            "include/game/g_target.c",
            "include/game/g_trigger.c",
            "include/game/g_turret.c",
            "include/game/g_utils.c",
            "include/game/g_weapon.c",
            // player/ and monster/ files too
        },
        .flags = &.{"-std=c99 -shared"},
    });

    mod.addIncludePath(b.path("include/common"));
    mod.addIncludePath(b.path("include/game"));
    mod.addIncludePath(b.path("include"));

    const game_dll = b.addLibrary(.{
        .linkage = .dynamic,
        .name = "game",
        .root_module = mod,
    });

    const q2_path = b.option(
        []const u8,
        "q2_path",
        "destination directory for the game.dll",
    );

    if (q2_path) |q2| {
        b.install_path = q2;
        std.log.info("q2 path: {s}", .{q2});
        std.log.info("b.install_path: {s}", .{b.install_path});
    }

    const copy_step = b.step("copy-mod", "Copy game.dll to q2_path");

    const q2_mod_dir: std.Build.InstallDir = .{
        .custom = "baseq2",
    };

    const dll_artifact = b.addInstallArtifact(game_dll, .{
        .dest_dir = .{
            .override = q2_mod_dir,
        },
    });

    copy_step.dependOn(&dll_artifact.step);
}

If you want zig to be the “child” and your existing build system to be the “parent” then you’ll need to probably be creative in the glue layer which holds them together. That could look like a bash script which builds the zig into an object file and then including that object file when you run your linker. At least, that’s roughly what static linking would look like.

When you allude to making a .dll, that would be dynamic linking. In other words, the quake code will load the zig code at runtime.

Static vs dynamic linking mostly have implications around packing, licensing, and distribution. I don’t think it’ll really matter which you pick for your use-case. I think static linking is basically a simpler choice since you end up producing just one binary in the end.

The alternative to all this is to invert control. Toss out your whole build system and get zig to build everything. You can even do this before writing a line of zig code except your build script, because zig is a complete general purpose build system. If you can get from 0=>1 on this, then it’s a nice path. How easy it is depends on how your current build system works.