Does Zig have modules?

Two questions I could do with some help with:

  • Does Zig have the concepts of modules?
  • If so, how to retrieve the module name during comptime or runtime.

So does Zig have modules? In most languages I have experience with, either the directory serves as the module delineation, or the file does.

In this question What does it mean when people say a file is a struct in Zig I asked about how a file in Zig could double up as a struct - hence that removes the file as the thing that serves as the “module”.

I skimmed the language reference here Documentation - The Zig Programming Language and I do not see “modules” explained as a concept.

But I am sure I have seen a couple of build.zig files that talks about “modules” so I am a bit confused on exactly what a module is in Zig.

First a questions: What are your requirements to classify something as a module?

Second The word module is a bit… historically confusing, in zig. It has since been tried to be clarified. In this defintion a module is:

a directory of files, along with a root source file that identifies the file referred to when the module is used with @import.

In that sense, a module is a type of build artifact. You can define modules in the build.zig.

If you mean a “file/or group of files” that contain namespaced code, then I think you get that functionality, even with the “Files as Struct” concept. Not all files are structs, but they are all namespaces.

2 Likes

Personally I think of modules (packages et al) as a namespace for other elements of the language like struct, classes, functions etc. Usually used for logical arrangement and also for code sharing.

Yes, zig have modules. A zig source file can be member of only one module.
A package can have zero or more modules: Package Management in 0.11.0 Release Notes

You can use @src() to get the module name, but currently this is available in master only: language: add module name field to @src

A file is a struct and a struct is a namespace.

Zig modules are not limited in their file structure. You can declare multiple modules in the build.zig and the only limitation is that a zig source file can be member of only one module.
You can also import modules by their name using: @import("module");

6 Likes

The part about a Zig source file being a member of only one module turns out to not be correct. Hadn’t occurred to me to try, so I did.

In fact, you can define two separate modules with the same .root_source_file. This is not a useful thing to do, so far as I know, and I wouldn’t count on it always being true.

You can even define two modules with the same name, and different root source files! That, I assume, is not intended behavior, and it’s unclear what happens when you do that and then try to include the module by name as a dependency in other code. I’m not curious enough to find out, but I would very much not count on this behavior long term.

A module is a Zig build target, with a root file, which is intended to be included in another Zig project as a dependency. A dependency and a module are not the same thing, because a given dependency (which has a name defined in a build.zig.zon) can export more than one module.

    if (b.lazyDependency("somedependency", .{
        .target = target,
        .optimize = optimize,
    })) |dep| {
        my_program.addImport("firstmodule", dep.module("firstmodule"));
        my_program.addImport("secondmodule", dep.module("secondmodule");
    }

Here, we have a dependency “somedependency”, presumably fetched from a code forge using zig fetch --save. It exports “firstmodule” and “secondmodule” in it’s own build.zig like so:

// https://code.forge/somedependency/build.zig
    const first_module = b.addModule("firstmodule", .{
        .root_source_file = b.path("src/firstmodule.zig"),
        .target = target,
        .optimize = optimize,
    });
// same for "secondmodule"

These can then be imported into my_program with @import("firstmodule"); and @import("secondmodule");.

As long as the two modules have a different name, and a different root source file, I’m confident that this is intended behavior, exporting more than one module in a dependency is a useful thing. For that matter, two modules with the same root source file and different names might be created with different build options, so maybe that use case is intended. I think the fact that you can make two differently-rooted modules with the same name is just something which isn’t currently checked for, due to the lack of an obvious mechanism for making use of the synonymous modules, or figuring out which one was intended.

But a source file which is the root of one module can be a dependency of a second module exported from the same build.zig, that’s definitely fine, and a good way to expose subsets of a larger library directly.

Last point: a module is a type of build target, not a type of source file. A project can have several build targets, they can be rooted in the same source file (it’s common to have a test build for example), and it all works. Modules happen when you call b.addModule, basically.

When you actually use the modules:

❯ zig build test
test
└─ run test
   └─ zig test Debug native 1 errors
src/c.zig:1:1: error: file exists in multiple modules
src/a.zig:1:19: note: imported from module a
const c = @import("c.zig");
                  ^~~~~~~
src/b.zig:1:19: note: imported from module b
const c = @import("c.zig");
                  ^~~~~~~

i.e. file c.zig cannot be part of both module a and module b.


It is easy to reproduce. Both a.zig and b.zig are:

const c = @import("c.zig");

test {
    _ = c;
}

c.zig is empty.

add in build.zig:

    const a_module = b.addModule("a", .{
        .root_source_file = b.path("src/a.zig"),
        .target = target,
        .optimize = optimize,
    });

    const b_module = b.addModule("b", .{
        .root_source_file = b.path("src/b.zig"),
        .target = target,
        .optimize = optimize,
    });

    unit_tests.root_module.addImport("a", a_module);
    unit_tests.root_module.addImport("b", b_module);

Where unit_tests is the step that runs as test.
The unit_tests root module must contain:

const a = @import("a");
const b = @import("b");

test {
	_ = a;
	_ = b;
}
2 Likes

Good to know, kinda makes sense. What happens if you just use a or just use b?

There aren’t that many circumstances where it makes sense to import a supermodule and a submodule of the same dependency. I think it should be supported but that adds significant complexity to the build system, might not be worth it.

It just works.

1 Like

I thought that might be the case.

So the rule is more like “a source file must exist in only one module imported as a dependency” rather than “any module must have a unique set of source files”.

I think it might be good to drop the first part entirely, although I don’t see it as a priority. I’m thinking of cases where the ‘main event’ is a module, and that module imports a file but only forwards some of the public declarations. It could be useful to expose that file as its own module, for a couple of reasons: user code might only need that one file’s functionality, or it might include the base library but also want access to the public functions from the ‘submodule’ file.

It’s a nice way to organize code, and the one-file rule means you can only get one of those things, using the submodule instead of the main one. Then again, forwarding all the public functions of the submodule solves the issue, so it isn’t a real pinch point that we can’t do that right now.

Worth pointing out that Zig modules, like Rust crates, don’t really have names. Name belongs to the dependency edge between the two modules.

So, both A and B can depend on C, but know it under two different names (eg, foo and bar)

3 Likes

This point is worth expanding a bit:

    if (b.lazyDependency("dependencyname", .{
        .target = target,
        .optimize = optimize,
    })) |dep| {
        my_program.addImport("whatimportcallsit", dep.module("modulename"));
    }

What @matklad is saying here is that your program can call “modulename” whatever it wants to using @import. For the build script to find it, you do have to use the name given to it in the dependency’s build.zig.

I’m fairly sure that the dependency name found in the build.zig.zon is also arbitrary. zig fetch --save will give it a name based on the URL when you use that, but as long as the .url and .hash field match, you can rename the struct itself, then use that name with b.lazyDependency or elsewhere. Useful in the event that two dependencies have the same repo name, which on a long enough timeline is sure to happen.