Skip to content

Calling C

One of Zig's design goals is direct interoperability with C.

One of Zig’s design goals is direct interoperability with C.

A Zig program can call C functions without wrapper generators, external build tools, or foreign function interfaces layered on top of the language. Zig understands C calling conventions, C data layout, and C header files directly.

A small example is enough to begin.

Suppose we have this C source file:

#include <stdio.h>

void greet(const char *name) {
    printf("hello, %s\n", name);
}

Save it as greet.c.

Now write a Zig program that calls it:

const std = @import("std");

extern fn greet(name: [*:0]const u8) void;

pub fn main() void {
    greet("zig");
}

Build and run:

zig run main.zig greet.c

The output is:

hello, zig

The important line is the declaration:

extern fn greet(name: [*:0]const u8) void;

extern says the function is defined outside Zig.

The declaration gives Zig enough information to call the function correctly:

  • the function name
  • the parameter types
  • the return type
  • the calling convention

The parameter type:

[*:0]const u8

is a many-item pointer terminated by zero. This matches a C string:

const char *

Zig does not have a separate built-in string type compatible with C. Instead, the programmer states the exact memory representation.

A Zig string literal:

"zig"

already contains a trailing zero byte when needed for C interoperation, so it can be passed directly here.

C functions that return integers work naturally.

This C source:

int add(int a, int b) {
    return a + b;
}

can be called from Zig:

const std = @import("std");

extern fn add(a: c_int, b: c_int) c_int;

pub fn main() void {
    const result = add(3, 4);
    std.debug.print("{d}\n", .{result});
}

Run it:

zig run main.zig add.c

The output is:

7

Notice the type:

c_int

Zig provides C-compatible integer aliases:

Zig typeC type
c_charchar
c_shortshort
c_intint
c_longlong
c_longlonglong long

These types match the target platform’s C ABI.

C pointers map naturally into Zig pointer types.

CZig
int **c_int
const char *[*:0]const u8
void **anyopaque

Zig distinguishes several pointer kinds because the language tries to make memory usage explicit.

A normal Zig slice:

[]const u8

contains:

  • a pointer
  • a length

A C string does not contain a length. It depends on a terminating zero byte instead. For this reason, Zig uses different types for the two representations.

C functions may also be declared with explicit calling conventions:

extern "c" fn puts(s: [*:0]const u8) c_int;

The "c" calling convention matches the platform ABI used by C compilers.

In practice, ordinary extern fn declarations already default to the C ABI on supported targets.

A Zig program may link against:

  • standalone C source files
  • static libraries
  • shared libraries
  • operating system APIs

For example:

zig build-exe main.zig greet.c

or:

zig build-exe main.zig -lc

to link against the system C library.

The reverse direction is also possible: C code can call Zig functions.

A Zig function exported to C looks like this:

export fn square(x: c_int) c_int {
    return x * x;
}

export makes the symbol visible to external code.

Zig tries to make interoperability ordinary rather than exceptional. A C function declaration in Zig looks close to its original form, and the generated machine code follows the same ABI rules as a C compiler.