Structuring imports inside a project

I can’t decide on what’s the best way to structure imports within my project.

A) I could directly import files I need, similarly to how I’d have done in in C, for example.

const Foo = @import("Foo.zig");
const Bar = @import("bar/Bar.zig");

B) Or I could just import the root file and use that.

const lib = @import("root.zig");

and then either use lib.Foo or alias it locally.

Which option to do you prefer in your projects?

I prefer A, it has the benefit of allowing you to have public declarations in the files without having to re-export them from the root, which you may not want for various things.

1 Like

I have the same opinion and prefer A, but then I see the large amount of imports and I wonder, shouldn’t I just import a single namespace?

you’d still have a bunch of aliases (an import is an alias to a file), unless you access the full path root.foo.bar.baz.buz, which I doubt.

You don’t change the amount of aliases, you just change how you get them.

1 Like

Currently, in practice, I only use pattern B) when importing modules.

But actually, I use B) quite a lot. I’m used to importing the main module and then using B). (not the "root" module!)

If I’m directly importing a file, I would just choose A).

1 Like

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…

8 Likes