Working with C

One of the great features of Zig is that it is fully compatible with C. Let's return to the Hello, world! example, this time using C to print instead of Zig.

const c = @cImport({
    @cInclude("stdio.h");
});

pub fn main() !void {
    _ = c.printf("Hello, world!\n");
}

That's it! Using the cImport directive, we can directly use C code inside of Zig. Of course, please compile this to verify that it works as expected.

For the rest of this page, we'll be working across multiple files. The filenames will be given in a comment that precedes the source code. Creating a new directory to store all these source files is encouraged, so that it doesn't get too messy.

Custom C code

Let's write some of our own code instead of using the C standard library. Our custom C program will have just a single greet function, that says hello to the name passed as a parameter.

// In file /c-src/greeter.h

void greet(const char *name);

// In file /c-src/greeter.c

#include "greeter.h"
#include <stdio.h>

void greet(const char *name) {
  if (name == NULL) {
    printf("Hello, world!\n");
  } else {
    printf("Hello, %s!\n", name);
  }
}

Let's verify that this works as a C program first. We can create a temporary main file for the C program.

// In file /c-src/main.c

#include "greeter.h"
#include <stdio.h>

int main() {
  greet("Alice");
  greet("Bob");
  greet(NULL);
  return 0;
}

And then compile it using gcc as with any other C program.

$ gcc -o greet c-src/greeter.c c-src/main.c  
$ ./greet
Hello, Alice!
Hello, Bob!
Hello, world!

It works! So how do we get gcc to work with Zig? Ah! We unveil the secret behind Zig's ability to handle C code so well. It comes with a C compiler :O

$ zig cc -o greet c-src/greeter.c c-src/main.c  
$ ./greet
Hello, Alice!
Hello, Bob!
Hello, world!

Okay, if you're cheeky and try doing zig cc --help, you might discover that it's actually Clang (on a Macbook at least). But the creator of Zig actually wrote about this, and TL;DR it is Clang but smaller and does more out-of-the-box.

Insane! Let's continue by trying using this C code in Zig instead of just C. We can start by modifying our build file to look for greeter.h in the correct place.

// In file /build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "greet",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });
    exe.addIncludePath(b.path("c-src"));

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Now, we can configure our main.zig file to import our custom greeter.c.

// In file /src/main.zig

const c = @cImport({
    @cInclude("greeter.c");
});

pub fn main() !void {
    c.greet("Alice");
    c.greet("Bob");
    c.greet(null);
}

We use the zig build run command instead of zig run, and we can see that everything works well!

$ zig build run
Hello, Alice!
Hello, Bob!
Hello, world!

Last updated