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.zigThis 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.cPractical 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);
}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));
}Notice what happened.
- Macros:
#define MAX_VALhas become apub constconstant. - Structs:
struct Vector2is now anextern struct, guaranteeing that the memory layout is identical to C’s. - Types:
inthas becomec_int(noti32, to maintain pure compatibility). - Pointers: C pointers are usually represented as
[*c]T, because the header doesn’t express length or nullability using Zig types. - Casts: Zig is strict. The translator has added
@as,@floatFromInt, and@intFromFloatto 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.gotoand other untranslatable constructs: a function can be downgraded to anexterndeclaration, so you can still link to and call it from code compiled as C.