Interop Zig-C: llamar a cualquier biblioteca C desde Zig sin FFI adicional

Décadas de trabajo en C no van a desaparecer. Hay miles de bibliotecas, drivers y sistemas escritos en C que funcionan perfectamente y nadie va a reescribir. Zig lo sabe, y por eso el interop con C no es una característica adicional que requiera bindings ni herramientas externas: está en el núcleo del lenguaje.

@cImport y @cInclude: incluir cabeceras C directamente

En Zig puedes incluir cabeceras C directamente con @cImport y @cInclude. El compilador parsea la cabecera C y traduce las declaraciones a tipos Zig. No hay fichero de bindings generado, no hay herramienta intermedia.

const std = @import("std");
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
    @cInclude("string.h");
});

pub fn main() void {
    // Llamar a printf de la libc directamente
    _ = c.printf("Hola desde C: %dn", 42);

    // Usar malloc y free de la libc
    const ptr = c.malloc(1024) orelse {
        std.debug.print("malloc fallón", .{});
        return;
    };
    defer c.free(ptr);

    // strlen también funciona
    const texto = "hola mundo";
    const len = c.strlen(texto);
    std.debug.print("Longitud: {d}n", .{len});
}

El compilador Zig incluye Clang internamente, así que parsear cabeceras C es algo que hace de forma nativa sin depender de ninguna instalación externa.

Usar SQLite desde Zig

Un ejemplo más realista: usar SQLite, una de las bibliotecas C más usadas del mundo.

const std = @import("std");
const c = @cImport({
    @cInclude("sqlite3.h");
});

pub fn main() !void {
    var db: ?*c.sqlite3 = null;

    const rc = c.sqlite3_open(":memory:", &db);
    if (rc != c.SQLITE_OK) {
        std.debug.print("Error al abrir: {s}n", .{c.sqlite3_errmsg(db)});
        return error.SqliteError;
    }
    defer _ = c.sqlite3_close(db);

    var errmsg: [*c]u8 = null;
    const sql = "CREATE TABLE test (id INTEGER PRIMARY KEY, nombre TEXT)";
    const rc2 = c.sqlite3_exec(db, sql, null, null, &errmsg);
    if (rc2 != c.SQLITE_OK) {
        std.debug.print("Error SQL: {s}n", .{errmsg});
        c.sqlite3_free(errmsg);
        return error.SqliteError;
    }

    std.debug.print("Tabla creada correctamenten", .{});
}

zig cc: el compilador C de Zig

Zig incluye un compilador C completo (basado en Clang) accesible como zig cc. Puedes usarlo como reemplazo de gcc o clang para compilar código C existente, y la gran ventaja es que soporta cross-compilation a cualquier target sin configuración adicional.

# Compilar un fichero C normal
zig cc programa.c -o programa

# Cross-compilar para ARM64 Linux
zig cc programa.c -o programa-arm64 -target aarch64-linux-musl

# Cross-compilar para Windows desde Linux
zig cc programa.c -o programa.exe -target x86_64-windows-gnu

Esto es útil cuando tienes proyectos C existentes que quieres cross-compilar sin montar toolchains específicos. zig cc lleva consigo libc estática para todos los targets principales.

Incluir código fuente C en un proyecto Zig

Si tienes ficheros .c que quieres compilar junto con tu código Zig, el sistema de build permite añadirlos directamente:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.addCSourceFiles(.{
        .files = &.{
            "src/util.c",
            "src/parser.c",
        },
        .flags = &.{"-std=c11", "-O2"},
    });

    exe.linkLibC();
    b.installArtifact(exe);
}

Exportar funciones Zig para usar desde C

El interop funciona en los dos sentidos. Puedes escribir código Zig que C puede llamar marcando las funciones con export y la convención de llamada C:

// lib.zig - código Zig que se expone como biblioteca C
const std = @import("std");

export fn sumar(a: i32, b: i32) i32 {
    return a + b;
}

export fn longitud_string(s: [*c]const u8) usize {
    return std.mem.len(s);
}

En el lado C, la cabecera es simplemente:

// lib.h
#pragma once
#include <stdint.h>
#include <stddef.h>

int32_t sumar(int32_t a, int32_t b);
size_t longitud_string(const char* s);

Tipos C en Zig

Zig tiene tipos específicos para la interoperabilidad con C. El tipo [*c]T representa un puntero C (que puede ser nulo y no tiene longitud implícita), distinto de los slices y punteros seguros de Zig.

Tipo C

Equivalente Zig

char *

[*c]u8

const char *

[*c]const u8

void *

*anyopaque

int

c_int

long

c_long

size_t

usize

La interop con C es una de las razones por las que Zig se usa mucho como reemplazo de C en proyectos existentes: puedes migrar fichero a fichero sin reescribir todo de golpe. Proyectos como Bun aprovechan exactamente esto: Zig compila junto con código C/C++ del motor JavaScriptCore sin necesidad de capas de bindings adicionales.

Imagen: Pexels / Ali Haider

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP