DEV Community


Posted on • Originally published at on

Interaction with C in Zig

From 1972, C became more and more important in the underlying world. Many projects are based on C, including the famous operating system: Linux; the multi-platform toolkit for creating GUI: GTK; "your friend": Ruby and many others great software. C also introduces the pointer, I especially point out is because there has several pointer ways at that time, but the one used by C is the winner, and most people only know this version right now: at before, the pointer we have usually directly use size rather than object as its value, which means a pointer p which points to location 1, after using the p++ would point to location 2; in C, the one we get used to, would depend on what is the object the p points to; that's why x[i] and *(x + i) are equivalent. From The C programming language:

As formal parameters in a function definition, char s[]; and char *s are equivalent.

C is an unbelievable simple language and makes so many great stuff, I think I can say it's a great language. But is not enough nowadays, let's take a look at a few examples:

  • where is my object?
  struct object *new_object() {
    struct object *obj = {};
    return obj;
Enter fullscreen mode Exit fullscreen mode
  • so, char s[] and char *s are equivalent?

We have a global here:

  char s[10];
Enter fullscreen mode Exit fullscreen mode

Can I use

  extern char *s;
Enter fullscreen mode Exit fullscreen mode

to operate the origin one? No, but the following are the same

  void function(char s[]);
  void function(char * const s);
Enter fullscreen mode Exit fullscreen mode
  • how to use boolean operations in C?
  if (1);
  if (0);
Enter fullscreen mode Exit fullscreen mode

How about: if (0x63)?

  • Can I point to that address?
  char s[10];
  // so what is c?
  char c = *(s + 10);
Enter fullscreen mode Exit fullscreen mode

Finally, we know how to kill ourself by creating a global thermonuclear war, or because we don't know?


We make many tools and define many rules to avoid to make those errors in C. Like MISRA C, model checking and more. Some of them are palliative, like define programming rules (unless that rule can be checked by linter, else is usually not really helpful), but some of them are very nice; here my point is focusing on the tool like F*.

What I'm going to introduce is Zig. The first language which I really think it can be a replacement of C. The reason I think Zig is better is the concept of compile-time in Zig. Which means we can get the benefit from pre/post-condition checking. Let's have an example to know what's that means:

fn check_upper_case_name(comptime str: []const u8) void {
    comptime {
        var i = 0;
        while (i < str.len) {
            if (str[i] >= 'a' and str[i] <= 'z') {
                @compileError("must be all uppercase");
            i += 1;
// so if we typed `check_upper_case_name("apple")`
// would cause a compile error
Enter fullscreen mode Exit fullscreen mode

This is a very powerful feature, but it's not enough to be a replacement of C. Rust has more features than C in the programming language designing view. It's not about performance, many languages can be faster than C(depends on the field of CS). The Go even as simple as C. But they cannot be a replacement of C. Because the interaction way is not simple enough, even have limitations (this is quite normal, it's caused by different languages design, I have mentioned how cgo makes trouble,
cgo helps we link binary multiple times and must set up the linker flags multiple times, what a good idea!). Others language use FFI, in Rust we have to write:


#[link(name = "snappy")]
extern {
    fn snappy_max_compressed_length(source_length: size_t) -> size_t;
Enter fullscreen mode Exit fullscreen mode

Everything looks good, but FFI has a common limitation(I think the language use this model would have this problem): we cannot access the #define in C. You know, we can access macro in C is already confuse people and thought it is just a variable(but it's not). But C programs use #define for many things, like error number. So we are required to do so:

const A_C_ERROR_XXX: u32 = 1;
const A_C_ERROR_XXX2: u32 = 2;
const A_C_ERROR_XXX3: u32 = 3;
Enter fullscreen mode Exit fullscreen mode

Hope you enjoy this process(or normally we would make a code generator and found the header location was changed after some versions).

Zig does a lot on making the interaction more convenience, that's why it can be the replacement of C. Not joking, let's see how to access existing C code from Zig.


const c = @cImport({
Enter fullscreen mode Exit fullscreen mode

We can even access macro:

fn netlink_err(err: c_int) !void {
    // error code is negative, we must convert it back first, so we use `-err`
    switch (-err) {
        c.NLE_SUCCESS => {},
        c.NLE_FAILURE => return NetLinkError.FAILURE,
        c.NLE_INTR => return NetLinkError.INTR,
        c.NLE_BAD_SOCK => return NetLinkError.BAD_SOCK,
        c.NLE_AGAIN => return NetLinkError.AGAIN,
        c.NLE_NOMEM => return NetLinkError.NOMEM,
        c.NLE_EXIST => return NetLinkError.EXIST,
        c.NLE_INVAL => return NetLinkError.INVAL,
        c.NLE_RANGE => return NetLinkError.RANGE,
        c.NLE_MSGSIZE => return NetLinkError.MSGSIZE,
        c.NLE_OPNOTSUPP => return NetLinkError.OPNOTSUPP,
        c.NLE_AF_NOSUPPORT => return NetLinkError.AF_NOSUPPORT,
        c.NLE_OBJ_NOTFOUND => return NetLinkError.OBJ_NOTFOUND,
        c.NLE_NOATTR => return NetLinkError.NOATTR,
        c.NLE_MISSING_ATTR => return NetLinkError.MISSING_ATTR,
        c.NLE_AF_MISMATCH => return NetLinkError.AF_MISMATCH,
        c.NLE_SEQ_MISMATCH => return NetLinkError.SEQ_MISMATCH,
        c.NLE_MSG_OVERFLOW => return NetLinkError.MSG_OVERFLOW,
        c.NLE_MSG_TRUNC => return NetLinkError.MSG_TRUNC,
        c.NLE_NOADDR => return NetLinkError.NOADDR,
        c.NLE_SRCRT_NOSUPPORT => return NetLinkError.SRCRT_NOSUPPORT,
        c.NLE_MSG_TOOSHORT => return NetLinkError.MSG_TOOSHORT,
        c.NLE_OBJ_MISMATCH => return NetLinkError.OBJ_MISMATCH,
        c.NLE_NOCACHE => return NetLinkError.NOCACHE,
        c.NLE_BUSY => return NetLinkError.BUSY,
        c.NLE_PROTO_MISMATCH => return NetLinkError.PROTO_MISMATCH,
        c.NLE_NOACCESS => return NetLinkError.NOACCESS,
        c.NLE_PERM => return NetLinkError.PERM,
        else => return NetLinkError.Unknown,

const NetLinkError = error{
Enter fullscreen mode Exit fullscreen mode

Yes, NLE_FAILURE is macro.

But we cannot just take a look at how to code it, right? How about the build system? Zig use Zig as build script, and already has several helpful functionalities! Here is an example(file should be named build.zig):


const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const exe = b.addExecutable("test", "test.zig");


Enter fullscreen mode Exit fullscreen mode

We use test.zig to build an executable, link to C libraries c, nl-3 and nl-route-3. Also, add an include directory for headers searching. All of them are the feature we exactly need when building a project works with C.


Now we got the idea why Zig is a proper replacement of C, so we should use it right now? For me is not, it points out the way we can move to. But itself is not complete enough for a production project, for example I haven't saught a package manager for Zig, seems like still in discussion. So we can take a sit, and observe the future of Zig. Anyway, I really hope it can be successful. How about you?

Top comments (0)