The Many Ways of Doing C Interop With Zig
Treat this post more as note-taking of all the different ways I have done C interop with Zig, this is by no means a complete guide.
I'm going to cover 4 different methods, starting with the method I use the least, and finishing with my most preferred method.
The Dumb Way
I call it the dumb way because it is by far the least scalable and "correct" way of doing it. This method essentially consists of rolling your own extern function declarations in Zig, and then linking it against the C library you want to interop with. I'm sure there are usecases where this is the best/only approach, I just have not encountered one yet. Below is a minimal example.
int add(int a, int b) {
return a + b;
}const lib = b.addLibrary(.{
.name = "my_lib",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
}),
});
lib.root_module.addCSourceFile(.{
.file = b.path("lib/my_lib.c"),
});
const mod = b.addModule("interop", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
mod.linkLibrary(lib);const std = @import("std");
extern fn add(a: c_int, b: c_int) c_int;
test "add" {
try std.testing.expectEqual(3, add(1, 2));
}Using the translate-c CLI
This method consists of running the translate-c CLI tool on your C source files and generating a Zig file that contains the bindings for the C functions and types. Unfortunately, due to lack of good documentation and needing to memorise a CLI API, it tends to be more time consuming than the upcoming methods. However, one strong advantage of this method is that it allows you to directly modify the generated Zig source code, which can be useful for debugging and cases where translate-c outputs incorrect code.
$ zig translate-c lib/src/my_lib.c -I lib/include > src/c.zigWe can actually inspect the generated Zig code and find our add function we created. Although not all C code can be translated directly to Zig, translate-c does a good job of handling most common cases.
pub export fn add(arg_a: c_int, arg_b: c_int) c_int {
var a = arg_a;
_ = &a;
var b = arg_b;
_ = &b;
return a + b;
}Lets say we have a libc dependency (e.g including stdio.h), we can link with libc using the -lc flag.
$ zig translate-c -lc lib/src/my_lib.c -I lib/include > src/c.zig@cImport
and @cInclude
This is by far the easiest way to interop with C code in Zig, the Zig documentation even includes a section on how to use these functions, see Import from C Header File. This is quite a convenient way of interoping with C as it uses translate-c to generate Zig bindings for C functions and types defined in the header files we include. We can port the previous example to use this method.@cImport@cIncludeint add(int a, int b);lib.installHeadersDirectory(b.path("lib/include"), "", .{});
lib.root_module.addCSourceFile(.{
.file = b.path("lib/src/my_lib.c"),
});const std = @import("std");
const c = @cImport({
@cInclude("my_lib.h");
});
test {
try std.testing.expectEqual(3, c.add(1, 2));
}const builtin = @import("builtin");
const c = @cImport({
@cDefine("NDEBUG", builtin.mode == .ReleaseFast);
if (something) {
@cDefine("_GNU_SOURCE", {});
}
@cInclude("stdlib.h");
if (something) {
@cUndef("_GNU_SOURCE");
}
@cInclude("soundio.h");
});usingnamespace keyword to cleanly isolate this. However, with the removal of usingnamespace in Zig 0.15, this approach is no longer possible and will instead lead to you needing a top-level namespace. As we will see in the next method, there is an alternative approach which resolves the issues with this method.b.addTranslateC
and C code as a module
This is my preferred approach, the underlying idea here is to translate our C code in our build.zig, create a module out of it, and import it in our source code. It can be done in the following way:b.addTranslateCconst my_lib = b.addTranslateC(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("lib/include/my_lib.h"),
});
const my_lib_mod = my_lib.createModule();
my_lib_mod.addCSourceFile(.{
.file = b.path("lib/src/my_lib.c"),
});
const mod = b.addModule("interop", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/root.zig"),
});
mod.addImport("my_lib", my_lib_mod);const std = @import("std");
const my_lib = @import("my_lib");
test {
try std.testing.expectEqual(3, my_lib.add(1, 2));
}