Desafio de Código Zig 3 — Programação de Sistemas

Desafio de Código Zig #3 — Programação de Sistemas

Programação de sistemas é o domínio natural do Zig. Estes desafios cobrem temas que aparecem em entrevistas para posições de infraestrutura, sistemas operacionais, embedded e backend de alta performance: alocadores customizados, I/O eficiente, concorrência, serialização binária e ferramentas de linha de comando.

Desafio 1: Arena Allocator Simples

Problema: Implemente um arena allocator que aloca de um buffer fixo e libera tudo de uma vez.

const std = @import("std");

const ArenaSimples = struct {
    const Self = @This();

    buffer: []u8,
    offset: usize,

    pub fn init(buffer: []u8) Self {
        return .{ .buffer = buffer, .offset = 0 };
    }

    pub fn alloc(self: *Self, comptime T: type, n: usize) ![]T {
        const tamanho = n * @sizeOf(T);
        const alinhamento = @alignOf(T);

        // Alinha o offset
        const offset_alinhado = std.mem.alignForward(usize, self.offset, alinhamento);

        if (offset_alinhado + tamanho > self.buffer.len) {
            return error.OutOfMemory;
        }

        const ptr: [*]T = @ptrCast(@alignCast(self.buffer.ptr + offset_alinhado));
        self.offset = offset_alinhado + tamanho;
        return ptr[0..n];
    }

    pub fn create(self: *Self, comptime T: type) !*T {
        const slice = try self.alloc(T, 1);
        return &slice[0];
    }

    /// Reseta a arena — "libera" tudo instantaneamente.
    pub fn reset(self: *Self) void {
        self.offset = 0;
    }

    /// Espaço utilizado.
    pub fn utilizado(self: *const Self) usize {
        return self.offset;
    }

    /// Espaço disponível.
    pub fn disponivel(self: *const Self) usize {
        return self.buffer.len - self.offset;
    }
};

test "arena allocator simples" {
    const testing = std.testing;

    var buffer: [1024]u8 = undefined;
    var arena = ArenaSimples.init(&buffer);

    // Aloca inteiros
    const nums = try arena.alloc(i32, 10);
    for (nums, 0..) |*n, i| {
        n.* = @intCast(i * 10);
    }
    try testing.expectEqual(@as(i32, 50), nums[5]);

    // Aloca struct
    const Ponto = struct { x: f64, y: f64 };
    const p = try arena.create(Ponto);
    p.* = .{ .x = 3.14, .y = 2.72 };
    try testing.expectEqual(@as(f64, 3.14), p.x);

    // Reset libera tudo
    const usado = arena.utilizado();
    try testing.expect(usado > 0);
    arena.reset();
    try testing.expectEqual(@as(usize, 0), arena.utilizado());
}

Desafio 2: Pool Allocator de Tamanho Fixo

Problema: Implemente um allocator que gerencia blocos de tamanho fixo (zero fragmentation).

const std = @import("std");

fn PoolAllocator(comptime T: type, comptime capacidade: usize) type {
    return struct {
        const Self = @This();

        blocos: [capacidade]T = undefined,
        livres: [capacidade]u32 = undefined,
        topo_livres: usize = capacidade,
        alocados: usize = 0,

        pub fn init() Self {
            var self = Self{};
            // Inicializa lista de blocos livres
            for (0..capacidade) |i| {
                self.livres[i] = @intCast(i);
            }
            return self;
        }

        pub fn alocar(self: *Self) ?*T {
            if (self.topo_livres == 0) return null;
            self.topo_livres -= 1;
            const idx = self.livres[self.topo_livres];
            self.alocados += 1;
            return &self.blocos[idx];
        }

        pub fn liberar(self: *Self, ptr: *T) void {
            // Calcula índice a partir do ponteiro
            const base = @intFromPtr(&self.blocos[0]);
            const alvo = @intFromPtr(ptr);
            const idx: u32 = @intCast((alvo - base) / @sizeOf(T));

            std.debug.assert(idx < capacidade);
            self.livres[self.topo_livres] = idx;
            self.topo_livres += 1;
            self.alocados -= 1;
        }

        pub fn utilizacao(self: *const Self) f64 {
            return @as(f64, @floatFromInt(self.alocados)) / @as(f64, @floatFromInt(capacidade));
        }
    };
}

