# Building Mixed Zig and C Projects

### Building Mixed Zig and C Projects

A mixed Zig and C project contains source files from both languages.

This is common when you are using an existing C library, replacing part of a C project with Zig, or writing a Zig program that depends on small C helper files.

A simple mixed project may look like this:

```text
project/
  build.zig
  src/
    main.zig
    mathlib.c
    mathlib.h
```

The Zig file contains the main program. The C header describes the C API. The C source file contains the C implementation.

#### The C Header

The header is the contract between Zig and C.

```c
// src/mathlib.h

#ifndef MATHLIB_H
#define MATHLIB_H

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

#endif
```

This file says two functions exist:

```text
add
sub
```

It does not contain the function bodies. It only describes their names, parameters, and return types.

#### The C Source File

The C source file implements the functions.

```c
// src/mathlib.c

#include "mathlib.h"

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

int sub(int a, int b) {
    return a - b;
}
```

This is the code the linker needs in the final executable.

#### The Zig File

The Zig file imports the C header with `@cImport`.

```zig
// src/main.zig

const std = @import("std");

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

pub fn main() void {
    const x = c.add(10, 20);
    const y = c.sub(10, 3);

    std.debug.print("x = {}, y = {}\n", .{ x, y });
}
```

This program calls the C functions through the imported namespace `c`.

The name `c` is only a convention. It helps readers see that `c.add` and `c.sub` come from C.

#### The Build File

The build file connects everything.

```zig
// build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "mixed",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.addIncludePath(b.path("src"));

    exe.addCSourceFile(.{
        .file = b.path("src/mathlib.c"),
        .flags = &.{},
    });

    exe.linkLibC();

    b.installArtifact(exe);
}
```

There are three important lines:

```zig
exe.addIncludePath(b.path("src"));
```

This lets `@cInclude("mathlib.h")` find the header.

```zig
exe.addCSourceFile(.{
    .file = b.path("src/mathlib.c"),
    .flags = &.{},
});
```

This compiles the C source file and links it into the executable.

```zig
exe.linkLibC();
```

This links the C standard library. Many C files need this, especially if they use libc functions.

#### Running the Project

Build the program:

```bash
zig build
```

Run the installed executable:

```bash
zig build run
```

To support `zig build run`, add a run step:

```zig
const run_cmd = b.addRunArtifact(exe);

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

A fuller build file becomes:

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "mixed",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });

    exe.addIncludePath(b.path("src"));

    exe.addCSourceFile(.{
        .file = b.path("src/mathlib.c"),
        .flags = &.{},
    });

    exe.linkLibC();

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

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

Now this works:

```bash
zig build run
```

Expected output:

```text
x = 30, y = 7
```

#### Keep the Boundary Simple

In a mixed project, the boundary between Zig and C should be small and clear.

This is good:

```zig
const result = c.add(10, 20);
```

This is also fine:

```zig
fn add(a: i32, b: i32) i32 {
    return @intCast(c.add(@intCast(a), @intCast(b)));
}
```

The second version wraps the C function in a Zig function. That lets the rest of your program use normal Zig types and naming.

A wrapper becomes more valuable when the C API has raw pointers, status codes, or manual cleanup.

For example, C might expose this:

```c
int read_file(const char *path, unsigned char *buffer, size_t len);
```

A Zig wrapper could expose this:

```zig
const ReadError = error{
    Failed,
};

