How to make Zig libraries for Zig?

I’m making a library in Zig, for Zig.

This library has multiple backends depending on what system you’re on, and these backends are NOT supposed to be user-facing.

My question is, when creating a library for others to use, how do you hide non user-facing code/imports?

In C, you would simply compile your library, and then provide a header file with the function declarations that are for the user. But zig doesn’t use compiled libraries for native Zig code.

I’ve had folks tell me to use addModule in the build.zig file, but this doesn’t seem correct as that’s for creating a compilable object (such as a .a/.so or executable binary).

Here’s my tree structure

.
├── build.zig
├── examples
├── include
│   ├── mica_gfx.h
│   └── mica_widgets.h
├── src
│   ├── c_api
│   │   └── core.zig
│   ├── core
│   │   ├── input.zig
│   │   ├── platform
│   │   │   ├── wayland.zig
│   │   │   ├── win32.zig
│   │   │   └── x11.zig
│   │   └── window.zig
│   ├── gfx
│   │   └── backends
│   └── widgets
│       └── builtin
├── test
├── test.c
├── tests
└── zig-out
    ├── include
    └── lib
        └── libmica_core.so

and as an example, I would only want users to import src/core/window.zig. Does this have to do with how I define my build.zig.zon, defining importable modules?

Your build must be complicated.

Please show an example.

Is there a reason you don’t want the module src exposed to the end user and the compiler?

There is another post on here about dynamic linking without libc. While not directly related it seems like it could be use.

I don’t think that’s completely right,
addModule is for creating modules that can be accessed from other packages, those then can be depended on by further/other modules, they can contain both Zig code and c source files and you can link artifacts to them, you can create packages that allow others to use those modules from their own modules/packages, which are then used as the root module for an executable or compiled library artifact, or they can be used as imports on other modules.

The advice you got is almost correct, the usual way to create Zig libraries is to create a package that provides a module (which is essentially a bundle of source code and related sources, include pathes, etc.) which you then use while writing your own code, all those modules then get compiled into a single compilation unit, going beyond a single compilation unit is for more advanced use cases, where people have some special reason to use more than one compilation unit and it requires creating multiple artifacts (executables, c-style-libraries).

So using modules is the right advice, but for those modules that should stay internal to a package you would use createModule instead of addModule the difference is that the latter also makes it accessible from outside the package, while createModule only creates the module (so it can be used for modules that are supposed to be used solely internally).

So you would expose the library with a module that uses addModule and the internal backends would use createModule and only add an import of that module to your api-module which then uses it access the backend functionality.

4 Likes

And when it comes to hiding non-user facing code inside a module, do not make declarations/functions “pub” by default, only the ones to be exposed.

2 Likes

Users can’t navigate the file hierarchy of your module, they can only navigate pub symbols in your root file (src/root.zig usually).

That’s how you define a public interface to your code. Anything that you don’t expose in your root file as a pub decl is inaccessible by users.

4 Likes

Could you provide a quick example of what that would look like?

Since there is nothing to compile, simply doing

const mod = b.addModule(.{...})

would throw an error as mod isn’t being used.

Would that be in the build.zig, build.zig.zon, or both?
For example if I simply had the structure

.
└── src
    ├── backend
    │   └── non_user_backend.zig
    └── user_api.zig

how would I define this project so users could only see what’s defined in user_api.zig?

I made a video about this:

https://www.youtube.com/watch?v=jy7w_7JZYyw

3 Likes

Perfect, that’s exactly what I was looking for.

The piece that I was missing is that discarding the pointer of addModule still keeps it in the build tree for reference.

So based on this, I could just do _ = b.addModule() on the user-facing api, and when a user decides to use this library, they’ll only see what’s exposed by that module?

Sounds correct to me. addModule inserts an entry into the b.modules hash map, so it is not lost forever when you discard the pointer.

(Welcome to Ziggit btw, I was that Reddit guy who suggested you to ask here :smiley: )

What addModule does differently is irrelevant to this behaviour.

The build API allocates all ‘steps’ on the heap as they need to live longer than the build function, as the actual running of the build happens after that.

I don’t think that is relevant to what OP is asking. createModule also allocates the Module on the heap, but it is definitely not in the build tree or can be referenced by client/user packages.

Mentioning heap allocation here will only confuse OP because there has been zero questions about how the steps are allocated in this thread. The questions are always about if the module created is exposed elsewhere, even if you don’t use it to addExecutable / addLibrary in the same build.zig of the library.

1 Like

I was just being pedantic, though I think it is good to know in general that steps are heap allocated.