test "pool allocator" {
    const testing = std.testing;

    const Particula = struct { x: f32, y: f32, vx: f32, vy: f32 };
    var pool = PoolAllocator(Particula, 100).init();

    // Aloca partículas
    var ptrs: [10]*Particula = undefined;
    for (&ptrs) |*p| {
        p.* = pool.alocar() orelse unreachable;
        p.*.* = .{ .x = 0, .y = 0, .vx = 1, .vy = 1 };
    }

    try testing.expectEqual(@as(usize, 10), pool.alocados);

    // Libera algumas
    pool.liberar(ptrs[3]);
    pool.liberar(ptrs[7]);
    try testing.expectEqual(@as(usize, 8), pool.alocados);

    // Reutiliza blocos liberados
    const p_novo = pool.alocar() orelse unreachable;
    p_novo.* = .{ .x = 99, .y = 99, .vx = 0, .vy = 0 };
    try testing.expectEqual(@as(usize, 9), pool.alocados);
}

Desafio 3: Leitor de Arquivo Linha por Linha

Problema: Implemente um leitor eficiente que processa um arquivo linha por linha sem carregar tudo na memória.

const std = @import("std");

/// Conta linhas, palavras e caracteres de um arquivo (como wc).
fn contarArquivo(caminho: []const u8) !struct {
    linhas: u64,
    palavras: u64,
    caracteres: u64,
} {
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();

    var buf_reader = std.io.bufferedReader(arquivo.reader());
    const reader = buf_reader.reader();

    var linhas: u64 = 0;
    var palavras: u64 = 0;
    var caracteres: u64 = 0;

    var buf: [8192]u8 = undefined;
    while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| {
        const linha = maybe_line orelse break;
        linhas += 1;
        caracteres += linha.len + 1; // +1 para newline

        // Conta palavras
        var em_palavra = false;
        for (linha) |c| {
            if (c == ' ' or c == '\t') {
                em_palavra = false;
            } else if (!em_palavra) {
                em_palavra = true;
                palavras += 1;
            }
        }
    } else |_| {}

    return .{
        .linhas = linhas,
        .palavras = palavras,
        .caracteres = caracteres,
    };
}

test "contador funciona com buffer" {
    // Teste com FixedBufferStream em vez de arquivo
    const dados = "hello world\nzig is great\n";
    var stream = std.io.fixedBufferStream(dados);
    var reader = stream.reader();

    var linhas: u64 = 0;
    var buf: [256]u8 = undefined;
    while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe| {
        _ = maybe orelse break;
        linhas += 1;
    } else |_| {}

    try std.testing.expectEqual(@as(u64, 2), linhas);
}

Desafio 4: Produtor/Consumidor com Threads

Problema: Implemente o padrão produtor/consumidor thread-safe.

const std = @import("std");

fn FilaSegura(comptime T: type, comptime capacidade: usize) type {
    return struct {
        const Self = @This();

        buffer: [capacidade]T = undefined,
        leitura: usize = 0,
        escrita: usize = 0,
        contagem: std.atomic.Value(usize) = std.atomic.Value(usize).init(0),
        mutex: std.Thread.Mutex = .{},
        cond_nao_vazio: std.Thread.Condition = .{},
        cond_nao_cheio: std.Thread.Condition = .{},

        pub fn produzir(self: *Self, item: T) void {
            self.mutex.lock();
            defer self.mutex.unlock();

            // Espera até ter espaço
            while (self.contagem.load(.acquire) >= capacidade) {
                self.cond_nao_cheio.wait(&self.mutex);
            }

            self.buffer[self.escrita] = item;
            self.escrita = (self.escrita + 1) % capacidade;
            _ = self.contagem.fetchAdd(1, .release);
            self.cond_nao_vazio.signal();
        }

        pub fn consumir(self: *Self) T {
            self.mutex.lock();
            defer self.mutex.unlock();

            // Espera até ter item
            while (self.contagem.load(.acquire) == 0) {
                self.cond_nao_vazio.wait(&self.mutex);
            }

            const item = self.buffer[self.leitura];
            self.leitura = (self.leitura + 1) % capacidade;
            _ = self.contagem.fetchSub(1, .release);
            self.cond_nao_cheio.signal();
            return item;
        }
    };
}

test "produtor/consumidor" {
    const testing = std.testing;

    var fila = FilaSegura(i32, 10){};
    var soma_produzida: i64 = 0;
    var soma_consumida = std.atomic.Value(i64).init(0);

    const num_itens = 100;

    // Produtor
    const produtor = try std.Thread.spawn(.{}, struct {
        fn func(f: *FilaSegura(i32, 10), soma: *i64) void {
            for (0..num_itens) |i| {
                const val: i32 = @intCast(i);
                f.produzir(val);
                soma.* += val;
            }
        }
    }.func, .{ &fila, &soma_produzida });

    // Consumidor
    const consumidor = try std.Thread.spawn(.{}, struct {
        fn func(f: *FilaSegura(i32, 10), soma: *std.atomic.Value(i64)) void {
            for (0..num_itens) |_| {
                const val = f.consumir();
                _ = soma.fetchAdd(val, .release);
            }
        }
    }.func, .{ &fila, &soma_consumida });

    produtor.join();
    consumidor.join();

    // Soma produzida deve ser igual à consumida
    try testing.expectEqual(soma_produzida, soma_consumida.load(.acquire));
}

