Skip to content

`@cImport`

@cImport is Zig’s built-in way to import C declarations from header files.

@cImport

@cImport is Zig’s built-in way to import C declarations from header files.

A C header file describes functions, structs, constants, enums, and macros. Zig can read that header, translate the declarations, and make them available inside Zig code.

For example, this imports the C standard I/O header:

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

After that, you can call C functions through c:

pub fn main() void {
    _ = c.puts("Hello from C");
}

This is the basic shape:

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

The imported C API becomes available under the name c.

You could choose another name:

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

Then you call:

_ = libc.puts("Hello");

The name is not special. c is just a common convention.

Why @cImport Exists

Many C libraries expose their API through header files.

A header might contain this:

// mathlib.h

int add(int a, int b);
int sub(int a, int b);

Without @cImport, you would need to manually rewrite these declarations in Zig. That would be tedious and error-prone.

With @cImport, Zig can read the C header directly:

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

Then Zig can call:

const x = c.add(10, 20);
const y = c.sub(10, 3);

This reduces duplicate work. The C header remains the source of truth.

@cImport Runs at Compile Time

@cImport happens while Zig is compiling your program.

That is important.

The compiler reads the C header, translates the declarations, and checks your Zig code against those declarations. If you call a C function with the wrong number of arguments or a wrong type, the compiler can often catch it.

For example, suppose C declares:

int add(int a, int b);

This Zig call is correct:

const x = c.add(1, 2);

This call is wrong:

const x = c.add(1);

The function expects two arguments. Zig knows that because it imported the C declaration.

@cInclude

Inside @cImport, you usually use @cInclude.

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

This is similar to writing this in C:

#include <stdio.h>

But in Zig, the include is part of a compile-time block.

You can include more than one header:

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

Now functions from all three headers are available through c.

Example:

const len = c.strlen("hello");
_ = c.puts("done");

@cDefine

Some C headers change behavior depending on preprocessor macros.

In C, you might write:

#define SOME_FEATURE 1
#include "library.h"

In Zig, you can do this:

const c = @cImport({
    @cDefine("SOME_FEATURE", "1");
    @cInclude("library.h");
});

The macro is defined before the header is imported.

This is useful when a C library has optional features controlled by macros.

For example:

const c = @cImport({
    @cDefine("RAYGUI_IMPLEMENTATION", "");
    @cInclude("raygui.h");
});

The exact macro names depend on the C library.

@cUndef

You can also undefine a macro:

const c = @cImport({
    @cDefine("DEBUG", "1");
    @cUndef("DEBUG");
    @cInclude("library.h");
});

This is less common, but it can be useful when a header behaves differently depending on whether a macro exists.

A Complete Example

Suppose we have this C header:

// mathlib.h

#ifndef MATHLIB_H
#define MATHLIB_H

int add(int a, int b);

#endif

And this C source file:

// mathlib.c

#include "mathlib.h"

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

Now we can call it from Zig:

const std = @import("std");

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

pub fn main() void {
    const result = c.add(10, 20);
    std.debug.print("result = {}\n", .{result});
}

This prints:

result = 30

But importing the header is not enough. Zig also needs the implementation from mathlib.c.

The header says the function exists.

The C source file contains the function body.

Your build must include both.

Header Import vs Linking

This is a common beginner mistake.

@cImport imports declarations. It does not magically link the C library.

For example:

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

This lets Zig understand names such as sqlite3_open, sqlite3_close, and sqlite3_exec.

But your final program still needs to link SQLite.

If the library is not linked, the compiler may understand the function call, but the linker will fail because it cannot find the actual compiled function.

The rule is:

@cImport gives Zig the C declarations.
Linking gives the final executable the C implementation.

Both are required.

Imported Names

When Zig imports a C header, the declarations become fields on the imported namespace.

For example:

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

You access imported names like this:

c.puts
c.printf
c.FILE
c.fopen
c.fclose

This keeps C names grouped together. It also makes the boundary obvious. When you see c.puts, you know this call goes to C.

You should usually keep that boundary visible.

Avoid this style:

const puts = c.puts;

It can be convenient, but it hides where the function comes from. In beginner code, prefer c.puts.

C Integers in Zig

C types are not always exactly the same as Zig types.

For example, C int is usually imported as c_int.

A C function like this:

int add(int a, int b);

is usually seen by Zig as something like:

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

