zig-translate-c-tool

zig translate-c: converting C code to Zig automatically

  • 3 min

zig translate-c is a tool that translates C declarations and code to Zig. In Zig 0.16, the translator is based on Aro, not libclang, and shares the translation infrastructure used by addTranslateC.

Zig includes a built-in command-line tool called translate-c.

It’s not a simple “find and replace”. The tool analyzes C types, macros, and pointers and attempts to emit Zig code with compatible behavior. The translation is best-effort: certain elements degrade to extern declarations, opaque types, or @compileError.

How to use it

From the terminal we use:

zig translate-c main.c > main.zig
Copied!

This takes the main.c file, translates it, and dumps the result into main.zig.

You can also pass C compilation flags if your code depends on special includes or definitions:

zig translate-c -I./include -DDEBUG=1 main.c
Copied!

Practical example

Let’s see what it actually does. Suppose we have this C code (math.c):

// math.c
#include <stdint.h>

#define MAX_VAL 100

struct Vector2 {
    float x;
    float y;
};

int sumar_vectores(struct Vector2* a, struct Vector2* b) {
    if (a->x > MAX_VAL) return -1;
    return (int)(a->x + b->x);
}
Copied!

If we run zig translate-c math.c, we’ll get a result similar to this (the exact text may change between versions):

pub const MAX_VAL = @as(c_int, 100);

pub const struct_Vector2 = extern struct {
    x: f32,
    y: f32,
};

pub export fn sumar_vectores(a: [*c]struct_Vector2, b: [*c]struct_Vector2) c_int {
    if (a.*.x > @as(f32, @floatFromInt(MAX_VAL))) {
        return -1;
    }
    return @as(c_int, @intFromFloat(a.*.x + b.*.x));
}
Copied!

Notice what happened.

  1. Macros: #define MAX_VAL has become a pub const constant.
  2. Structs: struct Vector2 is now an extern struct, guaranteeing that the memory layout is identical to C’s.
  3. Types: int has become c_int (not i32, to maintain pure compatibility).
  4. Pointers: C pointers are usually represented as [*c]T, because the header doesn’t express length or nullability using Zig types.
  5. Casts: Zig is strict. The translator has added @as, @floatFromInt, and @intFromFloat to replicate the implicit casts C performed silently.

What is it really for?

The resulting code is often “Zig with a C accent”. It’s not necessarily idiomatic: it retains c_int types, C pointers, and necessary conversions to preserve the original behavior.

So why is it so useful?

Consulting how a declaration is represented

If you’re unsure how Zig represents a particular C declaration, you can translate a minimal header and inspect the result. Remember that this representation prioritizes compatibility, not idiomatic design.

Diagnosing the translation of a header

addTranslateC, zig translate-c, and the deprecated @cImport share the same translation backend.

If an error occurs when translating a library, running zig translate-c library.h manually lets you inspect which declarations were converted and which ended up as @compileError or opaque.

Creating a starting point for porting

If you need to migrate a 5,000-line C file, translating it by hand is tedious. We can use translate-c as a first draft and then refactor function by function: replace [*c]T with slices when we know the length, express ownership, and change error codes to !T. Don’t assume every translation will compile without adjustments.

Limitations

translate-c doesn’t work miracles. Some C things are directly untranslatable or generate very ugly code:

  • Complex macros: Macros that abuse the preprocessor (concatenating tokens, hidden loops) are often ignored or generate errors.
  • void* pointers: they are translated to compatible opaque pointers, whose intended meaning you’ll have to recover from the C contract.
  • goto and other untranslatable constructs: a function can be downgraded to an extern declaration, so you can still link to and call it from code compiled as C.