zig-interoperabilidad-c

Interoperabilidad con C en Zig 0.16 mediante addTranslateC

  • 4 min

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"
Copied!

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);
Copied!

El código Zig importa el módulo por el nombre configurado:

const c = @import("c");

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

@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 CTipo en Zig (Importado)Notas
intc_intSuele ser i32, pero depende de la plataforma.
unsigned intc_uintIgual que arriba.
charc_charSu signo depende del objetivo.
void*?*anyopaquePuntero opaco opcional.
struct PointIdentificador generadoEl nombre concreto depende de cómo esté declarada la estructura.
#define MAX 10c.MAXLas 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:

  1. Un puntero a un solo entero.
  2. Un puntero a una lista de enteros.
  3. 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,
    }),
});
Copied!

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", .{});
Copied!

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"));
Copied!

Ejemplo con Raylib

Imagina usar Raylib, una librería de videojuegos en C, directamente en 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", .{});

// Añade translate_c.createModule() al root_module con el nombre "c",
// como en el ejemplo anterior.
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(); // ¡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);
    }
}
Copied!

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>
Copied!