So this works:

const result: c_int = c.add(10, 20);

Zig provides C-compatible type names such as:

C typeZig C-compatible type
charc_char
shortc_short
intc_int
longc_long
long longc_longlong
unsigned intc_uint
floatf32
doublef64

Use these C-compatible types at the C boundary.

Inside normal Zig code, you can convert to ordinary Zig types when needed.

C Strings

Many C functions expect null-terminated strings.

For example:

int puts(const char *s);

The string ends when C finds a zero byte.

This Zig call works:

_ = c.puts("hello");

String literals in Zig can be passed to C functions that expect null-terminated strings.

But be careful with ordinary slices:

const msg: []const u8 = "hello";

A slice has a pointer and a length. A C function that takes const char * does not know the length. It keeps reading until it finds a zero byte.

This can be unsafe:

_ = c.puts(msg.ptr);

The pointer alone does not prove that the data is null-terminated.

When working with C strings, keep this distinction clear:

Zig valueMeaning
[]const u8Pointer plus length
[*:0]const u8Pointer to bytes ending in zero
const char * in CPointer to bytes ending in zero

C strings need a sentinel zero byte.

C Structs

C structs can be imported too.

Suppose a header has this:

typedef struct Point {
    int x;
    int y;
} Point;

After importing the header, Zig can use the type:

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

pub fn main() void {
    var p = c.Point{
        .x = 10,
        .y = 20,
    };

    _ = p;
}

The imported struct keeps a C-compatible layout.

This matters because C and Zig must agree on where fields live in memory.

C Enums and Constants

C headers often define enums and constants:

#define MODE_READ 1
#define MODE_WRITE 2

enum Status {
    STATUS_OK = 0,
    STATUS_FAILED = 1,
};

After import, you may access them through c:

const mode = c.MODE_READ;
const status = c.STATUS_OK;

The exact imported form can vary depending on how the C header defines the names. C macros are especially tricky because not every macro maps cleanly to Zig.

Simple numeric macros usually work well.

Complex function-like macros may not.

Macros Are the Hard Part

C macros are not real functions. They are preprocessor substitutions.

Example:

#define SQUARE(x) ((x) * (x))

This may look like a function, but it is not. It is expanded by the C preprocessor before compilation.

Zig can import some macros, especially simple constants, but complex macros may not translate cleanly.

When a macro does not import well, you usually write a small C wrapper function.

For example:

// wrapper.h

int square_int(int x);
// wrapper.c

#define SQUARE(x) ((x) * (x))

int square_int(int x) {
    return SQUARE(x);
}

Then Zig imports the wrapper:

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

const y = c.square_int(9);

This is often cleaner than fighting with complicated C macros.

Include Paths

If the header is in the same directory, this may work:

@cInclude("mathlib.h");

If the header is somewhere else, the build must tell Zig where to search.

In build.zig, that usually means adding an include path to the compile step.

Conceptually:

Tell Zig where the header files are.
Then @cInclude can find them.

If Zig cannot find the header, you will get an error similar to:

'header.h' file not found

The fix is not inside @cImport itself. The fix is usually in the build configuration.

Keep C Imports Small

A common pattern is to put all C imports in one Zig file.

For example:

// c.zig

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

Then other Zig files can import that file:

const c = @import("c.zig").c;

This keeps C imports centralized.

For small programs, putting @cImport directly in main.zig is fine.

For larger programs, a dedicated C import module is cleaner.

Wrap C APIs in Zig APIs

Direct C imports are useful, but most of your program should not need to deal with raw C details.

Instead of spreading this everywhere:

const rc = c.some_library_do_work(ptr, len);
if (rc != 0) return error.Failed;

you can write a Zig wrapper:

fn doWork(buffer: []u8) !void {
    const rc = c.some_library_do_work(buffer.ptr, buffer.len);

    if (rc != 0) {
        return error.Failed;
    }
}

Now the rest of your code calls:

try doWork(buffer);

That is easier to read and safer to maintain.

The C boundary remains in one place.

What to Remember

@cImport imports C declarations into Zig.

It usually contains @cInclude.

It can also contain @cDefine and @cUndef.

It happens at compile time.

It does not replace linking.

It works best when C headers are clean and simple.

C strings, macros, memory ownership, and pointer rules still need care.

The best pattern is to use @cImport at the boundary, then wrap the imported C API in normal Zig functions.