Introdução
Portar uma biblioteca C para Zig pode significar duas coisas: criar um wrapper Zig idiomático sobre a biblioteca C existente, ou reescrever a biblioteca em Zig puro. Este guia cobre ambas as abordagens e ajuda você a decidir qual é mais adequada.
Para interoperabilidade geral, veja Interoperabilidade C-Zig. Para conversão de ponteiros, consulte Converter Ponteiros C para Zig.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja Como Instalar Zig
- Código fonte da biblioteca C a ser portada
- Familiaridade com Zig e C. Consulte Introdução ao Zig
Estratégia 1: Wrapper Zig (Recomendada para começar)
Criar um wrapper Zig sobre a biblioteca C é mais rápido e mantém a compatibilidade:
Passo 1: Integrar a Biblioteca C no build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "minha-lib-zig",
.root_source_file = b.path("src/wrapper.zig"),
.target = target,
.optimize = optimize,
});
// Compilar a biblioteca C como parte do build
lib.addCSourceFiles(.{
.files = &.{
"c-src/lib.c",
"c-src/utils.c",
"c-src/parser.c",
},
.flags = &.{ "-std=c11", "-Wall", "-DNDEBUG" },
});
lib.addIncludePath(b.path("c-src/include"));
lib.linkLibC();
b.installArtifact(lib);
}
Passo 2: Importar e Wrappear a API
// src/wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("minha_lib.h");
});
pub const Erro = error{
AlocacaoFalhou,
ArquivoNaoEncontrado,
FormatoInvalido,
Desconhecido,
};
fn traduzirErro(codigo: c_int) Erro {
return switch (codigo) {
c.ERR_ALLOC => error.AlocacaoFalhou,
c.ERR_NOT_FOUND => error.ArquivoNaoEncontrado,
c.ERR_FORMAT => error.FormatoInvalido,
else => error.Desconhecido,
};
}
pub const Parser = struct {
handle: *c.parser_t,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !Parser {
const handle = c.parser_create() orelse return error.AlocacaoFalhou;
return .{
.handle = handle,
.allocator = allocator,
};
}
pub fn deinit(self: *Parser) void {
c.parser_destroy(self.handle);
}
pub fn parsear(self: *Parser, dados: []const u8) !Resultado {
var resultado: c.result_t = undefined;
const rc = c.parser_parse(self.handle, dados.ptr, dados.len, &resultado);
if (rc != 0) return traduzirErro(rc);
return Resultado.fromC(self.allocator, &resultado);
}
};
pub const Resultado = struct {
tipo: Tipo,
valor: []const u8,
allocator: std.mem.Allocator,
pub fn fromC(allocator: std.mem.Allocator, c_result: *const c.result_t) !Resultado {
const valor = std.mem.span(c_result.value);
const copia = try allocator.dupe(u8, valor);
return .{
.tipo = @enumFromInt(c_result.type),
.valor = copia,
.allocator = allocator,
};
}
pub fn deinit(self: *Resultado) void {
self.allocator.free(self.valor);
}
const Tipo = enum(c_int) {
texto = c.TYPE_TEXT,
numero = c.TYPE_NUMBER,
booleano = c.TYPE_BOOL,
};
};
Passo 3: Criar API Idiomática
O wrapper deve oferecer uma API que se sinta natural em Zig:
// Uso do wrapper — se sente como código Zig nativo
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var parser = try Parser.init(allocator);
defer parser.deinit();
var resultado = try parser.parsear("{\"chave\": \"valor\"}");
defer resultado.deinit();
std.debug.print("Tipo: {} - Valor: {s}\n", .{ resultado.tipo, resultado.valor });
}
test "parser básico" {
var parser = try Parser.init(std.testing.allocator);
defer parser.deinit();
var resultado = try parser.parsear("42");
defer resultado.deinit();
try std.testing.expectEqual(Resultado.Tipo.numero, resultado.tipo);
}
Estratégia 2: Reescrita em Zig Puro
Para bibliotecas pequenas ou quando você quer eliminar a dependência de C:
Quando Reescrever
- Biblioteca C é pequena (< 5000 linhas)
- Você quer eliminar dependências C
- A biblioteca tem bugs conhecidos que são difíceis de corrigir em C
- Você quer aproveitar features de Zig (comptime, safety checks)
Processo
- Mapear a API pública: Liste todas as funções e tipos públicos da biblioteca C
- Criar a estrutura de tipos em Zig: Traduzir structs, enums, typedefs
- Implementar função por função: Com testes para cada uma
- Manter compatibilidade C: Exportar funções com
exportpara uso por código C existente
// Exportar para consumo por código C
pub export fn parser_create() ?*Parser {
// ...
}
pub export fn parser_destroy(p: *Parser) void {
p.deinit();
}
Veja Substituir Macros C por Comptime e Substituir malloc/free por Allocators para padrões de conversão.
Padrões Comuns de Conversão
Callbacks C para Zig
// API C com callback
typedef void (*callback_fn)(void* ctx, const char* msg);
void registrar(callback_fn cb, void* ctx);
// Wrapper Zig
pub fn registrar(comptime callback: fn ([]const u8) void) void {
const Wrapper = struct {
fn cCallback(ctx: ?*anyopaque, msg: [*:0]const u8) callconv(.C) void {
_ = ctx;
const msg_slice = std.mem.span(msg);
callback(msg_slice);
}
};
c.registrar(Wrapper.cCallback, null);
}
Gerenciamento de Memória na Fronteira
Quando C aloca e Zig precisa liberar (ou vice-versa), documente claramente:
/// Retorna string alocada com o allocator fornecido.
/// O chamador é responsável por liberar com allocator.free().
pub fn obterNome(allocator: std.mem.Allocator) ![]u8 {
const c_nome = c.get_name(); // C aloca internamente
defer c.free_name(c_nome); // Liberar a cópia C
// Copiar para memória gerenciada pelo allocator Zig
return allocator.dupe(u8, std.mem.span(c_nome));
}
Publicar como Pacote Zig
build.zig.zon
.{
.name = "minha-lib",
.version = "1.0.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"c-src",
},
}
Expor o Módulo
// Em build.zig
const mod = b.addModule("minha-lib", .{
.root_source_file = b.path("src/wrapper.zig"),
});
// Consumidores podem usar:
// const minha_lib = @import("minha-lib");
Testes
Testes são essenciais para garantir que o wrapper se comporta corretamente:
test "wrapper produz resultados consistentes com a lib C" {
var parser = try Parser.init(std.testing.allocator);
defer parser.deinit();
// Testar com dados válidos
var resultado = try parser.parsear("dados válidos");
defer resultado.deinit();
try std.testing.expect(resultado.valor.len > 0);
// Testar com dados inválidos
const erro = parser.parsear("");
try std.testing.expectError(error.FormatoInvalido, erro);
}
Veja Testes Unitários e Testes com Allocator.
Conclusão
Portar uma biblioteca C para Zig é um processo que pode ser feito incrementalmente. Comece com um wrapper que oferece API Zig idiomática sobre a biblioteca C existente. Com o tempo, migre a implementação para Zig puro se desejável.
O resultado é uma biblioteca que se integra naturalmente com código Zig, com gerenciamento de memória explícito via allocators, error handling tipado, e testes integrados.
Para mais, veja Chamar Funções C de Zig, Guia de Migração: C para Zig e Como Migrar um Projeto C para Zig.