@cImport() Is gone in Zig 0.17
Zig 0.17 removed @cImport(). It was deprecated in Zig 0.16 and now the compiler does not know it at all, so every Zig program that includes a C header file with @cImport() stops compiling. In this blog post, we will take a small program that worked in Zig 0.16, see how it fails in Zig 0.17 and fix it in three small steps.
Before Link to heading
The following program checks whether a file is executable. It calls the access() function of the C library and it needs the X_OK constant, which is defined in the unistd.h C header file. In Zig 0.16, @cImport() made the contents of unistd.h available under the name c.
const std = @import("std");
const c = @cImport(@cInclude("unistd.h"));
// Import the `access()` function from C
extern fn access(path: [*:0]const u8, mode: c_int) c_int;
pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len < 2) {
std.debug.print("Usage: isExecutableC <file>\n", .{});
return;
}
const filePath = args[1];
if (access(filePath, c.X_OK) == 0) {
std.debug.print("{s} is executable\n", .{filePath});
} else {
std.debug.print("{s} is NOT executable or does not exist\n", .{filePath});
}
}
Save the code as isExecutableC.zig and try to run it with Zig 0.17:
$ zig run -lc isExecutableC.zig -- /bin/ls
isExecutableC.zig:3:11: error: invalid builtin function: '@cImport'
const c = @cImport(@cInclude("unistd.h"));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The error message is clear: @cImport() is not a builtin function any more.
The fix Link to heading
In Zig 0.16, the compiler read the C header file for us while it was compiling the program. In Zig 0.17, we do that work ourselves, before compiling, using the zig translate-c command. The command reads a C header file and prints the same declarations as Zig code.
First, create a file named c.h in the same directory as isExecutableC.zig. It needs a single line:
#include <unistd.h>
Second, run zig translate-c once and save its output as c.zig:
$ zig translate-c -lc c.h > c.zig
You do not need to open or edit c.zig. On my macOS machine it is 7433 lines long because it contains everything that unistd.h defines, including the constant that we need:
$ grep X_OK c.zig
pub const X_OK = @as(c_int, 1) << @as(c_int, 0);
Third, replace the @cImport() line of the program with a regular @import() of the new file:
const c = @import("c.zig");
The c.h and c.zig filenames are not mandatory. You can use any filenames you want, as long as the filename that you give to zig translate-c and the filename in the @import line are the same. For example, this works equally well:
$ zig translate-c -lc headers.h > unistd.zig
const c = @import("unistd.zig");
The same applies to the c in const c, which is just the name of a Zig constant.
After Link to heading
This is the complete program for Zig 0.17. Only the third line is different, everything else is the same as before, including the use of c.X_OK.
const std = @import("std");
const c = @import("c.zig");
// Import the `access()` function from C
extern fn access(path: [*:0]const u8, mode: c_int) c_int;
pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len < 2) {
std.debug.print("Usage: isExecutableC <file>\n", .{});
return;
}
const filePath = args[1];
if (access(filePath, c.X_OK) == 0) {
std.debug.print("{s} is executable\n", .{filePath});
} else {
std.debug.print("{s} is NOT executable or does not exist\n", .{filePath});
}
}
Running it with Zig 0.17 produces the expected output:
$ zig run -lc isExecutableC.zig -- /bin/ls
/bin/ls is executable
$ zig run -lc isExecutableC.zig -- /etc/hosts
/etc/hosts is NOT executable or does not exist
Good to know Link to heading
There are four things to keep in mind when you use this approach:
- The
c.handc.zigfilenames are a personal choice, not a requirement. What matters is that the@importline uses the filename thatzig translate-ccreated. c.zigmust be in the same directory as the program that imports it. Zig does not allow importing a file from outside the directory of the program.- If more than one program in the same directory needs C header files, they can all share the same
c.zig. Just put all the#includelines inc.hand runzig translate-conce. - The generated
c.zigdepends on the operating system and the CPU architecture. Ac.zigthat was created on macOS is not valid on Linux, so create it on the machine where you are going to compile the program.
Last, if your project has a build.zig file, the Zig 0.17 release notes recommend adding the official translate-c package as a dependency and doing the translation as part of the build. For single file programs such as the one presented here, the zig translate-c command is all you need.
Happy coding in Zig!