One of the best things about Zig is its easy interoperability with C code.
There’s no need to build complex wrappers or bindings: you can just include C header files and
start using C structs and functions.
Setup
To include a .h
file in Zig, you can just use @cImport
like the following:
const c = @cImport({
@cInclude("example.h");
});
Just make sure the file is in the include paths. If you’re using the build.zig
:
const exe = b.addExecutable(.{
...
});
exe.addIncludePath(.{ .cwd_relative = "./src/" });
Alternatively, you can run zig translate-c <file.h> -I <include path> -lc > c.zig
to generate a .zig
file with the definitions of the C program.
Then you can just import it as you do with a regular .zig
file.
To compile, you probably also need to link with the library, and possibly also with libc
:
exe.linkSystemLibrary("x11");
exe.linkLibC();
Structs
If you’re linking with libc, and you’re using its fixed-width types, Zig will be able to infer and map to the correct primitive types. For example, this:
#include <stdint.h>
struct Rectangle {
uint32_t width;
uint32_t height;
};
translates to this (using translate-c
):
pub const struct_Rectangle = extern struct {
width: u32 = @import("std").mem.zeroes(u32),
height: u32 = @import("std").mem.zeroes(u32),
};
You may have noticed the extern struct
. Why is that?
Well, ordinary struct
s in Zig don’t have a well-defined memory layout: their size and the order of their fields are not guaranteed to always be the same.
This is done for optimization purposes.
extern struct
s, however, are compatible with the C ABI, so we are required to use them when interfacing when C code (and that should be the only reason).
Also, the struct name is prefixed with struct_
to respect C namespaces (it also does it for union
s and enum
s).
If you typedef
it, the correct name will be generated:
typedef struct Rectangle Rectangle;
pub const Rectangle = struct_Rectangle;
So… what if we make a function called struct_Rectangle
?
Here Zig will mangle one of the names - I guess it always mangles the struct as function names are important when linking.
In this case, I get:
pub const struct_Rectangle_4 = extern struct { ... };
pub const Rectangle = struct_Rectangle_4;
Usage
You can use these structs like any other struct:
const std = @import("std");
const c = @cImport({
@cInclude("example.h");
});
test "test-rectangle" {
const rect: c.Rectangle = .{
.width = 42,
.height = 420,
};
std.debug.print("{}", .{rect});
}
This will print:
cimport.struct_Rectangle{ .width = 42, .height = 420 }
Non-fixed size types
If you’re not linking with libc or not using fixed-width types, like in the following,
struct Rectangle {
int width;
int height;
};
then Zig cannot infer their sizes, because they depend on the compiler and the target.
Zig has C primitive types for this situation 1.
This translates to:
pub const struct_Rectangle = extern struct {
width: c_int = @import("std").mem.zeroes(c_int),
height: c_int = @import("std").mem.zeroes(c_int),
};
where c_int
is a C primitive type. The test code from before still works.
While the size of the c_*
fields is not fixed, you can still get their max and min values (similar to INT_MAX
, etc. in C):
const min = std.math.minInt(c_int);
const max = std.math.maxInt(c_int);
The compiler will also make sure you respect those limits:
const m: c_uint = std.math.maxInt(u32); // This compiles (on my machine)
const n: c_int = std.math.maxInt(u32); // This doesn't (on my machine)
Functions
If you declare a C function in a header file and its definition in a .c
file,
Zig will translate its declaration and mark it as extern
, so it will be resolved at link-time (or runtime when dynamic linking).
int calculate_area(struct Rectangle rectangle);
pub extern fn calculate_area(rectangle: struct_Rectangle) c_int;
Interestingly, if you define a function in the header file, Zig will convert it to native Zig code!
So it will correctly “copy” its definition as expected from functions declared in header files.
It will then mark it with export
to make it available to other code,
which will cause “multiple definition” errors as expected in C.
int calculate_area(struct Rectangle rectangle) {
return rectangle.width * rectangle.height;
}
pub export fn calculate_area(arg_rectangle: struct_Rectangle) c_int {
var rectangle = arg_rectangle;
_ = &rectangle;
return rectangle.width * rectangle.height;
}
An inline function will also be converted to Zig code, however, the export
keyword will be dropped,
and a callconv(.c)
will instruct the compiler on which calling convention to use when calling the function, in this case cdecl
.
In reality, functions marked with export
or extern
will have the C calling convention by default,
that’s why it only explicitly specified it now.
inline int calculate_area(struct Rectangle rectangle) { ... }
pub fn calculate_area(arg_rectangle: struct_Rectangle) callconv(.c) c_int { ... }
Zig has an inline fn
keyword, but its meaning is different from C,
that’s why the generated function has no inline
.
The Zig compiler may decide inline functions (in the C sense) without any explicit hint.
Pointers
Another nice thing about Zig is pointers; we have two kinds of pointers:
- pointer to a single item
*T
- pointer to multiple items (but of unknown number)
[*]T
null
pointers are not allowed: we just use optionals ?*T
.
An array, however, is a container of n
elements (not technically a pointer), where n
is the length of the array [n]T
.
This can be coerced to a [*]T
, but that way we lose its length, just like in C when we pass an array to a function.
We can also use a slice []T
though, which is basically a fat-pointer containing:
- a pointer
[*]T
- the length of the slice
C Pointers
When interfacing with C, however, we don’t have that luxury. Zig just cannot know what a pointer points to.
Zig uses C pointers [*c]T
when interfacing with C; again, we should use these for this reason only.
This type of pointer can be null
.
If we change our function to take a pointer to the struct, we get:
int calculate_area(struct Rectangle* rectangle);
pub extern fn calculate_area(rectangle: [*c]struct_Rectangle) c_int;
Note that if we change the function’s argument to be a constant pointer (struct Rectangle const *
), the generated code will still be the same.
Both in C and in Zig, arguments are passed by value, so any change to an argument is only effective inside the scope of the function.
But in C we can declare pointer arguments as constant: this means the pointer will not be changed inside the function;
this is not really useful anyway as in any case we would only be mutating a copy of the pointer.
In Zig we don’t have constant arguments, so the generated signature is the same.
A pointer to constant struct instead is converted as expected:
int calculate_area(const struct Rectangle *rectangle);
pub export fn calculate_area(arg_rectangle: [*c]const struct_Rectangle) c_int;
Using C Pointers
You can pass a single-item pointer to a C function as you do with any other pointer type:
const rect = c.struct_Rectangle{ .width = 17, .height = 10 };
const area = c.calculate_area(&rect);
You can pass an array by taking its reference as well.
This is nicer than C I think, since in C the array to pointer decay is automatic, except for sizeof
and &
.
In Zig this is explicit and we also conveniently have the .len
field.
const rects = [_]c.struct_Rectangle{ rect, rect };
const total_area = c.calculate_areas(&rects, rects.len);
Slices are pointers, so no need to take a reference:
const rects_slice = rects[0..rects.len];
const total_area = c.calculate_areas(rects_slice, rects_slice.len);
Casting C Pointers
C Pointers in Zig support the access syntax of both single- and multi-item pointers. So you can do:
// This is a [*c]cimport.struct_Rectangle
const rect_ptr = c.create_rectangle();
const rect = rect_ptr.*; // Access it as a single-item pointer
const rect = rect_ptr[0]; // Access it as a multi-item pointer
The Zig standard library and any other library will use Zig pointers instead of C Pointers, so you may want to convert them. This can easily be done as C Pointers coerce to Zig pointers:
const rect: *c.Rectangle = rect_ptr; // Cast to single-item pointer
const rects: [*]c.Rectangle = rect_ptr; // Cast to multi-item pointer
But for arrays of which you know the length, you probably want to cast it to a slice:
const rects_ptr = c.make_rectangles(42);
const rects: []c.Rectangle = rects_ptr[0..42];
for (rects) |rect| { // No need to carry around the array length as we do in C
std.debug.print("{}\n", .{rect});
}
Function pointers
struct Rectangle {
...
int (*area)(const struct Rectangle *);
};
We can initialize a function pointer normally, but we need to check for a null
pointer when calling it.
const rect: c.Rectangle = .{
.width = 10,
.height = 20,
.area = c.rectangle_area, // area is a function pointer
};
const area = rect.area.?(&rect); // area might be a null pointer
You can also declare a function in Zig and assign that to the pointer.
Just make sure to mark it with callconv(.c)
and to use the C Pointer type if necessary.
fn rectangle_area(rect: [*c]const c.Rectangle) callconv(.c) c_int {
return rect.*.width * rect.*.height;
}
Macros
You can define a constant for the imported C code in a similar way you do in C using @cDefine
. For example,
#define N 200
const c = @cImport({
@cDefine("N", "200");
@cInclude("example.h");
});
You can also undefine them by using @cUndef
.
Unfortunately, calling C macros doesn’t work in Zig, even though Zig tries its best to translate them to Zig functions - and most times it actually succeeds.
If Zig cannot translate a macro, it will fall back to a @compileError
.
Note that this doesn’t have an impact on the C side of things, it just means we cannot use them from Zig.
Here are some examples of macros that don’t work, kindly taken from here and here.
#define Warning(...) fprintf(stderr, __VA_ARGS__)
#define RANGE(i, y, x) \
for (i = (y); (((x) >= (y)) ? (i <= (x)) : (i >= x)); \
(((x) >= (y)) ? ((i)++) : ((i)--)))
#define SWAP(a, b) \
do { \
a ^= b; \
b ^= a; \
a ^= b; \
} while (0)
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
As you can see these are pretty basic, so if your C library expects you to use macros as part of its public interface, you should wrap them in one or more functions if you can, or use Zig comptime to port them to Zig.