fn readFile(path: [*:0]const u8, buffer: []u8) ReadError!void {
    const rc = c.read_file(path, buffer.ptr, buffer.len);

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

The rest of the Zig program calls:

```zig
try readFile("config.txt", buffer[0..]);
```

That is cleaner than repeating C-style checks everywhere.

#### Passing Data Between Zig and C

Data crossing the Zig/C boundary must have a compatible representation.

Simple numbers are easy:

```c
int add(int a, int b);
```

Zig can call this with C-compatible integers:

```zig
const x: c_int = c.add(10, 20);
```

Structs need C-compatible layout:

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

Strings need special care. A Zig slice has a pointer and a length:

```zig
[]const u8
```

A C string is usually a pointer to bytes ending in zero:

```c
const char *
```

So this is safe:

```zig
c.puts("hello");
```

But this may be unsafe:

```zig
const msg: []const u8 = "hello";
_ = c.puts(msg.ptr);
```

The pointer does not prove that the bytes are null-terminated.

Use a null-terminated pointer type when the C API expects a C string:

```zig
const msg: [*:0]const u8 = "hello";
_ = c.puts(msg);
```

#### Building Several C Files

A real C library may have several source files.

```text
src/
  main.zig
  c_lib/
    parser.c
    scanner.c
    util.c
    library.h
```

You can add them one by one:

```zig
exe.addCSourceFile(.{
    .file = b.path("src/c_lib/parser.c"),
    .flags = &.{},
});

exe.addCSourceFile(.{
    .file = b.path("src/c_lib/scanner.c"),
    .flags = &.{},
});

exe.addCSourceFile(.{
    .file = b.path("src/c_lib/util.c"),
    .flags = &.{},
});
```

Or use a list:

```zig
const c_flags = &.{
    "-std=c11",
};

const c_files = [_][]const u8{
    "src/c_lib/parser.c",
    "src/c_lib/scanner.c",
    "src/c_lib/util.c",
};

for (c_files) |file| {
    exe.addCSourceFile(.{
        .file = b.path(file),
        .flags = c_flags,
    });
}
```

This is easier to maintain when the list grows.

#### C Compile Flags

C files often need flags.

Examples:

```zig
const c_flags = &.{
    "-std=c11",
    "-Wall",
    "-Wextra",
};
```

Then pass them to each C source file:

```zig
exe.addCSourceFile(.{
    .file = b.path("src/mathlib.c"),
    .flags = c_flags,
});
```

Flags can control the C standard, warnings, feature macros, optimization behavior, and platform-specific settings.

Some libraries need macro definitions:

```zig
const c_flags = &.{
    "-DMY_LIBRARY_FEATURE=1",
};
```

If a macro affects both the header and the source file, keep both sides consistent.

In Zig import:

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

In build file:

```zig
const c_flags = &.{
    "-DMY_LIBRARY_FEATURE=1",
};
```

If the header sees one configuration and the C source is compiled with another, the declarations and implementations may not match.

#### Project Layout for Larger Mixed Projects

For larger projects, keep C code and Zig code organized.

One reasonable layout:

```text
project/
  build.zig
  src/
    main.zig
    c.zig
    wrapper.zig
  c/
    include/
      library.h
    src/
      library.c
      helper.c
```

Then `src/c.zig` contains the import:

```zig
pub const c = @cImport({
    @cInclude("library.h");
});
```

Other Zig files use:

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

The build file adds:

```zig
exe.addIncludePath(b.path("c/include"));
```

and compiles the C files:

```zig
const c_files = [_][]const u8{
    "c/src/library.c",
    "c/src/helper.c",
};

for (c_files) |file| {
    exe.addCSourceFile(.{
        .file = b.path(file),
        .flags = &.{"-std=c11"},
    });
}
```

This layout keeps the raw C import centralized. It also gives you a natural place for Zig wrappers.

#### Wrapping a C Library

Suppose C exposes an opaque handle:

```c
typedef struct Database Database;

Database *database_open(const char *path);
void database_close(Database *db);
int database_exec(Database *db, const char *sql);
```

The raw Zig calls might look like this:

```zig
const db = c.database_open("app.db") orelse return error.OpenFailed;
defer c.database_close(db);

const rc = c.database_exec(db, "create table users(id integer)");
if (rc != 0) return error.ExecFailed;
```

A Zig wrapper gives it a better interface:

```zig
const DatabaseError = error{
    OpenFailed,
    ExecFailed,
};

const Database = struct {
    ptr: *c.Database,

    pub fn open(path: [*:0]const u8) DatabaseError!Database {
        const ptr = c.database_open(path) orelse return error.OpenFailed;
        return .{ .ptr = ptr };
    }

    pub fn close(self: Database) void {
        c.database_close(self.ptr);
    }

    pub fn exec(self: Database, sql: [*:0]const u8) DatabaseError!void {
        const rc = c.database_exec(self.ptr, sql);

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

Now application code is clean:

```zig
const db = try Database.open("app.db");
defer db.close();

try db.exec("create table users(id integer)");
```

The wrapper has three benefits.

It converts null pointers into Zig errors.

It converts status codes into Zig errors.

It hides the raw C handle from most of the program.

#### Exporting Zig Functions to C

A mixed project can also call Zig from C.

Zig can export a C-compatible function:

```zig
export fn zig_add(a: c_int, b: c_int) c_int {
    return a + b;
}
```

C can declare it:

```c
int zig_add(int a, int b);
```

Then C can call it:

```c
int result = zig_add(10, 20);
```

The `export` keyword makes the function visible as a symbol in the final object or library.

Use C-compatible types in exported functions.

Avoid exporting Zig-specific types like slices, error unions, or normal Zig structs directly to C. C does not understand those types.

Instead, expose a C-shaped API:

```zig
export fn fill_buffer(buffer: [*]u8, len: usize) c_int {
    const slice = buffer[0..len];

    for (slice, 0..) |*byte, i| {
        byte.* = @intCast(i % 256);
    }

    return 0;
}
```

C sees pointer plus length. Zig internally converts that into a slice.

#### Build Modes Still Matter

Mixed projects still use Zig build modes.

Common modes:

```bash
zig build -Doptimize=Debug
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmall
```

These modes affect Zig code and may also affect how C code is compiled through the build system.

During development, use debug or safe builds. They give better diagnostics and stronger safety checks.

For release builds, choose the mode that matches your goal.

`ReleaseFast` favors speed.

`ReleaseSmall` favors binary size.

`ReleaseSafe` keeps more safety checks.

#### Common Errors

A header error usually looks like this:

```text
'library.h' file not found
```

This means the include path is wrong or missing.

Fix it with:

```zig
exe.addIncludePath(b.path("c/include"));
```

A linker error usually looks like this:

```text
undefined symbol: library_function
```

This means the function was declared but no compiled implementation was linked.

Fix it by adding the C source file or linking the library.

A type error usually means your Zig call does not match the imported C declaration.

For example, a C function expects a pointer:

```c
void update(int *value);
```

But Zig passes a value:

```zig
c.update(x);
```

The correct call is:

```zig
c.update(&x);
```

#### What to Remember

A mixed Zig and C project needs headers, implementations, and a build file that connects them.

Use `@cImport` for C declarations.

Use `addIncludePath` so headers can be found.

Use `addCSourceFile` or library linking so implementations are included.

Use `linkLibC` when the C code needs libc.

Keep C imports centralized.

Wrap raw C APIs in Zig functions.

Use C-compatible types at the boundary.

Expose simple C-shaped APIs when C needs to call Zig.

The best mixed projects keep the boundary narrow. C code stays at the edge. Zig code uses clearer wrappers, explicit errors, slices, and ownership rules.

