Cheatsheet: Adapter em Zig

Adapter em Zig

O padrão Adapter permite que interfaces incompatíveis trabalhem juntas, convertendo a interface de uma struct em outra que o código cliente espera. Em Zig, esse padrão é muito usado para envolver APIs C em interfaces idiomáticas Zig, ou para unificar diferentes implementações sob uma interface comum.

Quando Usar

  • Wrapping de bibliotecas C para interface idiomática Zig
  • Unificar diferentes fontes de dados sob mesma interface
  • Converter entre formatos de dados incompatíveis
  • Integrar bibliotecas de terceiros com API diferente da esperada

Adapter para API C

const std = @import("std");
const c = @cImport(@cInclude("stdio.h"));

// API C retorna ponteiro ou NULL, usa errno
// Adapter converte para interface Zig com error unions

const ArquivoAdaptado = struct {
    handle: *c.FILE,

    const Erro = error{
        FalhaAoAbrir,
        FalhaAoLer,
        FalhaAoEscrever,
    };

    pub fn abrir(caminho: [:0]const u8, modo: [:0]const u8) Erro!ArquivoAdaptado {
        const handle = c.fopen(caminho.ptr, modo.ptr);
        if (handle == null) return error.FalhaAoAbrir;
        return .{ .handle = handle.? };
    }

    pub fn fechar(self: *ArquivoAdaptado) void {
        _ = c.fclose(self.handle);
    }

    pub fn escrever(self: *ArquivoAdaptado, dados: []const u8) Erro!usize {
        const escrito = c.fwrite(dados.ptr, 1, dados.len, self.handle);
        if (escrito == 0) return error.FalhaAoEscrever;
        return escrito;
    }

    pub fn ler(self: *ArquivoAdaptado, buffer: []u8) Erro!usize {
        const lido = c.fread(buffer.ptr, 1, buffer.len, self.handle);
        if (lido == 0 and c.ferror(self.handle) != 0) return error.FalhaAoLer;
        return lido;
    }
};

pub fn main() !void {
    var arquivo = try ArquivoAdaptado.abrir("/tmp/teste.txt", "w");
    defer arquivo.fechar();

    _ = try arquivo.escrever("Olá do adapter!\n");
}

Adapter de Interface Genérica

const std = @import("std");

// Interface comum
const FonteDados = struct {
    ptr: *anyopaque,
    lerFn: *const fn (*anyopaque, []u8) ?[]const u8,

    pub fn ler(self: FonteDados, buffer: []u8) ?[]const u8 {
        return self.lerFn(self.ptr, buffer);
    }
};

// Adaptar ArrayList para FonteDados
const ArrayListAdapter = struct {
    lista: *std.ArrayList([]const u8),
    indice: usize = 0,

    pub fn fonteDados(self: *ArrayListAdapter) FonteDados {
        return .{
            .ptr = @ptrCast(self),
            .lerFn = @ptrCast(&proximo),
        };
    }

    fn proximo(self: *ArrayListAdapter, buffer: []u8) ?[]const u8 {
        _ = buffer;
        if (self.indice >= self.lista.items.len) return null;
        const item = self.lista.items[self.indice];
        self.indice += 1;
        return item;
    }
};

// Adaptar slice estático para FonteDados
const SliceAdapter = struct {
    dados: []const []const u8,
    indice: usize = 0,

    pub fn fonteDados(self: *SliceAdapter) FonteDados {
        return .{
            .ptr = @ptrCast(self),
            .lerFn = @ptrCast(&proximo),
        };
    }

    fn proximo(self: *SliceAdapter, buffer: []u8) ?[]const u8 {
        _ = buffer;
        if (self.indice >= self.dados.len) return null;
        const item = self.dados[self.indice];
        self.indice += 1;
        return item;
    }
};

// Código cliente trabalha com FonteDados sem saber a origem
fn processarDados(fonte: FonteDados) void {
    var buf: [1024]u8 = undefined;
    while (fonte.ler(&buf)) |dado| {
        std.debug.print("Dado: {s}\n", .{dado});
    }
}

Adapter com comptime para Múltiplos Backends

Uma técnica poderosa é usar comptime para criar adapters sem custo de runtime, resolvendo a implementação concreta em tempo de compilação:

const std = @import("std");

fn StorageAdapter(comptime Backend: type) type {
    return struct {
        backend: Backend,

        pub fn salvar(self: *@This(), chave: []const u8, valor: []const u8) !void {
            // Verifica em comptime que o backend tem o método correto
            if (!@hasDecl(Backend, "write")) {
                @compileError("Backend deve implementar write(chave, valor)");
            }
            try self.backend.write(chave, valor);
        }

        pub fn carregar(self: *@This(), chave: []const u8) !?[]const u8 {
            return self.backend.read(chave);
        }
    };
}

// Backend em memória (para testes)
const MemBackend = struct {
    dados: std.StringHashMap([]const u8),

    pub fn write(self: *MemBackend, chave: []const u8, valor: []const u8) !void {
        try self.dados.put(chave, valor);
    }
    pub fn read(self: *MemBackend, chave: []const u8) !?[]const u8 {
        return self.dados.get(chave);
    }
};

Neste caso, o adapter é resolvido em comptime — o binário final contém apenas as chamadas diretas ao MemBackend, sem indireção via ponteiro de função.

Considerações de Performance

  • Adapter para C via @cImport: o overhead é praticamente zero — são chamadas diretas às funções C com conversão de tipos, não há alocação extra.
  • Adapter com anyopaque + ponteiro de função: introduz uma indireção por chamada. Aceitável na maioria dos casos, mas evite no hot path de renderização ou parsing intenso.
  • Adapter comptime: custo zero em runtime. O compilador resolve tudo estáticamentee pode inlinar as chamadas.
  • Use @ptrCast com cuidado — certifique-se de que o alinhamento do ponteiro está correto. O compilador vai alertar sobre mismatches de alinhamento.

Erros Comuns

Esquecer de propagar o lifetime: o adapter não deve viver mais que o objeto que wrapa. Se o adapter guarda um ponteiro para o objeto original, certifique-se de que o original não é destruído antes.

Assinar o tipo errado em @ptrCast: ao criar interfaces com *anyopaque, a função que recebe o ponteiro deve ter assinatura idêntica à que foi convertida. O compilador não consegue verificar isso em todos os casos.

Não tratar todos os erros da API C: APIs C frequentemente sinalizam erros via valores de retorno negativos, errno, ou ponteiros nulos. Adapters devem mapear todos esses casos para error unions Zig — nunca ignore um valor de retorno de função C.

Perguntas Frequentes

Qual é a diferença entre Adapter e Facade? O Adapter converte uma interface em outra que o cliente já espera — é um “tradutor”. A Facade cria uma interface mais simples sobre um sistema complexo, sem necessariamente mudar a forma como o cliente se comunica.

Devo criar um adapter para cada biblioteca C que uso? Para bibliotecas grandes (SQLite, OpenSSL, libcurl), vale criar um adapter idiomático completo. Para poucas funções de uma biblioteca auxiliar, um wrapper de função simples é suficiente e mais fácil de manter.

Como testar um adapter para API C sem ter a biblioteca disponível? Crie um MockBackend que implementa a mesma interface que o adapter expõe, usando o padrão de comptime. Nos testes, injete o mock em vez do adapter real.

Quando Evitar

  • Quando as interfaces já são compatíveis
  • Quando um simples wrapper de função resolve
  • Se o adapter adiciona complexidade sem benefício claro
  • Quando é possível modificar a interface original

Veja Também

Continue aprendendo Zig

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