zig-interoperabilidad-c

C Interoperability in Zig 0.16 with addTranslateC

  • 4 min

C interoperability in Zig is the ability to translate .h headers, link libraries, and call their functions. This allows reusing projects like SDL, Raylib, or SQLite without manually maintaining all their declarations.

Zig includes tools to compile C and translate its declarations into compatible types. The call preserves the C ABI, although the translation does not automatically convert the API into an idiomatic or safe Zig interface.

Translating Headers from build.zig

Since Zig 0.16, the recommended path is to create an addTranslateC step in build.zig. First, we group the includes into a single header.

// src/c.h
#include "my_library.h"
Copied!

Then we translate that header and expose the result as module c:

// build.zig
const translate_c = b.addTranslateC(.{
    .root_source_file = b.path("src/c.h"),
    .target = target,
    .optimize = optimize,
});

const exe = b.addExecutable(.{
    .name = "my-app",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .imports = &.{.{.
            .name = "c",
            .module = translate_c.createModule(),
        }},
    }),
});

b.installArtifact(exe);
Copied!

The Zig code imports the module by the configured name:

const c = @import("c");

pub fn main() void {
    c.my_function();
}
Copied!

@cImport still works in Zig 0.16, but it is deprecated and scheduled for removal. For new code, it is better to use the translation step of the build system.

Type Translation

One of the things you will see when using @cImport is that the types are renamed to reflect that they come from C. Zig tries to maintain exact binary compatibility.

C TypeZig Type (Imported)Notes
intc_intUsually i32, but depends on the platform.
unsigned intc_uintSame as above.
charc_charIts signedness depends on the target.
void*?*anyopaqueOptional opaque pointer.
struct PointGenerated identifierThe specific name depends on how the structure is declared.
#define MAX 10c.MAXSimple macros become constants.

The C Pointer Type ([*c]T)

C is famous for its ambiguity with pointers. An int* in C can be:

  1. A pointer to a single integer.
  2. A pointer to a list of integers.
  3. It could be NULL.

Since Zig cannot know the original intent just by reading the .h, it uses a special transitional type: [*c]T (C Pointer).

This compatibility pointer:

  • Acts as a many-item pointer ([*]).
  • Can represent the zero address and be converted to optional pointers.
  • Supports pointer arithmetic.

When working with [*c]T, Zig does not know the length or the intent of the API. It is your responsibility to validate the address, length, lifetime, and active field as defined by the C contract.

Linking in build.zig

Translating the .h provides declarations, but you still need to link the binary library (.lib, .dll, .so, .a) or compile its .c files.

This is done in your build.zig.

Linking libc

Almost any C import requires libc.

const exe = b.addExecutable(.{
    .name = "my-app",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .link_libc = true,
    }),
});
Copied!

Linking System Libraries

If you have the library installed on your system (e.g., with apt install libsdl2-dev or brew install sdl2).

translate_c.linkSystemLibrary("SDL2", .{});
Copied!

Compiling Local C Code

If you have .c files in your project and want to compile them alongside your Zig:

exe.root_module.addCSourceFile(.{
    .file = b.path("src/my_library.c"),
    .flags = &[_][]const u8{"-std=c99", "-O3"},
});
// And tell it where the .h files are
exe.root_module.addIncludePath(b.path("src/include"));
Copied!

Example with Raylib

Imagine using Raylib, a C game programming library, directly from Zig.

src/c.h:

#include <raylib.h>
Copied!

build.zig:

const translate_c = b.addTranslateC(.{
    .root_source_file = b.path("src/c.h"),
    .target = target,
    .optimize = optimize,
});
translate_c.linkSystemLibrary("raylib", .{});

// Add translate_c.createModule() to the root_module with the name "c",
// as in the previous example.
Copied!

main.zig:

const c = @import("c");

pub fn main() void {
    const width = 800;
    const height = 450;

    c.InitWindow(width, height, "Zig + Raylib");
    defer c.CloseWindow(); // Using defer for Zig-style cleanup!

    c.SetTargetFPS(60);

    while (!c.WindowShouldClose()) {
        c.BeginDrawing();
        defer c.EndDrawing();

        c.ClearBackground(c.RAYWHITE);
        c.DrawText("Hello Zig!", 190, 200, 20, c.LIGHTGRAY);
    }
}
Copied!

We can apply defer around C functions like CloseWindow and EndDrawing. This improves the structure of the Zig code but does not add checks to the original API.

Preprocessor Definitions

Sometimes a header changes based on the #define directives active before the include. With the Zig 0.16 flow, we can place them in the header we pass to addTranslateC.

// src/c.h
#define _GNU_SOURCE
#include <stdio.h>
Copied!