How to expose API for C and Zig users

I am writing a static library in Zig that I want to expose to both C and Zig users. The function signature looks something like this:

pub fn myZigFunc(alloc: std.mem.Allocator) ![]u8 {
    //...
}

For myFunc, the returned slice will be allocated with the allocator. This means it will be trivial for Zig users to work with this function (i.e. they can free the slice, reallocate a bigger slice, etc.)

However, I’m not sure how to expose this same function signature for a C user. My first thought was to wrap this function in some sort of exported code that could pass in an allocator that would imitate malloc. That way, C users could use their respective functions (free, realloc, etc):

export fn myCFunc() [*c]u8 {
    const hypothetical_allocator = // Hypothetical allocator goes here
    const slice = myZigFunc(hypothetical_allocator) catch return null;
    return @ptrCast(@alignCast(slice.ptr));
}

Any ideas on how to accomplish this interoperability between a Zig allocator and C malloc?

std.heap.raw_c_allocator? It’s just an Allocator wrapper around libc’s malloc/free.

1 Like

Good find! I added your recommendation, and it compiles and runs.

This next question probably needs to go in it’s own thread, but I’ll ask anyway. Here’s the code with your recommendation:

export fn myCFunc() [*c]u8 {
    const slice = myZigFunc(std.heap.raw_c_allocator) catch return null;
    return @ptrCast(@alignCast(slice.ptr));
}

In order to compile this code, I add a hand-written header file:

extern char *myCFunc ();

When I compile a demo executable with the resulting library, I get this warning:

gcc demo-anyline.c -o demo-anyline libanyline.a
/usr/bin/ld: warning: root.o: missing .note.GNU-stack section implies executable stack
/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker

When I run the executable finishes running, it terminates with this error message:

fish: Job 1, './demo-anyline' terminated by signal SIGSEGV (Address boundary error)

As a sanity check, I found a working library and linked it’s respective archive (.a) and header files into my executable and it terminates just fine.

Any ideas why the exported code above seg faults?

Hmm. The missing .note.GNU-stack warning can be worked around by adding -Wl,-z noexecstack to the gcc command where you link to the library.

I’m on a musl, not glibc system at the moment, so I can’t do any more involved testing right now, but the following minimal demo works on my system:

// rawc.zig
const std = @import("std");
export fn myCFunc() [*c]u8 {
    const slice = std.heap.raw_c_allocator.alloc(u8, 64) catch return null;
    return @ptrCast(@alignCast(slice.ptr));
}
// main.c
#include <stdlib.h>
char *myCFunc(void);
int main(void) {
    free(myCFunc());
}
$ zig build-lib rawc.zig -fPIE -lc
$ gcc main.c -o demo ./librawc.a
$ ./demo
$ # no segfault

I think you also need to specify that you want glibc and not musl to Zig. You can do that by defining the target (eg x86_64-linux-gnu or native-linux-gnu).

Oh, if that’s the case then it would certainly explain why it’s working on my system. I was under the impression that the default was the system libc though, unless otherwise specified.

That’s correct - the default is system libc if no target is specified.

However, the moment you specify a target, the host system is not relevant anymore - you are cross compiling. In such case the default libc for Linux is static musl.

Interesting… I copied your example and sure enough, it doesn’t segfault anymore. When I go back to my actual static library, even with -Dtarget=x86_64-linux-gnu, I still get the segfault.

I must be missing a subtlety. I’ll start a new thread instead of continue to pollute this one. Thanks for the knowledge, everyone!