Blog: Understanding How Zig and C Interact

Hello everyone!

I’m a newcomer to Zig and am absolutely loving it. I wanted to explore using C libraries in Zig and what it means to creating a binding to a C library. In this post I cover a variety of topics, such as using zig cc to compile pure C code, compiling a static and shared library, linking to libraries and writing wrapper functions around C functions to make your C library more Zig friendly!

Please feel free to give some feedback, as I’m sure there’s plenty to learn. Thank you for your time!

Link to Github

9 Likes

Hey, nice post, just a small suggestion, I learned that the hard way before, but you don’t need to extern define C functions with Zig C types, you can just extern define the C functions, with coercible Zig type equivalent and let the compiler do the rest so for example in your little example.

const zmath_h = @cImport(@cInclude("zmath.h"));

pub extern fn add(a: c_int, b: c_int) c_int;
pub extern fn sub(a: c_int, b: c_int) c_int;

this wrapping can simply be done like this

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

pub extern fn add(a: i32, b: i32) callconv(.C) i32;
pub extern fn sub(a: i32, b: i32) callconv(.C) i32;

I’m not even sure the callconv(.C) is necessary, because it works without too, but point being that C type that can safely coerce to Zig equivalent, are always correct. At least that’s my understanding, as it was recommended to me before is to basically not use the c_types directly as they are there just for the automatic translation and shouldn’t be used by us directly if I’m not mistaken.

2 Likes

That’s true, but it mostly used for pointers. So , e.g., for a [*C]T that can only point to a single T and can never be null, you should use *T. For integers this is complicated because C allows these types to change depending on target and compiler. In some cases, c_int won’t translate to a i32, so in these cases, it’s better to use the C types.

It is, because Zig reserves the right to use any calling convention when not instructed to use one in particular, which could improve performance. Currently, I think it doesn’t actually leverage this, and always use the platform ABI.

4 Likes

Oh yes absolutely, I completely forgot about the fact that c_int is dependent on the platform, I was just trying to show that you can “coerce” the function signature to true Zig type directly, meaning you don’t necessarily have to extern define it with c_types and then wrap it inside of a Zig function. But thanks for pointing that, my bad for the bad example.

2 Likes

I see, thanks for your input.

With type coercion, I don’t think I am able to return error types directly from the C function, so I think I’d still need a separate way of wrapping the methods in Zig code. Here’s what I mean:

pub extern fn myCFun(a: i32, b: i32) i32; // this works
pub extern fn myCFun(a: i32, b: i32) !i32; // this doesn't work (ofc)

If the function was any more complex, I think returning an error type is still beneficial, but in the simple example in the blog post its very much overkill. I’ll look more into callconv(.C), as I’ve seen it, but am not too sure what it’s purpose is.

Thank you!

1 Like

Speaking of callconv(), is this required when calling C code from Zig? Looking through zig.guide’s explanation of callconv seems to suggest that it’s only necessary when calling Zig code from C.

If you have any insight, I’d greatly appreciate it. Thanks :slight_smile:

It is. The calling convention is the ABI. When caller and calle are both in Zig, Zig can decide whichever calling convention it wants to use. When interacting with C, either calling or being called, there needs to be a well-defined and explicit ABI, otherwise there’s no guarantee that both functions are speaking the same language.

4 Likes