Zig c++ how to handle template-heavy dependencies?

Hello,

I’m working on a project which wraps the C++ sdk of the DAW Reaper.

I’m having to compile the c++ dependencies using an external command instead of using addCSourceFiles and zig’s internal C compiler.

The reason is: the C++ dependencies carry a lot of templates which break my compilation. I figure there must be a way to work around the problem, but I can’t figure it out.

In one case, linking libCpp and using addCSourceFiles will compile fine on Macos but fail on Linux, due to the templates.

I’ve resorted to the following build. Here’s an abridged version:

const lib = b.addSharedLibrary(.{
    .name = "reaper_c1",
    .root_source_file = b.path("src/console1_extension.zig"),
    .target = target,
    .optimize = optimize,
});

const wdl_dep = b.dependency("WDL", .{ .target = target, .optimize = optimize });
const reaper_sdk_dep = b.dependency("reaper-sdk", .{ .target = target, .optimize = optimize });

var client_install: *std.Build.Step.InstallArtifact = undefined;
lib.linkLibC();

// I’m having to link against AppKit and libCpp on Macos
if (target.result.isDarwin()) {
    lib.root_module.linkFramework("AppKit", .{});
    lib.linkLibCpp();
    client_install = b.addInstallArtifact(lib, .{
        .dest_sub_path = "reaper_c1.dylib",
        .dest_dir = .{ .override = .{
            .custom = "",
        } },
    });
} else {
    client_install = b.addInstallArtifact(lib, .{
        .dest_sub_path = "reaper_c1.so",
        .dest_dir = .{ .override = .{
            .custom = "",
        } },
    });
}

// […]

// include path for c header files
lib.addIncludePath(b.path("./src/"));
lib.addIncludePath(wdl_dep.path("")); 
lib.addIncludePath(reaper_sdk_dep.path("")); 

// I need to add a system command to run a PHP script
// which generates some modstub and rc files
const php_cmd = b.addSystemCommand(&[_][]const u8{"php"});
php_cmd.addFileArg(php_script); // Convert LazyPath to string
php_cmd.addFileArg(resource_rc); // Convert LazyPath to string
lib.step.dependOn(&php_cmd.step);

// from here, get the CPP flags, list the cpp files
// and call the external compiler (clang or gcc) on each of the files. 
// their output is then added as 
const cpp_flags = [_][]const u8{
    if (target.result.isDarwin()) "clang" else "gcc",
    "-c",
    "-fPIC",
    "-O2",
    "-std=c++14",
    b.fmt("-I{s}", .{wdl_dep.path("").getPath(b)}),
    "-DSWELL_PROVIDED_BY_APP",
    "-o",
};
// Define cpp files using dependency paths
const cppfiles = [_]std.Build.LazyPath{
    b.path("src/csurf/control_surface.cpp"),
    b.path("src/csurf/control_surface_wrapper.cpp"),
    b.path("src/csurf/midi_wrapper.cpp"),
    modstub,
};

// Add the C++ source files to the library as object files
inline for (comptime cppfiles) |cppfile| {
    const filearg = try std.fmt.allocPrint(
        b.allocator,
        "{s}.o",
        .{std.fs.path.basename(cppfile.getPath(b))},
    );
    const cxx = b.addSystemCommand(&cpp_flags);
    lib.addObjectFile(cxx.addOutputFileArg(filearg));
    cxx.addFileArg(cppfile);
    cxx.step.dependOn(&php_cmd.step);
}

My questions:

  • is there a way to use the linkCSourceFiles for this - and have it handle the cpp templates on Linux, or am I stuck with this approach?
  • which compiler does zig wrap on Linux? GCC or Clang?

Wait, why would templates break your build on Linux? That sounds fishy. How are they breaking exactly?

I’m just worried you’re trying to solve a different problem than the one you’re having.

If you compile C++ code, then you should link libCpp on all platforms.

Hi,
I have an small example how to compile c++ code using zig toolchain. Zig is very useful for cross-compiling. I try to setup build.zig.zon for each library I need and I just add to my project.

You can also check more details about the compiler using:
zig c++ --version

Here is the output using zig 0.13

