Interoperabilidade Zig e C: Guia Completo

Interoperabilidade Zig e C: Como Usar Bibliotecas C em Zig

Uma das maiores vantagens de Zig é a interoperabilidade nativa com C — sem wrappers manuais, sem FFI complicado, sem overhead. O compilador Zig entende headers C diretamente, e o código gerado é ABI-compatível com C. Isso significa que todo o ecossistema de bibliotecas C, construído ao longo de décadas, está disponível para uso imediato em Zig.

Neste guia, vamos explorar como importar e usar bibliotecas C em projetos Zig, como exportar funções Zig para uso em projetos C, e veremos exemplos práticos com bibliotecas reais como SQLite e zlib.

Se você está começando com Zig, confira nosso artigo Por que aprender Zig e o cheatsheet de interop C.

Por Que a Interop com C é Importante

C é a lingua franca da programação de sistemas. Bibliotecas como OpenSSL, SQLite, zlib, POSIX e drivers de hardware são todas escritas em C. Qualquer linguagem que pretenda competir nesse espaço precisa interagir com código C de forma eficiente.

Diferente de linguagens como Rust (que usa bindgen e unsafe blocks) ou Go (que precisa do cgo com overhead de performance), Zig resolve isso no nível do compilador:

  • @cImport traduz headers C automaticamente para tipos Zig
  • Zero overhead — chamadas são diretas, sem marshalling
  • ABI compatível — structs, enums e funções seguem o layout C
  • O build system do Zig facilita a compilação e linking

Para uma comparação detalhada com Rust, veja Zig vs Rust. Para Go, confira Zig vs Go.

Importando Headers C com @cImport

O @cImport é o coração da interoperabilidade. Ele aceita diretivas como @cInclude, @cDefine e @cUndef:

// Importando a biblioteca padrão C
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
});

pub fn main() void {
    // Usando printf diretamente
    _ = c.printf("Hello from Zig using C printf!\n");

    // Alocando memória com malloc
    const ptr = c.malloc(100);
    if (ptr) |p| {
        c.free(p);
    }
}

O compilador Zig lê o header C, traduz os tipos e funções para equivalentes Zig e disponibiliza tudo no namespace c. Não precisa escrever bindings manualmente — o compilador faz isso por você.

Definindo Macros com @cDefine

Algumas bibliotecas C dependem de macros de pré-processador. Você pode defini-las antes do include:

const c = @cImport({
    // Habilitar funcionalidades POSIX
    @cDefine("_POSIX_C_SOURCE", "200809L");
    @cInclude("signal.h");
    @cInclude("unistd.h");
});

Consulte o glossário de comptime para entender como Zig resolve essas importações em tempo de compilação.

Exemplo Prático: Usando SQLite em Zig

SQLite é uma das bibliotecas C mais usadas no mundo. Veja como integrá-la:

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

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

    // Abrir banco de dados
    const rc = c.sqlite3_open(":memory:", &db);
    if (rc != c.SQLITE_OK) {
        std.debug.print("Erro ao abrir DB: {s}\n", .{c.sqlite3_errmsg(db)});
        return error.DatabaseOpenFailed;
    }
    defer _ = c.sqlite3_close(db);

    // Criar tabela
    var err_msg: ?[*:0]u8 = null;
    const sql = "CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT);";
    const create_rc = c.sqlite3_exec(db, sql, null, null, &err_msg);
    if (create_rc != c.SQLITE_OK) {
        std.debug.print("Erro SQL: {s}\n", .{err_msg.?});
        c.sqlite3_free(err_msg);
        return error.SqlError;
    }

    // Inserir dados
    const insert_sql = "INSERT INTO usuarios (nome) VALUES ('Maria'), ('João');";
    _ = c.sqlite3_exec(db, insert_sql, null, null, null);

    std.debug.print("SQLite integrado com sucesso!\n", .{});
}

Para compilar, configure o build.zig para linkar com SQLite:

// No build.zig
exe.linkSystemLibrary("sqlite3");
exe.linkLibC();

Para mais sobre integrações com bancos de dados, veja Zig e Banco de Dados.

Exemplo Prático: Compressão com zlib

Outro caso comum — usar zlib para compressão:

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

pub fn compress_data(input: []const u8, output: []u8) !usize {
    var dest_len: c.uLongf = @intCast(output.len);
    const src_len: c.uLong = @intCast(input.len);

    const result = c.compress(
        output.ptr,
        &dest_len,
        input.ptr,
        src_len,
    );

    if (result != c.Z_OK) {
        return error.CompressionFailed;
    }

    return @intCast(dest_len);
}

pub fn main() !void {
    const data = "Zig e C trabalhando juntos! " ** 100;
    var compressed: [4096]u8 = undefined;

    const compressed_size = try compress_data(data, &compressed);
    std.debug.print("Original: {d} bytes → Comprimido: {d} bytes\n", .{
        data.len,
        compressed_size,
    });
}

Exportando Funções Zig para C

