Zig pro CLI nástroje: migrace z 3000 řádků C na 1000 řádků bez pádů
Vývojáři nástrojů pro příkazový řádek často narážejí na problémy jazyka C: ruční správa paměti, pády programu a objemný kód. Přepsání analyzátoru logů logz do Zigu zredukovalo objem z 3147 řádků na 1089, zrychlilo běh o 33 % a odstranilo pády. Rozbor migrace, ukázky kódu a benchmarky.
Problémy C v reálném projektu
Analyzátor logů logz parsuje access.log a error.log nginx/apache, filtruje podle úrovně (DEBUG, INFO, WARN, ERROR), data, IP, sestavuje žebříček chyb a sloupcové grafy v terminálu pomocí unicode.
V C kód narostl na 3147 řádků:
- Funkce
process_file— 400 řádků monolitického kódu. - Pády z přetečení bufferů (256 bajtů proti 300bajtovým řetězcům).
- Úniky paměti, ladění přes Valgrind.
- Parser data s chybami typu off-by-one.
Zpracování chyb přes -1/NULL/errno rozmazává kontext. Makefile se zapomínal, sestavování zpomalovalo bez -MMD.
Jednoduchý parser řetězce 2026-01-17 ERROR [nginx] 192.168.1.100 500 vyžadoval 50 řádků: strtok, NULL-kontroly, strncpy, uvolňování bufferů.
Výhody Zigu oproti C a Rustu
Zig zachovává kontrolu C, ale přidává nástroje:
defer file.close()— zaručené uvolnění bez skrytého toku řízení.!Tv signatuře — sjednocení chyb, kompilátor vyžaduje zpracování.- Křížová kompilace:
zig build -Dtarget=x86_64-windowsbez toolchainů. - Rychlé sestavení malých projektů.
Rust s borrow checkerem je nadbytečný pro CLI, které čte soubor do stdout.
Architektura logz — pipeline z 5 modulů:
- Parsování argumentů.
- Čtení souboru.
- Parsování řádků.
- Filtrování.
- Statistika a výstup.
V C se vše míchalo v process_file. V Zig data proudí striktně dolů.
Srovnání kódu: argumenty CLI
C-verze (~80 řádků):
int parse_args(int argc, char **argv, Config *cfg) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--file") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "error: --file requires a value\n");
return -1;
}
cfg->filename = argv[++i];
} // podobně pro --level, --top-ip
}
return 0;
}
Zig (~40 řádků):
const Config = struct {
filename: []const u8 = "",
level: ?LogLevel = null,
top_ip: u32 = 10,
from_date: ?i64 = null,
};
fn parseArgs(allocator: std.mem.Allocator) !Config {
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
var cfg = Config{};
_ = args.next();
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--file")) {
cfg.filename = args.next() orelse return error.MissingValue;
} else if (std.mem.eql(u8, arg, "--level")) {
const lvl = args.next() orelse return error.MissingValue;
cfg.level = try LogLevel.parse(lvl);
} // podobně
}
return cfg;
}
orelse return error.MissingValue nahrazuje 3 kontroly. Chyba je typizovaná.
Parsování logů: od malloc k error unions
C (~65 řádků):
LogEntry *parse_line(char *line) {
LogEntry *entry = malloc(sizeof(LogEntry));
if (!entry) return NULL;
char *saveptr;
char *token = strtok_r(line, " ", &saveptr);
if (!token) { free(entry); return NULL; }
// parsování timestamp, level, 40 řádků...
return entry; // free na volajícím
}
Zig (~35 řádků):
const LogEntry = struct {
timestamp: i64,
level: LogLevel,
source: []const u8,
ip: []const u8,
status: u16,
};
fn parseLine(line: []const u8) !LogEntry {
var iter = std.mem.splitScalar(u8, line, ' ');
const ts_str = iter.next() orelse return error.InvalidFormat;
const timestamp = try parseTimestamp(ts_str);
const level_str = iter.next() orelse return error.InvalidFormat;
const level = try LogLevel.parse(level_str);
const source = iter.next() orelse return error.InvalidFormat;
const ip = iter.next() orelse return error.InvalidFormat;
_ = iter.next(); // HTTP-metoda
const status_str = iter.next() orelse return error.InvalidFormat;
const status = try std.fmt.parseInt(u16, status_str, 10);
return LogEntry{...};
}
Žádné malloc/free/strtok. Signatura !LogEntry zaručuje zpracování chyb.
Práce se soubory: defer v akci
pub fn processFile(
allocator: std.mem.Allocator,
path: []const u8,
cfg: Config,
) !Stats {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var buffered = std.io.bufferedReader(file.reader());
var reader = buffered.reader();
var stats = Stats.init(allocator);
defer stats.deinit();
var line_buf: [8192]u8 = undefined;
while (try reader.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
const entry = parseLine(line) catch continue;
if (cfg.matches(entry)) {
try stats.add(entry);
}
}
return stats;
}
defer zavírá zdroje při jakémkoli ukončení. V C bylo potřeba fclose před každým return.
Benchmarky a metriky
Test: 5 milionů řádků, 800 MB.
| Metrika | C | Zig |
|------------------|--------|--------|
| Čas (sekundy) | 4,2 | 2,8 |
| Binárka (KB) | 47 | 32 |
| Paměť (MB) | 124 | 89 |
| Sestavení (sek.) | 3,1 | 1,4 |
| Řádků kódu | 3147 | 1089 |
Zrychlení díky bufferedReader vs fgets. Kód je 3krát kompaktnější.
Typické chyby při migraci
- Allocators: page_allocator všude → ArenaAllocator pro CLI (deinit na konci).
- Slices:
[]const u8bez \0, pro C-API —.ptr+ null-termination. - Kompilátor: dlouhé chyby, čti první řádek.
- Proměnlivost: Zig 0.13, příklady 2022–2023 nekompilují — docs.ziglang.org.
Co je důležité
- Zig redukuje CLI kód 3krát díky error unions a defer.
- Žádné pády: typy paměti a absence skrytých alokací.
- Rychlejší než C o 30–40 % při správné bufferizaci.
- Ideální pro utility, embedded, křížovou kompilaci.
- Pro velké projekty s ekosystémem — Rust je vhodnější.
— Editorial Team
Zatím žádné komentáře.