Zurück zur Startseite

Zig vs C: CLI-Utility in 1000 Zeilen ohne Fehler

Artikel zerlegt Migration des CLI-Log-Analyzers logz von C (3147 Zeilen) zu Zig (1089 Zeilen). Code-Vergleich für Parsing von Argumenten/Logs, defer, Benchmarks. Vorteile für Systemtools: Geschwindigkeit, Speichersicherheit, Kompaktheit.

Zig ersetzt 3000 Zeilen C: echter Fall logz
Advertisement 728x90

Zig für CLI-Utilities: Migration von 3000 Zeilen C auf 1000 Zeilen ohne Segmentierungsfehler

CLI-Tool-Entwickler kämpfen oft mit den Tücken von C: manuelles Speichermanagement, Segmentierungsfehler und aufgeblähter Code. Die Neuimplementierung des Logz-Log-Analyzers in Zig reduzierte die Größe von 3147 auf 1089 Zeilen, beschleunigte die Ausführung um 33 % und beseitigte Abstürze. Eine detaillierte Analyse der Migration, Codebeispiele und Benchmarks.

Die praktischen Probleme mit C

Der Logz-Log-Analyzer parst nginx/apache access.log- und error.log-Dateien, filtert nach Level (DEBUG, INFO, WARN, ERROR), Datum und IP, erstellt Top-Fehlerlisten und rendert Balkendiagramme im Terminal mit Unicode.

In C war der Code auf 3147 Zeilen angewachsen:

Google AdInline article slot
  • Die Funktion process_file – ein 400-Zeilen-Monolith.
  • Segmentierungsfehler durch Pufferüberläufe (256 Bytes vs. 300-Byte-Zeilen).
  • Speicherlecks, debuggt mit Valgrind.
  • Ein Datumsparser mit Off-by-One-Fehlern.

Fehlerbehandlung via -1/NULL/errno verschleierte den Kontext. Das Makefile wurde oft vergessen, und Builds waren ohne -MMD langsam.

Das Parsen einer einfachen Zeile wie 2026-01-17 ERROR [nginx] 192.168.1.100 500 erforderte 50 Zeilen: strtok, NULL-Prüfungen, strncpy, Freigabe von Puffern.

Zigs Vorteile gegenüber C und Rust

Zig behält die Kontrolle von C bei, fügt aber leistungsstarke Werkzeuge hinzu:

Google AdInline article slot
  • defer file.close() – garantierte Bereinigung ohne versteckten Kontrollfluss.
  • !T in Signaturen – Fehler-Unionen, der Compiler erzwingt die Behandlung.
  • Cross-Kompilierung: zig build -Dtarget=x86_64-windows ohne Toolchains.
  • Schnelle Builds für kleine Projekte.

Rusts Borrow-Checker ist übertrieben für ein CLI-Tool, das eine Datei liest und auf stdout ausgibt.

Die Logz-Architektur ist eine Pipeline aus 5 Modulen:

  • Argumentparsing.
  • Dateilesen.
  • Zeilenparsing.
  • Filtern.
  • Statistik und Ausgabe.

In C war alles in process_file vermischt. In Zig fließen Daten strikt abwärts.

Google AdInline article slot

Code-Vergleich: CLI-Argumente

C-Version (~80 Zeilen):

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];
        } // similarly for --level, --top-ip
    }
    return 0;
}

Zig-Version (~40 Zeilen):

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);
        } // similarly
    }
    return cfg;
}

orelse return error.MissingValue ersetzt 3 Prüfungen. Der Fehler ist typisiert.

Logs parsen: Von malloc zu Fehler-Unionen

C (~65 Zeilen):

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; }

    // parsing timestamp, level, 40 lines...
    return entry; // free is the caller's responsibility
}

Zig (~35 Zeilen):

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-Methode
    const status_str = iter.next() orelse return error.InvalidFormat;
    const status = try std.fmt.parseInt(u16, status_str, 10);

    return LogEntry{...};
}

Kein malloc/free/strtok. Die Signatur !LogEntry garantiert Fehlerbehandlung.

Arbeiten mit Dateien: defer in Aktion

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 schließt Ressourcen bei jedem Exit. In C war fclose vor jedem return nötig.

Benchmarks und Metriken

Test: 5 Millionen Zeilen, 800 MB.

| Metrik | C | Zig |

|------------------|--------|--------|

| Zeit (Sek.) | 4,2 | 2,8 |

| Binärdatei (KB) | 47 | 32 |

| Speicher (MB) | 124 | 89 |

| Build (Sek.) | 3,1 | 1,4 |

| Codezeilen | 3147 | 1089 |

Beschleunigung durch bufferedReader vs fgets. Code ist 3-mal kompakter.

Häufige Migrationsfallen

  • Allokatoren: page_allocator überall verwenden → ArenaAllocator für CLI (deinit am Ende).
  • Slices: []const u8 ohne \0, für C-API – .ptr + Null-Terminierung.
  • Compiler: Lange Fehlermeldungen, erste Zeile lesen.
  • Volatilität: Zig 0.13, Beispiele von 2022–2023 kompilieren möglicherweise nicht – docs.ziglang.org prüfen.

Wichtige Erkenntnisse

  • Zig reduziert CLI-Code um das 3-fache dank Fehler-Unionen und defer.
  • Keine Segmentierungsfehler: Speichersicherheit und keine versteckten Allokationen.
  • 30–40 % schneller als C mit korrektem Buffering.
  • Ideal für Utilities, Embedded Systems, Cross-Kompilierung.
  • Für große Projekte mit Ökosystem – Rust ist vorzuziehen.

— Editorial Team

Advertisement 728x90

Weiterlesen