A interop funciona nos dois sentidos. Use export para tornar funções Zig chamáveis de C:

// lib.zig — biblioteca Zig exportada para C
const std = @import("std");

// Função callable de C
export fn zig_soma(a: i32, b: i32) i32 {
    return a + b;
}

// Struct com layout C
const Ponto = extern struct {
    x: f64,
    y: f64,
};

export fn zig_distancia(p1: Ponto, p2: Ponto) f64 {
    const dx = p1.x - p2.x;
    const dy = p1.y - p2.y;
    return @sqrt(dx * dx + dy * dy);
}

// Callback compatível com C
export fn zig_callback(data: ?*anyopaque) void {
    _ = data;
    std.debug.print("Callback Zig chamado de C!\n", .{});
}

Do lado C:

// main.c — usando a lib Zig
#include <stdio.h>

// Declarações das funções Zig
extern int zig_soma(int a, int b);

typedef struct {
    double x;
    double y;
} Ponto;

extern double zig_distancia(Ponto p1, Ponto p2);
extern void zig_callback(void *data);

int main() {
    printf("Soma: %d\n", zig_soma(10, 20));

    Ponto p1 = {0.0, 0.0};
    Ponto p2 = {3.0, 4.0};
    printf("Distância: %.2f\n", zig_distancia(p1, p2));

    zig_callback(NULL);
    return 0;
}

Esse padrão é ideal para migrar projetos C para Zig de forma incremental. Se está considerando essa migração, confira nosso guia Migrar Projeto C para Zig.

Tipos e Compatibilidade ABI

Para que a interop funcione corretamente, os tipos precisam ser ABI-compatíveis:

Tipo CTipo ZigNotas
intc_intTamanho depende da plataforma
unsigned longc_ulongUsado em muitas APIs POSIX
char *[*:0]u8String terminada em null
void *?*anyopaquePonteiro genérico opaco
structextern structLayout C garantido
enumextern enumValores numéricos compatíveis
size_tusizeTamanho de ponteiro

Use sempre extern struct ao invés de struct quando a struct será compartilhada com código C — isso garante que Zig use o mesmo layout de memória que o compilador C usaria.

Para mais sobre ponteiros e tipos em Zig, consulte o glossário de pointer types e slice.

translate-c: Convertendo Código C para Zig

O comando zig translate-c converte código C para Zig automaticamente. É útil para entender como Zig interpreta headers ou migrar código:

# Traduzir um header
zig translate-c /usr/include/stdio.h > stdio_zig.zig

# Com flags de pré-processador
zig translate-c -I/usr/local/include mylib.h

O código gerado não é idiomático, mas funciona como ponto de partida para refatoração. Use junto com o comptime para criar wrappers mais elegantes.

Configurando o Build System

O build system do Zig simplifica o linking com bibliotecas C:

// build.zig
const std = @import("std");

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

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

    // Linkar com bibliotecas C do sistema
    exe.linkSystemLibrary("sqlite3");
    exe.linkSystemLibrary("z"); // zlib
    exe.linkLibC();

    // Adicionar caminhos de include personalizados
    exe.addIncludePath(b.path("vendor/include"));
    exe.addLibraryPath(b.path("vendor/lib"));

    b.installArtifact(exe);
}

Para projetos com cross-compilation, o Zig resolve automaticamente os sysroots e headers para o target desejado — uma vantagem enorme sobre toolchains tradicionais.

Veja mais detalhes em build.zig no glossário e o overview do build system.

Boas Práticas

  1. Use extern struct para qualquer struct compartilhada com C
  2. Sempre chame linkLibC() quando usar @cImport
  3. Prefira wrappers idiomáticos — wraps funções C em APIs Zig com error handling adequado
  4. Teste a interop — use o framework de testes do Zig para validar chamadas C
  5. Documente conversões de tipo — especialmente ponteiros opcionais vs nullable
  6. Verifique alignment ao passar structs entre Zig e C

Comparação com Outras Linguagens

A interop com C é um diferencial de Zig frente a outras linguagens de sistemas:

  • Rust usa bindgen + unsafe blocks, com overhead cognitivo significativo. Veja a comparação completa Zig vs Rust e explore o ecossistema Rust em rustlang.com.br
  • Go usa cgo que adiciona overhead de performance nas chamadas. Compare em detalhes em Zig vs Go e saiba mais sobre Go em golang.com.br
  • Zig — chamadas diretas, zero overhead, tradução automática de headers

Para desenvolvedores vindo de outras linguagens, temos guias específicos: Zig para programadores Kotlin, Java e C#.

Conclusão

A interoperabilidade com C é um dos pontos mais fortes de Zig. O @cImport elimina a necessidade de bindings manuais, o export permite migração incremental, e o build system simplifica o linking. Com o vasto ecossistema de bibliotecas C à disposição, você pode começar a escrever código Zig hoje sem abrir mão de decades de software existente.

Para continuar aprendendo, explore o cheatsheet de interop C, veja como funciona o build system do Zig e confira o roadmap de desenvolvedor Zig.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.