C Sources Only Module Behavior

Related, but wanted a more modern follow-up as this thread is pretty old:

For a “module” containing anything other than Zig sources (C sources, header paths, linked libraries, etc.), is the “correct” way to add that as a dependency to use:

exe.root_module.addImport("mod_name", mod_variable);

While this works, it still feels a little like abusing a feature for something other than intended… Am I missing a command that may have been added that is a little more intentional? I feel like I’m reaching for a function named addDependency() or something that says:
“This exe depends on mod_variable, and so you need to compile all the sources/etc. contained within mod_variable, but this isn’t Zig code so there’s nothing to reference via @import()

There is no reason to use modules when you don’t have zig code.
But it is fine to have packages that have C artifacts (executables, static or dynamic libraries).

Hmmm, maybe a better question then is how do I accomplish this use case:

  • I have some C code (usually from some MCU vendor) that is some sort of self contained project
  • For a variety of reasons, I do not want to compile this code into a static library and then link it in to my final executable, I just want to include these sources/include directories in my main executable
    • For instance, why build a static library with code that is only ever ending up in my executable/not distributed elsewhere?
    • Another, way more annoying reason is this. A lot of the vendor code I use makes heavy use of weak symbols to provide a “default” implementation for an interrupt handler, configuration function, etc. All of the sudden, now that you’ve introduced this intermediate static library, you have to worry about link order and how the linker will resolve the weak symbols. I’ve actually been bit by this in the past and it wasn’t fun :slight_smile:

I’m looking for something akin to Meson’s “Dependency”:

The nice thing about a dependency in Meson is it doesn’t itself have to produce any artifact or executable. It’s simply a collection of sources, includes, etc. with associated compiler flags. Having an executable (or any artifact) depend on this dependency simply just adds all these sources/etc. to the compilation.

After more reading, you are correct, modules are indeed not my solution:

But is there anything currently that is…?

I don’t have practical experience in this area, so I am not sure if this actually would work, however I wondered if the following would make sense?

What if you create a zig module that contains just extern declarations for the c stuff (basically similar to what c-translate/cImport generates but possibly hand written), you then could use this:

I don’t know if this would help zig, with correctly resolving the weak linkage of certain symbols within a built artifact, that gets added to an executable.

When you are going to use the C sources addCSourceFile, or addCSourceFiles must be used.
You have two options, either you add them to your executable, or you add them to a static library and then you can reuse it in the executable or elsewhere.
See also: Static library from C source file - #2 by dimdin

Interesting! I’ll dig into this a little more and see if I can finally just solve outright the stack overflow behavior I linked instead of stepping around it :slight_smile:

1 Like

Ahhhh okay, I’ll dig a little more into what the compiler CLI ends up looking like and make sure it works for my use case!

