Bibliotecas de Serialização em Zig — MessagePack, Protobuf e Mais

Bibliotecas de Serialização em Zig — MessagePack, Protobuf e Mais

Serialização eficiente de dados é crucial para comunicação entre serviços, armazenamento e protocolos de rede. O Zig oferece vantagens únicas para serialização: structs packed com layout de memória controlado, comptime para gerar código de serialização automaticamente e interoperabilidade C para usar bibliotecas existentes. Além do suporte a JSON na std, o ecossistema oferece diversas opções binárias.

Serialização Binária Nativa

O Zig permite serialização direta de structs packed:

const std = @import("std");

const Mensagem = packed struct {
    tipo: u8,
    flags: u8,
    tamanho: u16,
    timestamp: u64,
    checksum: u32,
};

pub fn serializar(msg: *const Mensagem) [@sizeOf(Mensagem)]u8 {
    return @bitCast(msg.*);
}

pub fn deserializar(bytes: [@sizeOf(Mensagem)]u8) Mensagem {
    return @bitCast(bytes);
}

pub fn enviarPelaRede(stream: std.net.Stream, msg: *const Mensagem) !void {
    const bytes = serializar(msg);
    try stream.writeAll(&bytes);
}

pub fn receberDaRede(stream: std.net.Stream) !Mensagem {
    var bytes: [@sizeOf(Mensagem)]u8 = undefined;
    _ = try stream.readAll(&bytes);
    return deserializar(bytes);
}

MessagePack

O MessagePack é um formato binário compacto e rápido, ideal para comunicação entre serviços:

const msgpack = @import("zig-msgpack");

const Produto = struct {
    id: u64,
    nome: []const u8,
    preco: f64,
    tags: []const []const u8,
};

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const produto = Produto{
        .id = 42,
        .nome = "Widget Premium",
        .preco = 99.90,
        .tags = &.{ "eletrônicos", "premium" },
    };

    // Serializar
    var buf: [1024]u8 = undefined;
    const encoded = try msgpack.encode(Produto, &buf, produto);

    // Deserializar
    const decoded = try msgpack.decode(Produto, allocator, encoded);
    std.debug.print("Produto: {s} - R${d:.2}\n", .{ decoded.nome, decoded.preco });
}

Protocol Buffers

zig-protobuf

const protobuf = @import("zig-protobuf");

// Gerado a partir de .proto
const Usuario = protobuf.Message(struct {
    id: u64 = 0,       // field 1
    nome: []const u8 = "",  // field 2
    email: []const u8 = "", // field 3
    idade: u32 = 0,     // field 4
});

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Criar mensagem
    var usuario = Usuario{
        .id = 1,
        .nome = "Maria",
        .email = "maria@exemplo.com",
        .idade = 28,
    };

    // Serializar
    var buf: [256]u8 = undefined;
    const tamanho = try usuario.encode(&buf);

    // Deserializar
    const decoded = try Usuario.decode(allocator, buf[0..tamanho]);
    std.debug.print("Usuário: {s}\n", .{decoded.nome});
}

CBOR

O CBOR (Concise Binary Object Representation) é um formato binário baseado no modelo de dados JSON:

const cbor = @import("zig-cbor");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Serializar para CBOR
    var buf: [1024]u8 = undefined;
    var encoder = cbor.Encoder.init(&buf);

    try encoder.encodeMapHeader(3);
    try encoder.encodeString("nome");
    try encoder.encodeString("João");
    try encoder.encodeString("idade");
    try encoder.encodeUint(30);
    try encoder.encodeString("ativo");
    try encoder.encodeBool(true);

    const encoded = encoder.getWritten();

    // Deserializar
    var decoder = cbor.Decoder.init(encoded, allocator);
    const valor = try decoder.decode();
    _ = valor;
}

Serialização com Comptime

Uma das vantagens mais poderosas do Zig é gerar serializadores em comptime:

fn AutoSerializer(comptime T: type) type {
    return struct {
        pub fn serialize(value: T, writer: anytype) !void {
            const info = @typeInfo(T);
            switch (info) {
                .Struct => |s| {
                    inline for (s.fields) |field| {
                        try serializeField(field.type, @field(value, field.name), writer);
                    }
                },
                else => @compileError("Tipo não suportado: " ++ @typeName(T)),
            }
        }

        fn serializeField(comptime FieldType: type, value: FieldType, writer: anytype) !void {
            switch (@typeInfo(FieldType)) {
                .Int => try writer.writeInt(FieldType, value, .little),
                .Float => {
                    const IntType = std.meta.Int(.unsigned, @bitSizeOf(FieldType));
                    try writer.writeInt(IntType, @bitCast(value), .little);
                },
                .Bool => try writer.writeByte(if (value) 1 else 0),
                .Pointer => |ptr| {
                    if (ptr.size == .Slice) {
                        try writer.writeInt(u32, @intCast(value.len), .little);
                        try writer.writeAll(value);
                    }
                },
                else => {},
            }
        }

        pub fn deserialize(reader: anytype, allocator: std.mem.Allocator) !T {
            var result: T = undefined;
            const info = @typeInfo(T);
            inline for (info.Struct.fields) |field| {
                @field(result, field.name) = try deserializeField(
                    field.type, reader, allocator,
                );
            }
            return result;
        }

        fn deserializeField(comptime FieldType: type, reader: anytype, allocator: std.mem.Allocator) !FieldType {
            _ = allocator;
            switch (@typeInfo(FieldType)) {
                .Int => return try reader.readInt(FieldType, .little),
                .Float => {
                    const IntType = std.meta.Int(.unsigned, @bitSizeOf(FieldType));
                    const bits = try reader.readInt(IntType, .little);
                    return @bitCast(bits);
                },
                .Bool => return (try reader.readByte()) != 0,
                else => return undefined,
            }
        }
    };
}

// Uso
const MeuDado = struct {
    id: u32,
    valor: f64,
    ativo: bool,
};

const Serializer = AutoSerializer(MeuDado);

test "serialização automática" {
    var buf: [256]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buf);

    const original = MeuDado{ .id = 42, .valor = 3.14, .ativo = true };
    try Serializer.serialize(original, stream.writer());

    stream.pos = 0;
    const restaurado = try Serializer.deserialize(stream.reader(), std.testing.allocator);

    try std.testing.expectEqual(original.id, restaurado.id);
}

Comparação de Formatos

FormatoTamanhoVelocidadeSchemaLegívelUso Ideal
JSONGrandeMédiaNãoSimAPIs web, config
MessagePackPequenoRápidaNãoNãoRPC, cache
ProtobufPequenoRápidaSimNãogRPC, microserviços
CBORPequenoRápidaNãoNãoIoT, COSE
Packed structMínimoMáximaImplícitoNãoProtocolos internos
FlatBuffersMédioMáximaSimNãoJogos, mobile

Boas Práticas

  1. Escolha o formato pelo caso de uso: JSON para interop, MessagePack para performance, Protobuf para evolução de schema
  2. Versione seus formatos: Adicione campos de versão para compatibilidade futura
  3. Valide após deserialização: Verifique invariantes dos dados
  4. Use comptime: Gere serializadores automaticamente para reduzir código manual
  5. Considere endianness: Use little-endian para x86/ARM, big-endian para rede

Próximos Passos

Explore as bibliotecas JSON para serialização textual, as bibliotecas de compressão para otimizar tamanho, e as bibliotecas de rede para transmissão eficiente. Consulte nossos tutoriais para projetos práticos.

Continue aprendendo Zig

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