Desafio 5: Serialização Binária

Problema: Implemente serialização/deserialização binária para structs.

const std = @import("std");

const Registro = struct {
    id: u32,
    tipo: u8,
    valor: f64,
    nome: [32]u8,
    nome_len: u8,

    pub fn comNome(id: u32, tipo: u8, valor: f64, nome: []const u8) Registro {
        var r = Registro{
            .id = id,
            .tipo = tipo,
            .valor = valor,
            .nome = undefined,
            .nome_len = @intCast(@min(nome.len, 32)),
        };
        @memcpy(r.nome[0..r.nome_len], nome[0..r.nome_len]);
        return r;
    }

    /// Serializa para bytes (big-endian para portabilidade).
    pub fn serializar(self: *const Registro, buf: []u8) !usize {
        if (buf.len < 46) return error.BufferTooSmall;
        var pos: usize = 0;

        // ID (4 bytes)
        std.mem.writeInt(u32, buf[pos..][0..4], self.id, .big);
        pos += 4;

        // Tipo (1 byte)
        buf[pos] = self.tipo;
        pos += 1;

        // Valor (8 bytes)
        const valor_bits: u64 = @bitCast(self.valor);
        std.mem.writeInt(u64, buf[pos..][0..8], valor_bits, .big);
        pos += 8;

        // Nome length + nome
        buf[pos] = self.nome_len;
        pos += 1;
        @memcpy(buf[pos .. pos + self.nome_len], self.nome[0..self.nome_len]);
        pos += self.nome_len;

        return pos;
    }

    /// Deserializa de bytes.
    pub fn deserializar(buf: []const u8) !Registro {
        if (buf.len < 14) return error.BufferTooSmall;
        var pos: usize = 0;

        const id = std.mem.readInt(u32, buf[pos..][0..4], .big);
        pos += 4;

        const tipo = buf[pos];
        pos += 1;

        const valor_bits = std.mem.readInt(u64, buf[pos..][0..8], .big);
        const valor: f64 = @bitCast(valor_bits);
        pos += 8;

        const nome_len = buf[pos];
        pos += 1;

        if (pos + nome_len > buf.len) return error.BufferTooSmall;

        var nome: [32]u8 = undefined;
        @memcpy(nome[0..nome_len], buf[pos .. pos + nome_len]);

        return .{
            .id = id,
            .tipo = tipo,
            .valor = valor,
            .nome = nome,
            .nome_len = nome_len,
        };
    }
};

test "serialização binária" {
    const testing = std.testing;

    const original = Registro.comNome(42, 3, 3.14159, "sensor_temp");

    var buf: [256]u8 = undefined;
    const tamanho = try original.serializar(&buf);

    const restaurado = try Registro.deserializar(buf[0..tamanho]);

    try testing.expectEqual(original.id, restaurado.id);
    try testing.expectEqual(original.tipo, restaurado.tipo);
    try testing.expectApproxEqAbs(original.valor, restaurado.valor, 0.0001);
    try testing.expectEqual(original.nome_len, restaurado.nome_len);
    try testing.expectEqualSlices(u8, original.nome[0..original.nome_len], restaurado.nome[0..restaurado.nome_len]);
}

Desafio 6: Parser de Argumentos CLI

Problema: Implemente um parser de argumentos de linha de comando.

const std = @import("std");
const Allocator = std.mem.Allocator;

