Issue building C++ dynamic library

I am working on converting my C++ project to use the zig build system as a part of this effort I am converting the yaml-cpp build system to zig as well.

I have run into an issue where if I build yaml-cpp source files directly as a part of my executable I have no issue running my code as expected. However, when I build it as a dynamic library and try to link to it I have a malloc error. I also have no issue if I change the linkage type for the library to static instead of dynamic.

main(88451,0x20c492140) malloc: *** error for object 0x102e384b8: pointer being freed was not allocated
main(88451,0x20c492140) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort      ./zig-out/bin/main

This is the version of my build script which adds everything to the executable directly

const std = @import("std");
const zcc = @import("compile_commands");

pub fn build(b: *std.Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{ .name = "main", .root_module = b.createModule(.{
        .target = target,
        .optimize = optimize,
    }) });

    exe.addIncludePath(b.path("include"));
    exe.addIncludePath(b.path("include/inputs"));
    exe.addIncludePath(b.path("yaml-cpp-0.8.0/include"));
    exe.addCSourceFiles(.{
        .files = &.{
            "main.C",
            "src/InputErrorHelper.C",
            "src/InputParameters.C",
            "src/TypeNameHelper.C",
            "src/Parameter.C",
            "yaml-cpp-0.8.0/src/binary.cpp",
            "yaml-cpp-0.8.0/src/convert.cpp",
            "yaml-cpp-0.8.0/src/depthguard.cpp",
            "yaml-cpp-0.8.0/src/directives.cpp",
            "yaml-cpp-0.8.0/src/emit.cpp",
            "yaml-cpp-0.8.0/src/emitfromevents.cpp",
            "yaml-cpp-0.8.0/src/emitter.cpp",
            "yaml-cpp-0.8.0/src/emitterstate.cpp",
            "yaml-cpp-0.8.0/src/emitterutils.cpp",
            "yaml-cpp-0.8.0/src/exceptions.cpp",
            "yaml-cpp-0.8.0/src/exp.cpp",
            "yaml-cpp-0.8.0/src/memory.cpp",
            "yaml-cpp-0.8.0/src/node_data.cpp",
            "yaml-cpp-0.8.0/src/node.cpp",
            "yaml-cpp-0.8.0/src/nodebuilder.cpp",
            "yaml-cpp-0.8.0/src/nodeevents.cpp",
            "yaml-cpp-0.8.0/src/null.cpp",
            "yaml-cpp-0.8.0/src/ostream_wrapper.cpp",
            "yaml-cpp-0.8.0/src/parse.cpp",
            "yaml-cpp-0.8.0/src/parser.cpp",
            "yaml-cpp-0.8.0/src/regex_yaml.cpp",
            "yaml-cpp-0.8.0/src/scanner.cpp",
            "yaml-cpp-0.8.0/src/scanscalar.cpp",
            "yaml-cpp-0.8.0/src/scantag.cpp",
            "yaml-cpp-0.8.0/src/scantoken.cpp",
            "yaml-cpp-0.8.0/src/simplekey.cpp",
            "yaml-cpp-0.8.0/src/singledocparser.cpp",
            "yaml-cpp-0.8.0/src/stream.cpp",
            "yaml-cpp-0.8.0/src/tag.cpp",
        },
        .flags = &.{
            "-std=c++17",
            "-g",
        },
    });
    exe.linkLibCpp();
    b.installArtifact(exe);
}

and this is the version with the dynamic library

const std = @import("std");
const zcc = @import("compile_commands");