At first I thought it seemed like you wanted to use build artifacts (dep exposes a shared or static lib as a build artifact, and in your build.zig you exe.linkLibrary(my_dep.artifact("mylib")). But, I reread your original post and it seems like you just want to copy some directories/files relative to the build root to the output destination. Have you considered addWriteFiles? Zig Documentation

You can do something like:

const wf = b.addWriteFiles();

_ = wf.addCopyDirectory(b.path("lib/mylib"), "mylib", .{});

const dir = b.addInstallDirectory(.{
  .source_dir = wf.getDirectory(),
  .install_dir = .prefix,
  .install_subdir = "",



Ahh, not quite, let me re-frame my original question since I didn’t do a great job of it.

My question is mostly organizational. Let’s say I have a mini project laid out like so:

- build.zig
- src/
  - main.c
  - other.c
  - other.h
  - sub-project/
    - subproj.c
    - subproj.h
    ... (other files) ... 

For one reason or another, let’s say I do not want to build the files contained within subproject/ into a static (or shared) library. However, my main executable, which consists of main.c, other.c/h depends on these files. Obviously in this toy example, I could just manually include those sources/include directories in my executable in my upper level build.zig.

However, let’s say sub-project contains LOTS of files, and LOTS of sub-directories, and has it’s own unique compile flags I want to apply to the files. It would be nice if I could add:

- sub-project/
  - build.zig

Which adds all these sources/includes/flags to some sort of dependency that I can then @import from my build.zig in the root directory. Now imagine this sub-project doesn’t necessarily live within my project but is in a Git repo and you might start to see what I’m after…

The reason addWriteFiles() won’t quite accomplish what I’m after is exe will depend on the action of copying over files, but it won’t automatically add those sources/includes to exe’s compilation.

Just to put a pin in this since it took me a good hour or two to finally chase down what was happening, it’s slightly different than what I originally thought. The linker issue is as follows:

My garbage (ST) HAL code essentially contains the following:

void defaultHandler(void)
    while (1)
void OtherInterrupt_Handler(void) __attribute__((weak, alias("defaultHandler")));


void OtherInterrupt_Handler(void);


void OtherInterrupt_Handler(void) {
    // Actual useful code

When you compile these three files into a static library, what symbol actually ends up getting used depends on the order the objects are specified while archiving. So, if fileB.o goes first, it picks the actual function implementation, if fileA.o goes first, it resolves to the weak alias of defaultHandler().

Contrast this behavior with just compiling these files along with a main.c into a binary, where what you expect happens: the “strong” symbol executing the useful code gets used independent of compilation order. When (past me) you aren’t expecting this, you’re left scratching your head about why compiling all your code into a binary works, but compiling some code into a static library then into your binary doesn’t, despite being identical code/compile settings.

The TLDR is defining a symbol weakly and strongly in the same static library is an anti-pattern due to what I just described. I guess to be fair to ST, this HAL code was never intended for a static library, but I digress.

Something that may work for this situation is what in CMake is called an Object Library, which when you add this as a dependency, the object files are added to your executable rather than linked as a library.

After some playing around with the build system I was able to get Zig to produce the desired behavior by creating an empty module with only an Object artifact and C sources, like such:

    const start = b.addObject(.{
        .name = "start",
        .target = target,
        .optimize = optimize,

        .files = &.{"start.c"}
        .root = b.path("src/start"),

    const exe = b.addExecutable(.{
        .name = "app.elf",
        .root_source_file = b.path("src/app.zig"),
        .target = target,
        .optimize = optimize,

This of course is not interesting to do within a single build.zig like that, but from here, the object library could be moved to another package and added like b.dependency("start", ...args...).artifact("start");

It’s essentially a roundabout way of doing exe.addCSourceFiles but may be useful if for practical reasons you want these objects to be in a separate module.


Nice I like this approach! I actually use CMake’s interface library for this purpose, since as of fairly recently you can add sources to it. Originally it was intended for header only libraries but w/ the ability to add sources it essentially functions exactly how I want.

I’ve also seen a couple of Zig project’s take the approach of just having a function called something like:

pub fn addToExe(exe: *Compile) void {
... add sources/whatever ...

That you can import directly from a .zig file in the project directory and use it in your build.zig.

I like the object approach though since you can explicitly declare it as a dependency using Zig’s builtin syntax.

1 Like

I’m not certain on this approach and if anyone has insight on whether there is a better alternative, I’d love some feedback.

What I mostly find myself doing when having to consume such vendor libraries/code to Zig is to first create a Zig module with bindings to the various types/functions (e.g. redeclare certain types with extern struct, integer-backed enums, etc. and ensure functions are better constrained to use multi-pointers or single pointers, proper optionals, etc.), then in the build.zig use addCSourceFiles on the declared module.

This way, I get a nice Zig package that I can import just like any other module into other projects, and all relevant C sources/other dependencies are pulled in as well through the module (without having to add sources/headers to each individual binary/library/other output artifact). And of course there’s the added benefit of minimizing [*c] pointers and c_int, etc. so that it is nicer to work with in Zig code.

It seems nicer to work with than the CMake “Object Library” alternative, only because it seems to fit nicely into other Zig build system/package manager uses, but I’m not 100% confident on whether that would solve the linker ordering issue…

The object library approach is specifically for code that must be part of the executable to link correctly, like overrides of weak symbols, overrides of libc symbols(e.g. syscalls, custom malloc impls), and entry C code that calls into your zig code(although in that case setting the entry symbol on your exe to cause it to be undefined for linking will work too).

A module seems to be preferable if you have more to it like a zig api and library functions. The use case here is something like STM32 where there are some startup and syscall sources that are distributed with the SDK that you might use out of the box without any API on top.


Yep @kibels described it perfectly. Your approach is absolutely valid for “normal” C code you want to wrap with Zig bindings. This is more for “C only” code that has all the qualifiers described. Think using Zig as a build system for a C project rather than having any actual Zig code in your project

1 Like

Shoot, actually this doesn’t quite work :frowning: Header paths aren’t passed along as to the executable via addObject().

It was as simple as adding a call to installHeadersDirectory(). See @kibels answer + this for the “complete” answer on how to do this!

Here’s a quick repo I made to demonstrate:

I’m just going to keep populating this repo with various examples of adding “sub-projects” in Zig since docs are light + post to showcase eventually :slight_smile:

1 Like

I almost made a follow-up about needing to add headers to the installed headers to make them part of the artifact but I figured it was only useful if you are as used to working with CMake as me and expect headers to come along with targets by default. :grin:

Since the obj library is mainly for things you don’t call from the app anyway they probably don’t need headers except possibly configuration headers.

1 Like