const ArgParser = struct {
    const Self = @This();

    const Opcao = struct {
        nome_curto: ?u8,
        nome_longo: []const u8,
        descricao: []const u8,
        requer_valor: bool,
        valor: ?[]const u8,
        presente: bool,
    };

    opcoes: std.ArrayList(Opcao),
    posicionais: std.ArrayList([]const u8),
    allocator: Allocator,

    pub fn init(allocator: Allocator) Self {
        return .{
            .opcoes = std.ArrayList(Opcao).init(allocator),
            .posicionais = std.ArrayList([]const u8).init(allocator),
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Self) void {
        self.opcoes.deinit();
        self.posicionais.deinit();
    }

    pub fn adicionarOpcao(self: *Self, curto: ?u8, longo: []const u8, desc: []const u8, requer_valor: bool) !void {
        try self.opcoes.append(.{
            .nome_curto = curto,
            .nome_longo = longo,
            .descricao = desc,
            .requer_valor = requer_valor,
            .valor = null,
            .presente = false,
        });
    }

    pub fn parse(self: *Self, args: []const []const u8) !void {
        var i: usize = 0;
        while (i < args.len) : (i += 1) {
            const arg = args[i];

            if (std.mem.startsWith(u8, arg, "--")) {
                // Flag longa
                const nome = arg[2..];
                for (self.opcoes.items) |*opt| {
                    if (std.mem.eql(u8, opt.nome_longo, nome)) {
                        opt.presente = true;
                        if (opt.requer_valor and i + 1 < args.len) {
                            i += 1;
                            opt.valor = args[i];
                        }
                        break;
                    }
                }
            } else if (arg.len == 2 and arg[0] == '-') {
                // Flag curta
                for (self.opcoes.items) |*opt| {
                    if (opt.nome_curto == arg[1]) {
                        opt.presente = true;
                        if (opt.requer_valor and i + 1 < args.len) {
                            i += 1;
                            opt.valor = args[i];
                        }
                        break;
                    }
                }
            } else {
                try self.posicionais.append(arg);
            }
        }
    }

    pub fn temFlag(self: *const Self, nome_longo: []const u8) bool {
        for (self.opcoes.items) |opt| {
            if (std.mem.eql(u8, opt.nome_longo, nome_longo)) return opt.presente;
        }
        return false;
    }

    pub fn obterValor(self: *const Self, nome_longo: []const u8) ?[]const u8 {
        for (self.opcoes.items) |opt| {
            if (std.mem.eql(u8, opt.nome_longo, nome_longo)) return opt.valor;
        }
        return null;
    }
};

test "parser de argumentos CLI" {
    const testing = std.testing;
    const allocator = testing.allocator;

    var parser = ArgParser.init(allocator);
    defer parser.deinit();

    try parser.adicionarOpcao('v', "verbose", "Modo detalhado", false);
    try parser.adicionarOpcao('o', "output", "Arquivo de saída", true);
    try parser.adicionarOpcao('n', "count", "Número de repetições", true);

    const args = [_][]const u8{ "--verbose", "-o", "resultado.txt", "arquivo1.txt", "arquivo2.txt" };
    try parser.parse(&args);

    try testing.expect(parser.temFlag("verbose"));
    try testing.expectEqualStrings("resultado.txt", parser.obterValor("output").?);
    try testing.expect(!parser.temFlag("count"));
    try testing.expectEqual(@as(usize, 2), parser.posicionais.items.len);
}

Desafio 7: Rate Limiter (Token Bucket)

Problema: Implemente um rate limiter usando o algoritmo Token Bucket.

const std = @import("std");

const TokenBucket = struct {
    const Self = @This();

    tokens: f64,
    capacidade: f64,
    taxa_por_segundo: f64,
    ultimo_reabastecimento: i128,

    pub fn init(capacidade: f64, taxa_por_segundo: f64) Self {
        return .{
            .tokens = capacidade, // Começa cheio
            .capacidade = capacidade,
            .taxa_por_segundo = taxa_por_segundo,
            .ultimo_reabastecimento = std.time.nanoTimestamp(),
        };
    }

    pub fn permitir(self: *Self) bool {
        self.reabastecer();

        if (self.tokens >= 1.0) {
            self.tokens -= 1.0;
            return true;
        }
        return false;
    }

    pub fn permitirN(self: *Self, n: f64) bool {
        self.reabastecer();

        if (self.tokens >= n) {
            self.tokens -= n;
            return true;
        }
        return false;
    }

    fn reabastecer(self: *Self) void {
        const agora = std.time.nanoTimestamp();
        const decorrido_ns = agora - self.ultimo_reabastecimento;
        const decorrido_s: f64 = @as(f64, @floatFromInt(decorrido_ns)) / 1_000_000_000.0;

        const novos_tokens = decorrido_s * self.taxa_por_segundo;
        self.tokens = @min(self.capacidade, self.tokens + novos_tokens);
        self.ultimo_reabastecimento = agora;
    }
};

test "token bucket rate limiter" {
    const testing = std.testing;

    // 10 tokens, 5 por segundo
    var bucket = TokenBucket.init(10, 5);

    // Consome todos os tokens
    var permitidos: u32 = 0;
    for (0..15) |_| {
        if (bucket.permitir()) permitidos += 1;
    }
    try testing.expectEqual(@as(u32, 10), permitidos);

    // Espera tokens reabastecerrem
    std.time.sleep(500 * std.time.ns_per_ms); // 0.5s -> ~2.5 tokens

    permitidos = 0;
    for (0..5) |_| {
        if (bucket.permitir()) permitidos += 1;
    }
    try testing.expect(permitidos >= 2);
}

Dicas para Entrevistas de Sistemas

  1. Entenda alocadores — arena, pool, general purpose: quando usar cada um
  2. Pense em concorrência — atomics, mutexes, condições
  3. Serialização — endianness, alinhamento, portabilidade
  4. I/O bufferizado — nunca faça syscalls por byte
  5. Limites de recursos — rate limiting, pool de conexões, backpressure

Recursos Relacionados

Continue aprendendo Zig

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