Vtable Interface em Zig — O que é e Como Usar

Vtable Interface em Zig — O que é e Como Usar

Definição

Uma vtable (virtual table) em Zig é uma struct de ponteiros de função usada para implementar polimorfismo em runtime. Como Zig não tem herança, traits ou interfaces na linguagem, vtables são o padrão idiomático para quando você precisa de dispatch dinâmico — ou seja, chamar métodos diferentes dependendo do tipo concreto, sem conhecer o tipo em tempo de compilação.

A biblioteca padrão do Zig usa vtables extensivamente. O tipo std.mem.Allocator é o exemplo mais conhecido: ele armazena um ponteiro para implementação e uma vtable com as funções alloc, resize e free.

Por que Vtable Interface Importa

  1. Polimorfismo em runtime: Armazenar diferentes tipos em uma mesma variável.
  2. Extensibilidade: Novos tipos podem implementar a interface sem modificar código existente.
  3. Padrão da std lib: Allocator, Writer e Reader da std usam vtables.
  4. Controle total: O programador controla o layout de memória e o overhead.

Exemplo Prático

Interface Forma com Vtable

const std = @import("std");

const Forma = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        areaFn: *const fn (ptr: *anyopaque) f64,
        nomeFn: *const fn (ptr: *anyopaque) []const u8,
    };

    pub fn area(self: Forma) f64 {
        return self.vtable.areaFn(self.ptr);
    }

    pub fn nome(self: Forma) []const u8 {
        return self.vtable.nomeFn(self.ptr);
    }

    pub fn init(ptr: anytype) Forma {
        const T = @TypeOf(ptr);
        const PtrInfo = @typeInfo(T);
        const Child = PtrInfo.pointer.child;

        return .{
            .ptr = @ptrCast(ptr),
            .vtable = &.{
                .areaFn = @ptrCast(&struct {
                    pub fn call(p: *anyopaque) f64 {
                        const self: *Child = @ptrCast(@alignCast(p));
                        return self.area();
                    }
                }.call),
                .nomeFn = @ptrCast(&struct {
                    pub fn call(p: *anyopaque) []const u8 {
                        const self: *Child = @ptrCast(@alignCast(p));
                        return self.nome();
                    }
                }.call),
            },
        };
    }
};

Implementando a Interface

const std = @import("std");

const Circulo = struct {
    raio: f64,

    pub fn area(self: *Circulo) f64 {
        return std.math.pi * self.raio * self.raio;
    }

    pub fn nome(_: *Circulo) []const u8 {
        return "Círculo";
    }
};

const Retangulo = struct {
    largura: f64,
    altura: f64,

    pub fn area(self: *Retangulo) f64 {
        return self.largura * self.altura;
    }

    pub fn nome(_: *Retangulo) []const u8 {
        return "Retângulo";
    }
};

pub fn main() void {
    var circ = Circulo{ .raio = 5.0 };
    var ret = Retangulo{ .largura = 4.0, .altura = 6.0 };

    // Ambos podem ser tratados como Forma
    const formas = [_]Forma{
        Forma.init(&circ),
        Forma.init(&ret),
    };

    for (formas) |forma| {
        std.debug.print("{s}: área = {d:.2}\n", .{ forma.nome(), forma.area() });
    }
}

Padrão da std lib: Allocator

const std = @import("std");

// std.mem.Allocator é uma vtable interface!
// Qualquer allocator pode ser usado uniformemente:

fn alocarEUsar(allocator: std.mem.Allocator) !void {
    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);
    @memset(dados, 'Z');
    std.debug.print("Primeiros bytes: {s}\n", .{dados[0..5]});
}

pub fn main() !void {
    // Mesma função, diferentes implementações
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    try alocarEUsar(gpa.allocator());           // GPA
    try alocarEUsar(std.heap.page_allocator);    // Page allocator
}

Quando Usar Vtable vs anytype

Critérioanytype (comptime)Vtable (runtime)
Tipo conhecido em comptimeSimNunca
Armazenar em coleçãoApenas mesmo tipoTipos diferentes
PerformanceInlining possívelIndireção por ponteiro
Tamanho do binárioMaior (duplicação)Menor
Uso típicoFunções genéricasPlugins, coleções heterogêneas

Armadilhas Comuns

  • Lifetime: O ponteiro na vtable deve apontar para dados que sobrevivem ao uso da interface. Cuidado com variáveis locais.
  • Alinhamento: @ptrCast e @alignCast são necessários ao converter *anyopaque de volta para o tipo concreto.
  • Overhead: Cada chamada via vtable tem overhead de indireção. Para código hot-path, prefira anytype (comptime).
  • Complexidade: Vtables manuais são verbosas. Avalie se anytype ou um union(enum) seriam mais simples.

Termos Relacionados

Tutoriais Relacionados

Continue aprendendo Zig

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