pub fn build(b: *std.Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const yaml_cpp = b.addLibrary(.{ .name = "yaml-cpp", .linkage = .dynamic, .version = .{ .major = 0, .minor = 8, .patch = 0 }, .root_module = b.createModule(.{
        .target = target,
        .optimize = optimize,
    }) });

    yaml_cpp.addIncludePath(b.path("yaml-cpp-0.8.0/include"));
    yaml_cpp.linkLibCpp();
    yaml_cpp.addCSourceFiles(.{
        .files = &.{
            "yaml-cpp-0.8.0/src/binary.cpp",
            "yaml-cpp-0.8.0/src/convert.cpp",
            "yaml-cpp-0.8.0/src/depthguard.cpp",
            "yaml-cpp-0.8.0/src/directives.cpp",
            "yaml-cpp-0.8.0/src/emit.cpp",
            "yaml-cpp-0.8.0/src/emitfromevents.cpp",
            "yaml-cpp-0.8.0/src/emitter.cpp",
            "yaml-cpp-0.8.0/src/emitterstate.cpp",
            "yaml-cpp-0.8.0/src/emitterutils.cpp",
            "yaml-cpp-0.8.0/src/exceptions.cpp",
            "yaml-cpp-0.8.0/src/exp.cpp",
            "yaml-cpp-0.8.0/src/memory.cpp",
            "yaml-cpp-0.8.0/src/node_data.cpp",
            "yaml-cpp-0.8.0/src/node.cpp",
            "yaml-cpp-0.8.0/src/nodebuilder.cpp",
            "yaml-cpp-0.8.0/src/nodeevents.cpp",
            "yaml-cpp-0.8.0/src/null.cpp",
            "yaml-cpp-0.8.0/src/ostream_wrapper.cpp",
            "yaml-cpp-0.8.0/src/parse.cpp",
            "yaml-cpp-0.8.0/src/parser.cpp",
            "yaml-cpp-0.8.0/src/regex_yaml.cpp",
            "yaml-cpp-0.8.0/src/scanner.cpp",
            "yaml-cpp-0.8.0/src/scanscalar.cpp",
            "yaml-cpp-0.8.0/src/scantag.cpp",
            "yaml-cpp-0.8.0/src/scantoken.cpp",
            "yaml-cpp-0.8.0/src/simplekey.cpp",
            "yaml-cpp-0.8.0/src/singledocparser.cpp",
            "yaml-cpp-0.8.0/src/stream.cpp",
            "yaml-cpp-0.8.0/src/tag.cpp",
        },
        .flags = &.{
            "-std=c++17",
            "-g",
        },
    });

    const exe = b.addExecutable(.{ .name = "main", .root_module = b.createModule(.{
        .target = target,
        .optimize = optimize,
    }) });

    exe.addIncludePath(b.path("include"));
    exe.addIncludePath(b.path("include/inputs"));
    exe.addIncludePath(b.path("yaml-cpp-0.8.0/include"));
    exe.addCSourceFiles(.{
        .files = &.{
            "main.C",
            "src/InputErrorHelper.C",
            "src/InputParameters.C",
            "src/TypeNameHelper.C",
            "src/Parameter.C",
        },
        .flags = &.{
            "-std=c++17",
            "-g",
        },
    });
    exe.linkLibrary(yaml_cpp);
    exe.linkLibCpp();
    b.installArtifact(exe);
    b.installArtifact(yaml_cpp);
}

All the source code for this is available here https://github.com/gsgall/prism/tree/rewrite/inputs

I am using zig version 0.16.0-dev.368+2a97e0af6 on an apple silicon laptop.

I have been stuck on this for quite a while so if anyone has some insight as to what the issue might be I would really appreciate it!

Isn’t his a case of having 2 runtime linked ? Because C++ is such a great and well designed language that this wouldn’t surprise me. I already had to deal with something similar. Maybe worth exploring, with asan to see if that’s the issue. I also had an issue related to Zig this time with C++, when trying to cross-compile to raspberry pi and link to some C++ library Zig has an heuristic that automatically detect C++ (or maybe that’s the clang driver don’t quote me on that) anyway those lib I was trying to link against where using libstc++ but clang by default uses libc++ and so I had the linker screaming and crying that it couldn’t decide which symbols to pick. I didn’t go further because honestly I’m quite haunted by C++, and I couldn’t find a way to remove prevent Zig from adding -lc++ to the compile command

I have looked into where the malloc error is is occurring and when I build with make or static then I don’t have any issues with it so I don’t think it’s a code issue.

For the linking I also thought it might be something like that with different libraries but everything I need is being built with zig and it looks like both the library and the executable are being linked to the same C++.

otool -L zig-out/bin/main
zig-out/bin/main:
	@rpath/libyaml-cpp.dylib (compatibility version 1.0.0, current version 0.8.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)
