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
@ptrCastcom 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
- Facade — Simplificar interface complexa
- Decorator — Adicionar comportamento sem alterar interface
- Interop com C — Wrapping de APIs C
- Type Erasure — Apagar tipo para interface comum
- Troubleshooting: Link C — Problemas de linkagem