Skip to content

Calling Zig from C

C can call Zig when the Zig function is exported with a C-compatible ABI.

C can call Zig when the Zig function is exported with a C-compatible ABI.

The Zig function:

export fn add(a: c_int, b: c_int) c_int {
    return a + b;
}

export makes the symbol visible outside the Zig object file. The parameter and return types use C-compatible integer types, so C can call the function safely.

Put this in math.zig:

export fn add(a: c_int, b: c_int) c_int {
    return a + b;
}

Now write a C program:

#include <stdio.h>

int add(int a, int b);

int main(void) {
    printf("%d\n", add(3, 4));
    return 0;
}

Build them together:

zig cc main.c math.zig -o main

Run it:

./main

The output is:

7

The C declaration:

int add(int a, int b);

must match the Zig declaration:

export fn add(a: c_int, b: c_int) c_int

The names must match. The argument types must match. The return type must match.

For simple C types, use Zig’s C aliases:

c_char
c_short
c_int
c_long
c_longlong
c_uint
c_ulong

For pointers, export ordinary pointer-shaped data, not Zig-only structures.

This Zig function accepts a C string:

const std = @import("std");

export fn length(s: [*:0]const u8) c_int {
    const n = std.mem.len(s);
    return @intCast(n);
}

C can call it:

#include <stdio.h>

int length(const char *s);

int main(void) {
    printf("%d\n", length("hello"));
    return 0;
}

A C string is not a Zig slice. It is a pointer to bytes ending in zero. That is why the Zig type is:

[*:0]const u8

Do not export a function like this to C:

export fn bad(s: []const u8) void {
    _ = s;
}

A Zig slice is two machine words: pointer and length. C does not know that type unless you define the same layout manually.

Use an explicit C-shaped interface instead:

export fn write_bytes(ptr: [*]const u8, len: usize) void {
    const bytes = ptr[0..len];
    _ = bytes;
}

C calls it as:

void write_bytes(const unsigned char *ptr, size_t len);

Structs can cross the boundary when their layout is fixed.

const Point = extern struct {
    x: c_int,
    y: c_int,
};

export fn sum_point(p: Point) c_int {
    return p.x + p.y;
}

The matching C code:

struct Point {
    int x;
    int y;
};

int sum_point(struct Point p);

The keyword extern struct tells Zig to use C layout rules. A normal Zig struct has layout chosen by Zig and must not be treated as a C struct.

The same rule applies to enums and unions. Use C-shaped declarations at the boundary. Convert to better Zig types inside the Zig code.

A common pattern is:

export fn api_function(raw_ptr: ?*anyopaque) c_int {
    if (raw_ptr == null) return -1;

    // Convert raw C-shaped inputs here.
    // Then call ordinary Zig code.
    return 0;
}

The exported layer should be small. It should check inputs, convert C data into Zig data, and return C-compatible results.

The rest of the program can remain ordinary Zig.