otool -L zig-out/lib/libyaml-cpp.dylib
zig-out/lib/libyaml-cpp.dylib:
	@rpath/libyaml-cpp.dylib (compatibility version 1.0.0, current version 0.8.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

Feel free to use my Zig build of yaml-cpp here: GitHub - JacobCrabill/zig-yaml-cpp: Zig build of the yaml-cpp repo

I’ve used it from other C++ code built with Zig, so I know it works in that use case, at least.

Thank you so much! I was able to compile and run my code with your version.

I’m sure I’ll be able to learn about the way to properly build these sort of things from your file!

1 Like

My first guess would be that some C++ object is created inside the DLL but destroyed outside the DLL (maybe because lifetime is managed via smart-pointers). In some scenarios this might work in others not, in general I would not build DLLs that expose a C++ API, but wrap the C++ code in a C API and design the C API in a way which makes sure that all C++ objects which are created inside the DLL are also destroyed inside the DLL so that it is guaranteed that allocation and deallocation happens through the same allocator.

Well, for my project there is no reason to wrap everything in a C api since it is all C++ code.

But within this vein I did a bit more exploring based on @JacobCrabill’s zig build and I realized I was building yaml-cpp as a static library instead of a dynamic one. When I go back and compare the dynamic library with otool for my project build with my Makefile versus with the zig build.

Build with Makefile

@rpath/libinputs.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libyaml-cpp.0.8.dylib (compatibility version 0.8.0, current version 0.8.0)
@rpath/libc++.1.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

Build with zig build

@rpath/libinputs.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libyaml-cpp.0.8.dylib (compatibility version 0.8.0, current version 0.8.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

So I am now assuming what is happening is the libc++ is being linked statically into the dynamically linkable libraries, and that this is the cause of the malloc error.

So, is there a way to force libc++ to be dynamically linked into the program instead of statically so I can test this idea?

I have also been able to replicate a similar error in a much simpler project. This issue seems to come up only when I link two shared libraries.

In this project I have two libraries, libfoo, and libber. Each of these contains a single simple class with a default destructor.

The main function only creates an instance of each of these classes. It also has logic to conditionally include each of these classes.

This simple example has the following behavior on my machine

  • Static linkage to both libraries - No issue
  • Dynamic linkage to one library - No issue
  • Dynamic linkage to both - seg fault

The link to this example is here https://github.com/gsgall/boiler-plate-project/tree/39c72221e4f86ce0dfa66dff32cbdc6886720b60 reproduces the issue.

  • Static linkage for both: zig build run
  • Dynamic linkage for libbar: zig build run -Dlinkage=dynamic -Dfoo=false
  • Dynamic linkage for libfoo: zig build run -Dlinkage=dynamic -Dbar=false
  • Dynamic linkage for both: zig build run -Dlinkage=dynamic

Dynamic linkage for both is the one that results in a segfault. Either I think I am doing something wrong with the build system or there is a bug somewhere in the build system since the objects in this example are so simple.

Did you try step-debugging through the main function to check where the segfault exactly happens? I would probably also create a destructor with a log message and set a breakpoint there.

From looking at the simplified code I would expect that actually nothing is heap-allocated (even for the string objects I would expect that they live completely on the stack because of small-string optimization).

But since the strings are passed by value the situation might be more complex. Did you try to pass the strings as const-ref and check if that works? Not as a solution, but as a hint at what might go wrong.

Also did you try replacing the std::cout-stuff with a simple printf (or even __builtin_printf)? As far as I’m aware, std::cout involves global state and that may also complicate the investigation.

(PS: also I’ve never seen a .C extension for C++ files, but I guess that shouldn’t be the problem since the if Zig would try to build those files as C there would be build errors).

Thank you for the advice I think I have narrowed this down to a linker issue.

I have made the suggested changes with const std::string & as the argument for the constructors, this did not change the issue.

I also renamed the files to have the .cpp extension, this didn’t make a difference either (I use that extension since that is the standard for the team I work with).

I also removed the calls to std::cout within the constructor and without those calls I still get the same error. So the constructors are just setting the member variable and doing nothing else.

I took a look in the debugger as well

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000000000000
  * frame #1: 0x000000010002b3f4 main`main at main.cpp:14:20
    frame #2: 0x000000019e196b98 dyld`start + 6076

frame #1: 0x000000010002b3f4 main`main at main.cpp:14:20
   11  	  const auto foo = Foo("foo");
   12  	#endif
   13  	#ifdef BAR
-> 14  	  const auto bar = Bar("bar");
   15  	#endif
   16  	  return EXIT_SUCCESS;
   17  	}

So it is failing on the call to the constructor. Now if I try to set a break point for the constructor I get the following issue

(lldb) b Bar::Bar
Breakpoint 2: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) disassemble -n 'Bar::Bar'
error: Unable to find symbol with name 'Bar::Bar'.

This leads me to believe that this is caused by an error with the linking done by the build system.

Additionally, if I change the order of linking in the build.zig file then the symbol Foo::Foo is the one which is undefined, which also points me towards this being an issue with the linking done by the build system.

Updated code: https://github.com/gsgall/boiler-plate-project/tree/8be0680126bf0e4551d73148b0da98f6c957f2bb

This issue is also consistent across some versions of zig. Specifically I tested this with zig 0.16.0-dev.732+2f3234c76 and 0.15.2.

1 Like