Para chamar C em Zig, importe o header com @cImport e vincule a implementação durante a compilação. Para chamar Zig em C, declare funções com export e forneça um header C compatível. Na fronteira entre as linguagens, passe ponteiro e tamanho separadamente, respeite a ABI C e documente quem libera a memória. Um slice Zig não é um tipo de parâmetro C.
Este guia usa Zig 0.14.1 nos exemplos executáveis. Confira zig version antes de copiar o código: a API de build.zig muda entre versões. Os comandos abaixo usam Linux como referência; nomes e caminhos de bibliotecas do sistema podem variar.
Como chamar a biblioteca padrão C com @cImport
Salve como main.zig:
const c = @cImport({
@cInclude("stdio.h");
@cInclude("stdlib.h");
});
pub fn main() !void {
_ = c.puts("Zig chamou a biblioteca C!");
const raw = c.malloc(16) orelse return error.OutOfMemory;
defer c.free(raw);
const bytes: [*]u8 = @ptrCast(raw);
const buffer = bytes[0..16];
@memset(buffer, 0);
}
Compile e execute:
zig run main.zig -lc
@cInclude seleciona as declarações; -lc vincula a libc. Importar um header não vincula a biblioteca que implementa suas funções. Neste exemplo, malloc pode retornar null, o tamanho é conhecido pelo chamador e defer garante a chamada de free.
Isso não torna uma API C automaticamente segura. Se a função escrever além do buffer ou guardar um ponteiro que deixa de ser válido, Zig não consegue corrigir o contrato da biblioteca.
Compilar um arquivo C junto com Zig
Um exemplo local elimina a dependência de bibliotecas externas. Crie três arquivos no mesmo diretório.
soma.h:
#ifndef SOMA_H
#define SOMA_H
int soma(int a, int b);
#endif
soma.c:
#include "soma.h"
int soma(int a, int b) {
return a + b;
}
main.zig:
const std = @import("std");
const c = @cImport({
@cInclude("soma.h");
});
pub fn main() void {
std.debug.print("soma = {d}\n", .{c.soma(20, 22)});
}
Execute:
zig run main.zig soma.c -I. -lc
# soma = 42
-I. permite encontrar o header; soma.c fornece a implementação. Os argumentos são pequenos para evitar overflow de int neste exemplo. Em uma API real, defina limites ou retorne um código de erro.
O mesmo projeto em build.zig
Para Zig 0.14.1, salve este build.zig junto dos três arquivos:
const std = @import("std");
pub fn build(b: *std.Build) void {
const exe = b.addExecutable(.{
.name = "interop",
.root_source_file = b.path("main.zig"),
.target = b.standardTargetOptions(.{}),
.optimize = b.standardOptimizeOption(.{}),
});
exe.addIncludePath(b.path("."));
exe.addCSourceFile(.{
.file = b.path("soma.c"),
.flags = &.{"-std=c11"},
});
exe.linkLibC();
b.installArtifact(exe);
}
zig build
./zig-out/bin/interop
Para uma biblioteca já instalada, use exe.linkSystemLibrary("sqlite3") ou exe.linkSystemLibrary("z") em vez de compilar sua implementação com addCSourceFile. Consulte também o guia do build system.
Ponteiro e tamanho: output.ptr não carrega output.len
Um slice []u8 contém um ponteiro e um comprimento. Uma função C que recebe apenas unsigned char * não recebe esse comprimento implicitamente. Ao passar output.ptr, passe também output.len no parâmetro previsto pela API.
| Contrato da API C | Como representar no lado Zig |
|---|---|
| Buffer de entrada e tamanho | input.ptr e o tamanho convertido para o tipo do header |
| Buffer de saída e capacidade | output.ptr e output.len; a capacidade não é o tamanho final |
| Comprimento escrito por referência | Variável mutável do tipo importado, passada com &len |
| String terminada em zero | Ponteiro com sentinela, como [*:0]const u8, depois de validar o contrato |
| Ponteiro que pode ser nulo | Verificação explícita antes de desreferenciar |
Em APIs como zlib, o comprimento de saída é entrada e saída: antes da chamada indica a capacidade; depois de uma chamada bem-sucedida indica quantos bytes foram escritos. Isso é diferente de uma função que recebe um unsigned char ** e aloca o buffer internamente.
Exemplo com zlib: capacidade versus tamanho final
Salve como compress.zig. É necessário ter a biblioteca zlib e seu header de desenvolvimento instalados.
const std = @import("std");
const c = @cImport({
@cInclude("zlib.h");
});
fn compressData(input: []const u8, output: []u8) !usize {
const input_len = std.math.cast(c.uLong, input.len)
orelse return error.InputTooLarge;
var output_len = std.math.cast(c.uLongf, output.len)
orelse return error.OutputTooLarge;
const rc = c.compress(output.ptr, &output_len, input.ptr, input_len);
switch (rc) {
c.Z_OK => {},
c.Z_BUF_ERROR => return error.OutputTooSmall,
c.Z_MEM_ERROR => return error.OutOfMemory,
else => return error.CompressionFailed,
}
const written = std.math.cast(usize, output_len)
orelse return error.InvalidOutputLength;
if (written > output.len) return error.InvalidOutputLength;
return written;
}
pub fn main() !void {
const input = "Zig e C trabalhando juntos! " ** 100;
var output: [4096]u8 = undefined;
const written = try compressData(input, &output);
const compressed = output[0..written];
std.debug.print("Original: {d}; comprimido: {d} bytes\n", .{
input.len, compressed.len,
});
}
# Debian/Ubuntu: instale zlib1g-dev antes de compilar
zig run compress.zig -lc -lz
O buffer fixo é suficiente para esta entrada de demonstração, não para qualquer entrada. Para um wrapper geral, obtenha a capacidade com compressBound, confira a conversão para usize e aloque esse espaço. Não leia nem transmita os bytes de output[written..]: eles não fazem parte do resultado e podem continuar indefinidos.
A mesma separação entre capacidade, tamanho escrito e status de retorno aparece em muitas APIs de sistemas. Em SQLite, porém, há outros contratos: funções como sqlite3_open retornam um handle por referência, e mensagens alocadas por sqlite3_exec devem ser liberadas com sqlite3_free. Veja o guia de bancos de dados para esse caso.
malloc/free e propriedade da memória
A regra não é “libere todo ponteiro recebido”. A regra é seguir o contrato de propriedade e usar o liberador correspondente.
- Memória obtida por
mallocpertence ao chamador e é liberada porfree. - Memória de um allocator Zig deve ser liberada pelo allocator correspondente, não por
free. - Um ponteiro emprestado por uma biblioteca pode não permitir liberação pelo chamador.
- Um buffer da stack só pode ser usado enquanto seu escopo e sua vida útil forem válidos. Se C guardar o ponteiro, ele precisa continuar válido após a chamada.
- Uma API que aloca um resultado pode exigir uma função própria de destruição. Documente essa função no wrapper.
O @ptrCast do primeiro exemplo apenas muda o tipo do ponteiro. Ele não aumenta o bloco alocado, não inicializa a memória e não comprova sua vida útil. Para tipos mais alinhados que u8, o alinhamento também precisa ser validado.
Exportar uma função Zig para chamar em C
Use tipos compatíveis com C e publique um header que corresponda à função exportada.
lib.zig:
export fn zig_soma(a: c_int, b: c_int) c_int {
return a + b;
}
zig_api.h:
#ifndef ZIG_API_H
#define ZIG_API_H
int zig_soma(int a, int b);
#endif
caller.c:
#include <stdio.h>
#include "zig_api.h"
int main(void) {
printf("Soma: %d\n", zig_soma(10, 20));
return 0;
}
Compile a biblioteca estática e o chamador:
zig build-lib lib.zig -static -O ReleaseSafe
zig cc caller.c ./liblib.a -o caller
./caller
# Soma: 30
Novamente, os valores de demonstração não causam overflow. Para exportar uma operação que pode falhar, prefira um código de status C e parâmetros de saída. Não exponha slices ou error unions Zig como se fossem tipos C. Um callback passado por ponteiro deve declarar a convenção de chamada C, por exemplo *const fn (?*anyopaque) callconv(.c) void em Zig 0.14.1.
Esse padrão permite migrar um projeto C para Zig por partes, mantendo uma interface C estável.
Tipos e compatibilidade ABI
Sempre que possível, use os tipos traduzidos do header em vez de reconstruí-los manualmente.
| Tipo C | Tipo ou cuidado em Zig |
|---|---|
int | c_int, não presumir que sempre corresponde a i32 |
unsigned long | c_ulong; seu tamanho varia conforme a ABI |
size_t | usize nos targets usuais do Zig; confira a tradução do header |
unsigned char * | Geralmente traduzido como [*c]u8; não contém comprimento |
const char * | Geralmente [*c]const u8; só trate como string se houver terminador zero |
void * | ?*anyopaque, sem informação sobre o tipo apontado |
| Struct definida manualmente para C | extern struct, com campos compatíveis |
| Enum C | Use a representação traduzida pelo compilador; Zig não tem extern enum |
Um ponteiro C traduzido ([*c]T) pode ser nulo. Já [*:0]const u8 expressa um contrato mais forte de string terminada em zero; converter entre os dois não comprova que a string tem terminador. O cheatsheet de interop C complementa esta referência.
translate-c e macros
Para inspecionar como o compilador entende um header:
zig translate-c -I. soma.h > soma-translated.zig
Use o mesmo target, includes e definições de pré-processador do projeto real. Dentro de @cImport, uma definição pode vir antes do include:
const c = @cImport({
@cDefine("_POSIX_C_SOURCE", "200809L");
@cInclude("unistd.h");
});
Nem toda construção C ou macro pode ser traduzida como uma função Zig utilizável. Macros complexas podem exigir um pequeno wrapper C. A tradução também não equivale a uma migração para código Zig idiomático.
Erros comuns ao integrar Zig e C
| Sintoma | O que verificar |
|---|---|
| Header não encontrado | Pacote de desenvolvimento instalado e caminho de include (-I) |
| Símbolo indefinido no linker | Implementação .c, arquivo de biblioteca ou flag como -lz/-lsqlite3 |
| Crash ao ler uma string | Ponteiro nulo, ausência de terminador zero ou vida útil encerrada |
Saída truncada ou Z_BUF_ERROR | Capacidade insuficiente; não confundir capacidade com tamanho final |
| Funciona em um target e falha em outro | Tipos C, layout, alinhamento e bibliotecas da arquitetura de destino |
O guia de cross-compilation ajuda com targets. Zig pode fornecer libc para targets suportados, mas não fornece automaticamente toda biblioteca externa, como SQLite ou zlib, para qualquer arquitetura.
Perguntas frequentes
Preciso de libc para todo @cImport?
Não. Importar um header que define apenas tipos ou funções próprias não exige, por si só, libc. Use -lc ou linkLibC() quando o código ou as bibliotecas dependerem dela. Os exemplos deste guia usam libc.
Chamar C em Zig tem overhead de FFI?
Uma chamada com ABI C pode ser direta, sem uma camada obrigatória de marshalling. Isso não significa custo total zero: conversões, cópias, alocações e o próprio trabalho da biblioteca continuam tendo custo. Wrappers precisam ser medidos no contexto da aplicação.
Posso passar um slice Zig para uma função C?
Não como parâmetro C comum. Passe seu ponteiro e comprimento em parâmetros separados, ou use uma struct extern cujo layout esteja declarado também no header C. Para uma string C, garanta ainda o terminador zero.
Como evitar vazamentos na interoperabilidade?
Identifique quem é dono de cada resultado, use o liberador exigido pela API e registre o cleanup com defer assim que adquirir o recurso. Confira caminhos de erro e o caso em que a função devolve um recurso mesmo sem sucesso.
Checklist antes de publicar uma integração
- Compilar com a versão de Zig documentada e o target correto.
- Distinguir header, implementação e biblioteca vinculada.
- Conferir conversões de comprimento para os tipos C.
- Testar ponteiros nulos, buffers pequenos e retornos de erro.
- Validar propriedade, vida útil e liberador de cada recurso.
- Não expor tipos Zig sem representação C na interface exportada.
Continue com o guia de testes em Zig e o guia de tratamento de erros para transformar as chamadas C em wrappers previsíveis.