In my chipz emulator toy project I’ve split the project into a handful modules, mirrored in the directory structure, e.g.:
src/
chips/ => chip emulators
common/ => shared helpers
host/ => host system glue (gfx, audio, input...)
systems/ => computer system emulators
Each of those directories has multiple source files, and a single re-export file, e.g. the chips directory has a re-export file chips.zig:
pub const z80 = @import("z80.zig");
pub const z80ctc = @import("z80ctc.zig");
pub const z80pio = @import("z80pio.zig");
pub const ay3891 = @import("ay3891.zig");
pub const intel8255 = @import("intel8255.zig");
…or the common/common.zig file looks like this:
pub const bitutils = @import("bitutils.zig");
pub const memory = @import("memory.zig");
pub const glue = @import("glue.zig");
pub const clock = @import("clock.zig");
pub const utils = @import("utils.zig");
pub const keybuf = @import("keybuf.zig");
pub const audio = @import("audio.zig");
pub const Beeper = @import("Beeper.zig");
pub const BoundedArray = @import("bounded_array.zig").BoundedArray;
In addition there is a top-level chipz.zig re-export file, this is for when the project is used as dependency for another project:
pub const chips = @import("chips");
pub const common = @import("common");
pub const systems = @import("systems");
pub const host = @import("host");
…up in the build.zig, each of those reexport files corresponds to one module:
const mod_common = b.addModule("common", .{
.root_source_file = b.path("src/common/common.zig"),
.target = target,
.optimize = optimize,
});
const mod_chips = b.addModule("chips", .{
.root_source_file = b.path("src/chips/chips.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "common", .module = mod_common },
},
});
const mod_systems = b.addModule("systems", .{
.root_source_file = b.path("src/systems/systems.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "common", .module = mod_common },
.{ .name = "chips", .module = mod_chips },
},
});
const mod_host = b.addModule("host", .{
.root_source_file = b.path("src/host/host.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "sokol", .module = dep_sokol.module("sokol") },
.{ .name = "common", .module = mod_common },
.{ .name = "shaders", .module = mod_shaders },
},
});
// top-level modules
const mod_chipz = b.addModule("chipz", .{
.root_source_file = b.path("src/chipz.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "common", .module = mod_common },
.{ .name = "chips", .module = mod_chips },
.{ .name = "systems", .module = mod_systems },
.{ .name = "host", .module = mod_host },
},
});
…and finally the import code for an emulated computer system looks like this, e.g. typically I import the top-level module and then ‘drill down’ to the submodule or even individual symbols:
const std = @import("std");
const assert = std.debug.assert;
const chips = @import("chips");
const z80 = chips.z80;
const ay3891 = chips.ay3891;
const common = @import("common");
const memory = common.memory;
const clock = common.clock;
const cp = common.utils.cp;
const audio = common.audio;
const DisplayInfo = common.glue.DisplayInfo;
…I think this sort of hierarchical project structure makes sense for most not-entirely-trivial projects, but may admittedly be overkill for simple cmdline tools…