@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.h and c.zig filenames are a personal choice, not a requirement. What matters is that the @import line uses the filename that zig translate-c created.
  • c.zig must 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 #include lines in c.h and run zig translate-c once.
  • The generated c.zig depends on the operating system and the CPU architecture. A c.zig that 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!