clang version 18.1.6 (https://github.com/ziglang/zig-bootstrap 98bc6bf4fc4009888d33941daf6b600d20a42a56)
Target: x86_64-unknown-linux-musl
Thread model: posix
InstalledDir: /usr/bin
1 Like

Thanks for the responses!

I investigated using my addCSourceFile version ((pin to commit)[build: remove wdl relative paths · AntoineBalaine/reaperConsole1@73bc0ab · GitHub]):

lib.linkLibC();
lib.linkLibCpp(); // linking for both macos & linux
const cpp_flags = [_][]const u8{
    "-c",
    "-fPIC",
    "-O2",
    "-std=c++14",
    b.fmt("-I{s}", .{wdl_dep.path("WDL").getPath(b)}),
    "-DSWELL_PROVIDED_BY_APP",
};

const cppfiles = [_]std.Build.LazyPath{
    b.path("src/csurf/control_surface.cpp"),
    b.path("src/csurf/control_surface_wrapper.cpp"),
    b.path("src/csurf/midi_wrapper.cpp"),
    modstub,
};

// Add the C++ source files to the library
for (cppfiles) |cppfile| {
    lib.addCSourceFile(.{
        .file = cppfile,
        .flags = &cpp_flags,
    });
}

This version compiles on macos, but the linking doesn’t happen: I get into a trace trap every time I try to call the C++ code. The app just throws an exception an crashes:

unsigned char *MIDI_event_message(MIDI_event_t *evt) {
  return evt->midi_message;
// Exception has occurred: Exception
//EXC_BREAKPOINT (code=1, subcode=0x13c67586c)
};

On Linux, the build is a non-starter. I get this kind of errors:

install
└─ install reaper_c1
   └─ zig build-lib reaper_c1 Debug native 21 errors
/usr/lib/zig/libcxx/include/__type_traits/enable_if.h:20:1: error: templates must have C++ linkage
template <bool, class _Tp = void>
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
home/user/.cache/zig/p/122026ee38b3ef9cbb44a2cf8f1735ba5dbaef29975560ad3e2f78b43437707a8b9a/WDL/swell/swell-modstub-generic.cpp:26:10: note: in file included from home/user/.cache/zig/p/122026ee38b3ef9cbb44a2cf8f1735ba5dbaef29975560ad3e2f78b43437707a8b9a/WDL/swell/swell-modstub-generic.cpp:26:
#include "swell.h"


/usr/lib/zig/libcxx/include/__type_traits/enable_if.h:23:29: error: explicit specialization of undeclared template struct 'enable_if'
struct _LIBCPP_TEMPLATE_VIS enable_if<true, _Tp> {
                            ^~~~~~~~~~~~~~~~~~~~
home/user/.cache/zig/p/122026ee38b3ef9cbb44a2cf8f1735ba5dbaef29975560ad3e2f78b43437707a8b9a/WDL/swell/swell-modstub-generic.cpp:26:10: note: in file included from home/user/.cache/zig/p/122026ee38b3ef9cbb44a2cf8f1735ba5dbaef29975560ad3e2f78b43437707a8b9a/WDL/swell/swell-modstub-generic.cpp:26:
#include "swell.h"
/usr/lib/zig/libcxx/include/__type_traits/enable_if.h:28:48: error: no template named 'enable_if'
using __enable_if_t _LIBCPP_NODEBUG = typename enable_if<_Bp, _Tp>::type;


… BUNCH OF SIMILAR ERRORS

Any thoughts? I’m quite puzzled.

I see in @a.maza ’s example build that he’s calling linkLibrary. I’m just using addIncludePaths, is there a big difference?

The compiler treats them as C sources.
Does modstub have a c++ extension?
You might want to add -x c++ in cpp_flags.

The modstub: on macos, I have to use a .mm extension. It tells the host UI of my library how to draw my lib’s settings in its preferences menu.

    const modstub = if (target.result.isDarwin())
        wdl_dep.path("WDL/swell/swell-modstub.mm")
    else
        wdl_dep.path("WDL/swell/swell-modstub-generic.cpp");

When it comes to -x,

    const cpp_flags = [_][]const u8{
        "-x",
        "-c",
        "-fPIC",
        "-O2",
        "-std=c++14",
        b.fmt("-I{s}", .{wdl_dep.path("WDL").getPath(b)}),
        "-DSWELL_PROVIDED_BY_APP",
    };

This breaks the compilation on both Macos and Linux:

:1:1: error: '-x -c' after last input file has no effect
:1:1: error: language not recognized: '-c'
warning(compilation): failed to delete '.zig-cache/tmp/d281134a8ce09b1a-control_surface.o.d': FileNotFound
warning(compilation): failed to delete '.zig-cache/tmp/972fb517c9c94036-midi_wrapper.o.d': FileNotFound
warning(compilation): failed to delete '.zig-cache/tmp/d959594545c1a875-control_surface_wrapper.o.d': FileNotFound

Removing the -c flag yields the same error message.

The -x option takes an argument, you need:

    const cpp_flags = [_][]const u8{
        "-x",
        "c++",
        "-c",  // only if this was already here
        "-fPIC",
        ...
1 Like

Oh, I see. Well, for what it’s worth - it triggers the build, but I’m still getting the trace trap, the app’s still crashing and I’m still getting the same errs on the Linux build.

From discussions I recall with some reaper community members, the reason why I had to resort to using gcc on linux was because the linux build needed to use gcc’s libstdc++ instead of clang’s libc++.

Something something about C++ dependencies using non-standard syntax which was accepted by gcc and not clang on linux…

I tried passing -stdlib=libstdc++ as flag, but the compiler tells me the argument’s unused.

So, is there a way to link libstdc++ while using addCSourceFiles ?

Ok, I think I found my answer:

C++ pretends to have an ABI, but it’s a lie. That’s the crucial problem here.

Zig provides libc++, however the libc++ that zig provides cannot be mixed with any C++ objects compiled from other compilers. If you do, you get undefined behavior. So you can’t use zig c++ on libonnx2c_lib.a or /usr/local/lib/libprotobuf.so because these C++ objects were compiled with something other than zig c++.

I’m upset that C++ works this way but there is nothing we can do about it.

So basically, I can’t use the zig compiler for this task unless I recompile all the dependencies myself - which is not possible since they’re provided by the host app…

Thanks everyone for helping me here.

1 Like