La interoperabilidad con C en Zig es la capacidad de traducir cabeceras .h, enlazar bibliotecas y llamar a sus funciones. Esto permite reutilizar proyectos como SDL, Raylib o SQLite sin mantener a mano todas sus declaraciones.
Zig incluye herramientas para compilar C y traducir sus declaraciones a tipos compatibles. La llamada conserva la ABI de C, aunque la traducción no convierte automáticamente la API en una interfaz idiomática o segura de Zig.
Traducir cabeceras desde build.zig
Desde Zig 0.16, la ruta recomendada es crear un paso addTranslateC en build.zig. Primero agrupamos los includes en una cabecera.
// src/c.h
#include "mi_libreria.h"Después traducimos esa cabecera y exponemos el resultado como módulo 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 = "mi-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);El código Zig importa el módulo por el nombre configurado:
const c = @import("c");
pub fn main() void {
c.mi_funcion();
}@cImport todavía funciona en Zig 0.16, pero está deprecado y previsto para desaparecer. En código nuevo conviene usar el paso de traducción del sistema de construcción.
Traducción de tipos
Una de las cosas que verás al usar @cImport es que los tipos cambian de nombre para reflejar que vienen de C. Zig intenta mantener la compatibilidad binaria exacta.
| Tipo en C | Tipo en Zig (Importado) | Notas |
|---|---|---|
int | c_int | Suele ser i32, pero depende de la plataforma. |
unsigned int | c_uint | Igual que arriba. |
char | c_char | Su signo depende del objetivo. |
void* | ?*anyopaque | Puntero opaco opcional. |
struct Point | Identificador generado | El nombre concreto depende de cómo esté declarada la estructura. |
#define MAX 10 | c.MAX | Las macros simples se convierten en constantes. |
El tipo puntero de C ([*c]T)
C es famoso por su ambigüedad con los punteros. Un int* en C puede ser:
- Un puntero a un solo entero.
- Un puntero a una lista de enteros.
- Podría ser
NULL.
Como Zig no puede saber cuál es la intención original solo leyendo el .h, utiliza un tipo especial de transición: [*c]T (Puntero C).
Este puntero de compatibilidad:
- Actúa como un puntero de muchos elementos (
[*]). - Puede representar la dirección cero y convertirse a punteros opcionales.
- Soporta aritmética de punteros.
Cuando trabajas con [*c]T, Zig no conoce la longitud ni la intención de la API. Es tu responsabilidad validar la dirección, la longitud, la vida útil y el campo activo que establezca el contrato de C.
Vinculación en build.zig
Traducir el .h aporta declaraciones, pero todavía necesitas enlazar la biblioteca binaria (.lib, .dll, .so, .a) o compilar sus archivos .c.
Esto se hace en tu build.zig.
Vincular libc
Casi cualquier importación de C requiere la LibC.
const exe = b.addExecutable(.{
.name = "mi-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.link_libc = true,
}),
});Vincular bibliotecas del sistema
Si tienes la librería instalada en tu sistema (por ejemplo, con apt install libsdl2-dev o brew install sdl2).
translate_c.linkSystemLibrary("SDL2", .{});Compilar código C local
Si tienes archivos .c en tu proyecto y quieres compilarlos junto con tu Zig:
exe.root_module.addCSourceFile(.{
.file = b.path("src/mi_libreria.c"),
.flags = &[_][]const u8{"-std=c99", "-O3"},
});
// Y le dices dónde están los .h
exe.root_module.addIncludePath(b.path("src/include"));Ejemplo con Raylib
Imagina usar Raylib, una librería de videojuegos en C, directamente en Zig.
src/c.h:
#include <raylib.h>build.zig:
const translate_c = b.addTranslateC(.{
.root_source_file = b.path("src/c.h"),
.target = target,
.optimize = optimize,
});
translate_c.linkSystemLibrary("raylib", .{});
// Añade translate_c.createModule() al root_module con el nombre "c",
// como en el ejemplo anterior.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(); // ¡Usamos defer para limpiar estilo Zig!
c.SetTargetFPS(60);
while (!c.WindowShouldClose()) {
c.BeginDrawing();
defer c.EndDrawing();
c.ClearBackground(c.RAYWHITE);
c.DrawText("¡Hola Zig!", 190, 200, 20, c.LIGHTGRAY);
}
}Podemos aplicar defer alrededor de funciones de C como CloseWindow y EndDrawing. Esto mejora la estructura del código Zig, pero no añade comprobaciones a la API original.
Definiciones del preprocesador
A veces una cabecera cambia según los #define activos antes del include. Con el flujo de Zig 0.16 podemos colocarlos en la cabecera que pasamos a addTranslateC.
// src/c.h
#define _GNU_SOURCE
#include <stdio.h>