How to use zig from c

Hello, can someone provide a minimal example of how to export a function in zig and link it statically to the main c-program where it then gets called from? I couldnt find any info about how to do this anywhere.

I also wanna export a struct too btw

Somehow I can’t -femit-h to get to work to output a C header, but basically:

bla.zig:

const Bla = extern struct {
    a: i32,
    b: i32,
    c: i32,
};

export fn get_bla(a: i32, b: i32, c: i32) Bla {
    return Bla{ .a = a, .b = b, .c = c };
}

…compile into a library via zig build-lib bla.zig.

Result shoul be a libbla.a.

main.c:

#include <stdio.h>

typedef struct {
    int a, b, c;
} Bla;

extern Bla get_bla(int a, int b, int c);

int main() {
    const Bla bla = get_bla(23, 45, 67);
    __builtin_dump_struct(&bla, printf);
}

…compile the C source and link with the libbla.a:

zig cc zig cc main.c libbla.a -o main

…you should see an exe main, run that and:

> ./main                                                                                                                           
Bla {
  int a = 23
  int b = 45
  int c = 67
}

…the missing piece is generating a C header with public declarations instead of declaring the C struct and extern function manually in the C code, normally there’s an -femit-h cmdline arg for that, but either I’m not using it right, or it currently doesn’t work.

1 Like

Thank you for your help!

could you provide a minimal build script that compiles this? thats the main part i cant figure out

Btw, -femit-h currently is expected to not work apparently:

2 Likes

A concrete build.zig is left as an excercise to the reader :wink:

Basically:

    const exe = b.addExecutable(.{
        .name = "bla",
        .target = target,
        .optimize = optimize,
        link_libc = true,
    });

…then add the main.c and the library:

    exe.addCSourceFile(.{ b.path("main.c") });
    exe.linkLibrary(lib);
2 Likes

Will try this later, thank you!

As a second semester college student the first sentence feels oddly familiar haha

I still does not work and I get the same error as before:

error: ld.lld: undefined symbol: hello_test()
    note: referenced by PriorityQueue_zig_performance.cpp:4 (extra/PriorityQueue_zig_performance.cpp:4)

my build file section responsible for this is

    const pq_mod = b.addModule("PriorityQueue", .{
        .root_source_file = b.path("extra/PriorityQueue_zig_export.zig"),
        .target = target,
        .optimize = optimize,
    });
    const pq_lib = b.addLibrary(.{
        .name = "PriorityQueue",
        .root_module = pq_mod,
    });

    {
        const exe = b.addExecutable(.{
            .name = "zig_perf",
            .target = target,
            .optimize = optimize,
        });
        exe.addCSourceFile(.{
            .file = b.path("extra/PriorityQueue_zig_performance.cpp"),
        });
        exe.linkLibCpp();
        exe.linkLibrary(pq_lib);

        b.installArtifact(exe);
        b.installArtifact(pq_lib);
    }

my zig file is

const std = @import("std");

export fn hello_test() callconv(.C) void {
    std.io.getStdOut().writeAll("hello, world\n") catch unreachable;
}

and my cpp file is

extern void hello_test();

int main() {
    hello_test();
    return 0;
}

dont get confused by all the priorityqueue names, i wanna try to use the zig priority queue from c++ but first get a little demo working

This problem has to do with C++'s name mangling.

Since extra/PriorityQueue_zig_performance.cpp has the file ending cpp, the buildsystem assumes that this is a C++ file (and I guess you want it to be a C++ file too).

But this means that your

extern void hello_test();

will be assumed to be a C++ too.

So you need to tell your compiler to treat it like a C function, so that the compiler doesn’t apply name mangling to the declaration and make the linker capable of finding it.

You can do that like this:

extern "C" {
    extern void hello_test();
}

If you look up the object file, the mangled name would be (on my Linux x86_64 system) _Z10hello_testv instead of the hello_test that you want.

4 Likes

OMG it works!!!

theres no way i couldve figured that out by myself, thank you so much

we have to use cpp for university and nothing made me appreciate zig more than using cpp :sob:

Tbf, you asked about C, not C++ :wink:

In my example code, if you rename main.c to main.cc (e.g. so that it is compiled as C++), you also need to change:

extern Bla get_bla(int a, int b, int c);

to:

extern "C" Bla get_bla(int a, int b, int c);

…that way the C++ compiler knows that this is an external C symbol which doesn’t use name mangling.

2 Likes

Yea I admit that I should have expressed myself more clearly, although I find it strange that this happened because with the exception of a single keyword, people always claim that all c is valid cpp

…only people who didn’t follow the development of C and C++ since around the mid-1990s :wink:

C and C++ are different languages and have diverged significantly since C99 to a point where C99 code simply doesn’t compile anymore in C++ mode.

There are also plenty of differences even when trying to build C89 code. For instance union type punning is valid in C, but UB in C++.

It is possible to write a dialect that compiles both in C and C++ (e.g. the ā€œcommon C/C++ subsetā€), but this ā€œbastard dialectā€ is not standard C.

2 Likes

The more you know, I’ll definitely tell some of my c friends about this!

ā€˜C++ friends’ most likely :wink: I’ve seen this mindset typically with C++ peeps who haven’t kept up with C development, never with actual C programmers.

1 Like

No actually, the person in mind is really not a fan of cpp and I myself didnt know about this too, which is why I find it so surprising. The idea of cpp being a superset has to be one of the biggest commonly believed myths ive fallen a victim to until today

Let me tell you, C++ programmers aren’t the only people working with C who don’t keep up with C’s development. There’s a ton of people working with C89/99 because of platform compatibility.

C89 vs C99 is exactly where I draw the line between ā€œoldā€ and ā€œnewā€ C, since C99 was a significant language update via designated init and compound literals. Everything after C99 was more or less just minor updates.

Especially the designated init feature in C99 still stands out as one of the most well-designed ā€˜afterthought’ language features in any language. Every little detail just clicks, and most more recent languages (including Zig unfortunately, and not to mention the totally botched designated-init thing in C++20) miss one or another minor feature which might look unimportant on its own but is missing dearly when coming from C99.

3 Likes

I agree with you that C89 to C99 is the biggest language schism, but I’d also balk at calling a language standard older than some of the developers working with it ā€œnewā€. :wink:

I draw the line right after C99. C99 is usually supported sans a few major features we all know are poorly supported. Anything after is the ā€œcheck your compiler documentation for supported featuresā€ part of C.

(Either way I don’t want to turn this thread into a religious war about C standards, lord knows there’s been enough of those.)

1 Like