Portar uma Biblioteca C para Zig

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

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

  1. Mapear a API pública: Liste todas as funções e tipos públicos da biblioteca C
  2. Criar a estrutura de tipos em Zig: Traduzir structs, enums, typedefs
  3. Implementar função por função: Com testes para cada uma
  4. Manter compatibilidade C: Exportar funções com export para 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.

Continue aprendendo Zig

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