Using a single-header C library from Zig

What is the correct way of using single-header C libraries from Zig?
I’m sorry for the long post, there must be a small thing I’m missing, but I’m really lost here.

Until recently, I was happily doing “direct approach”:

const c = @cImport(
{
    @cDefine("RAYGUI_IMPLEMENTATION", {});
    @cInclude("raygui.h");
});

Now it doesn’t work anymore, and I have decided to switch to the “official” (?) approach, which, I assume, looks like this: add a C wrapper for the “raygui.h” to the project. This file #defines "RAYGUI_IMPLEMENTATION" and #includes "raygui.h".

However, I don’t understand how would I then call functions / use types defined in “raygui.h” from Zig.

Here’s a small example - my header library, shlib.h:

#ifndef SHLIB_H
#define SHLIB_H

// C single-header library

int multiply(int a, int b)
{
    return a * b;
}

typedef struct my_args_struct
{
    int a;
    int b;
} my_args;

int multiply_args(my_args args)
{
    return args.a * args.b;
}

#endif

Using “direct approach”:

const std = @import("std");
const c = @cImport({ @cInclude("shlib.h"); });

pub fn main() void
{
    std.debug.print("multiply returned {}\n", .{ c.multiply(2, 2) });
    const args = c.my_args{ .a = 10, .b = 20 };
    std.debug.print("multiply_args returned {}\n", .{ c.multiply_args(args) });
}

I compile it with zig build-exe direct.zig -idirafter ./ using 0.12.0-dev.819+75b48ef50 and it runs happily, which is strange, because it segfaults with my Zig Raylib examples.

Using “official approach”, I have to add my C wrapper (which is a simple one-liner, #include <shlib.h>), to my compilation command, something like zig build-exe official.zig shlib_impl.c -idirafter ./.

But then how do I call multiply(), create an instance of my_args or call multiply_args() from Zig code?

If I simply invoke it as multiply(2, 2), Zig rightly complains about error: use of undeclared identifier 'multiply'.
I also can’t @import("shlib_impl.c"); (shlib_impl.c is my C wrapper) or @cImport({ @cInclude("shlib_impl.c"); });

1 Like

I had the same issue trying to compile the stb_image header only library. Here is what you need to do:
First you need a C wrapper:

/// raygui_impl.c
#define RAYGUI_IMPLEMENTATION
#includes "raygui.h"

Then you need to include it in the compilation:

/// build.zig                     ↓ Could add multiple files           ↓ compilation flags
exe.addCSourceFiles(&[_][]const u8{".../raygui_impl.c"}, &[_][]const u8{"-g", "-O3"});

And finally you still need to include the header, but without the #define:

const c = @cImport({
    @cInclude("raygui.h");
});

If you want there is also a way to avoid creating the extra C file, by adding the define directly to the compile command arguments with -D.


Now this scheme won’t work with your shlib.h because it isn’t a fully functional header-only library. If you were to include shlib.h from 2 different compilation units, you would get a duplicate symbol error. Header-only libraries separate the header from the implementation to avoid this:

#ifndef SHLIB_H
#define SHLIB_H

// C single-header library

int multiply(int a, int b);

typedef struct my_args_struct
{
    int a;
    int b;
} my_args;

int multiply_args(my_args args);

#endif

#ifdef SHLIB_IMPLEMENTATION

int multiply(int a, int b)
{
    return a * b;
}

int multiply_args(my_args args)
{
    return args.a * args.b;
}
#endif
4 Likes

Thank you!

Header-only libraries separate the header from the implementation

Now that I think of it, it makes sense. The single header file, in fact, combines .h and .c files into one.
I’d never given much thought to single-header libraries, and I’m still not a fan, but sometimes you’ve got to work with what you’ve got.

Just a small note - shouldn’t the #ifndef with the include guard symbol, SHLIB_H, encompass the whole file?

shouldn’t the #ifndef with the include guard symbol, SHLIB_H, encompass the whole file?

No. Imagine the following scenario:

// Somewhere in another included file
#include "shlib.h"
...
#define SHLIB_IMPLEMENTATION
#include "shlib.h" // oops: the include guard swallowed the implementation

But now that I think about it, it should probably #undef SHLIB_IMPLEMENTATION to avoid problems in the opposite case:

#define SHLIB_IMPLEMENTATION
#include "shlib.h"
...
// Somewhere in another included file
#include "shlib.h" // oops: the implementation is added twice

…aand this is exactly why header libraries, and header files in general are such a bad idea.

Header files were an ugly hack from the very beginning. The programmer has better things to think about than what includes what, and how it all unfolds.

3 Likes

The wonders of preprocessor text inserts…

3 Likes

Indeed, it’s not the header files per se that are the problem, it’s the preprocessor and how #include works. You have dependency chasing (who includes/imports what) in every language.

I wanted to add this for the sake of clarity. @IntegratedQuantum, you probably know this, but I wanted to clarify for other readers. The typical formula for header guards is the following:

#ifndef MY_HEADER_GUARD
#define MY_HEADER_GUARD
// your header file code here
#endif

These do wrap an entire header file. If I include the same file twice in a row, MY_HEADER_GUARD is already defined and the second #ifndef gets bypassed.

Typically, headers do not contain implementations unless they are templated or inlined code. Otherwise, we get “ODR” violations.

In certain standard libraries, you’ll also get #pragma once with header guards because it’s not guaranteed to be supported.

@IntegratedQuantum I understand what you’re saying about doing a #define in the middle of a file, but full file header guards are a common case. Typically speaking, they are named after the file they are guarding with an unhealthy dose of underscores.

The case that you are referring to does conditional compilation statements midway through implementation files (although, they can be used to remove basically anything). The truly annoying thing is that you’ll get #ifndef and other preprocessor checks right in the middle of files to avoid the inclusion of certain code lines.

Usually, these are actually maintained independently and will be global for the whole system. In those cases, you want them to be defined across translation units.

To your point (however), there are certain files that are the most annoying. These are order dependent headers and yes, they do in fact have the exact problem you are outlining. The old afx libraries were like this and the entire system either doesn’t compile or (better yet) intellisense just suddenly gives up.

2 Likes

They’re a workaround (an euphemism for a crutch) for not getting a header file included more than once during pre-processing of the same compilation unit (one .C file), and they’re an OK solution, considering the tangled mess that the pre-processor can create.

Which is why not having your header file completely enveloped in a header guard block does not sit well with me.

If you add all the macro shenanigans, all the #define’d constants that the compiler knows nothing about… pre-processor is such a beautiful idea we all have to live with some 50 years later :man_facepalming:

I followed these steps but I’m getting an error on 0.11.0

error: C import failed
const miniaudio = @cImport({
                  ^~~~~~~~

error: 'miniaudio.h' file not found
#include <miniaudio.h>
         ^
// miniaudio_impl.c
#define MINIAUDIO_IMPLEMENTATION
#include "miniaudio.h"
// main.zig
const miniaudio = @cImport({
    @cInclude("miniaudio.h");
});
// build.zig
exe.addCSourceFiles(&[_][]const u8{"src/miniaudio_impl.c"}, &[_][]const u8{ "-g", "-O3" });
exe.addIncludePath(.{ .path = "src" });

maybe?

2 Likes

Yep that was it thanks! I thought src would have been included by default.

1 Like