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
- Entenda alocadores — arena, pool, general purpose: quando usar cada um
- Pense em concorrência — atomics, mutexes, condições
- Serialização — endianness, alinhamento, portabilidade
- I/O bufferizado — nunca faça syscalls por byte
- Limites de recursos — rate limiting, pool de conexões, backpressure
Recursos Relacionados
- std.mem — Alocadores e operações de memória
- std.thread — Threads e sincronização
- std.process — Gerenciamento de processos
- std.time — Medição de performance
- Desafio de Código #1: Strings
- Desafio de Código #2: Estruturas de Dados
- Perguntas de Entrevista: Memória
- Perguntas de Entrevista: Concorrência