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:
@cImporttraduz 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 C | Tipo Zig | Notas |
|---|---|---|
int | c_int | Tamanho depende da plataforma |
unsigned long | c_ulong | Usado em muitas APIs POSIX |
char * | [*:0]u8 | String terminada em null |
void * | ?*anyopaque | Ponteiro genérico opaco |
struct | extern struct | Layout C garantido |
enum | extern enum | Valores numéricos compatíveis |
size_t | usize | Tamanho 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
- Use
extern structpara qualquer struct compartilhada com C - Sempre chame
linkLibC()quando usar@cImport - Prefira wrappers idiomáticos — wraps funções C em APIs Zig com error handling adequado
- Teste a interop — use o framework de testes do Zig para validar chamadas C
- Documente conversões de tipo — especialmente ponteiros opcionais vs nullable
- 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+unsafeblocks, com overhead cognitivo significativo. Veja a comparação completa Zig vs Rust e explore o ecossistema Rust em rustlang.com.br - Go usa
cgoque 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.