Powrót do strony głównej

Zig vs C: narzędzie CLI na 1000 linii bez błędów

Artykuł omawia migrację CLI-analizatora logów logz z C (3147 linii) na Zig (1089 linii). Porównanie kodu parsowania argumentów/ logów, defer, benchmarki. Zalety dla narzędzi systemowych: prędkość, bezpieczeństwo pamięci, kompaktowość.

Zig zastępuje 3000 linii C: rzeczywisty przypadek logz
Advertisement 728x90

Zig dla narzędzi CLI: migracja z 3000 linii C na 1000 linii bez błędów segmentacji

Programiści narzędzi CLI często borykają się z problemami C: ręczne zarządzanie pamięcią, błędy segmentacji i rozwlekły kod. Przepisanie analizatora logów logz na Zig zmniejszyło objętość z 3147 linii do 1089, przyspieszyło wykonanie o 33% i wyeliminowało awarie. Analiza migracji, przykłady kodu i benchmarki.

Problemy C w rzeczywistym projekcie

Analizator logów logz parsuje access.log i error.log nginx/apache, filtruje według poziomu (DEBUG, INFO, WARN, ERROR), daty, IP, buduje ranking błędów i wykresy słupkowe w terminalu przez unicode.

W C kod rozrósł się do 3147 linii:

Google AdInline article slot
  • Funkcja process_file — 400 linii monolitu.
  • Błędy segmentacji od przepełnienia buforów (256 bajtów wobec 300-bajtowych linii).
  • Wycieki pamięci, debugowanie przez Valgrind.
  • Parser daty z błędami off-by-one.

Obsługa błędów przez -1/NULL/errno zaciera kontekst. Makefile był zapominany, kompilacja zwalniała bez -MMD.

Prosty parser linii 2026-01-17 ERROR [nginx] 192.168.1.100 500 wymagał 50 linii: strtok, sprawdzanie NULL, strncpy, zwalnianie buforów.

Zalety Zig nad C i Rust

Zig zachowuje kontrolę C, ale dodaje narzędzia:

Google AdInline article slot
  • defer file.close() — gwarantowane sprzątanie bez ukrytego przepływu sterowania.
  • !T w sygnaturze — unia błędów, kompilator wymaga obsługi.
  • Kompilacja krzyżowa: zig build -Dtarget=x86_64-windows bez toolchainów.
  • Szybka kompilacja małych projektów.

Rust z borrow checker jest nadmiarowy dla CLI, które czyta plik do stdout.

Architektura logz — pipeline z 5 modułów:

  • Parsowanie argumentów.
  • Czytanie pliku.
  • Parsowanie linii.
  • Filtrowanie.
  • Statystyki i wyjście.

W C wszystko mieszało się w process_file. W Zig dane płyną ściśle w dół.

Google AdInline article slot

Porównanie kodu: argumenty CLI

Wersja C (~80 linii):

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

Zig (~40 linii):

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

orelse return error.MissingValue zastępuje 3 sprawdzenia. Błąd jest typowany.

Parsowanie logów: od malloc do unii błędów

C (~65 linii):

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

    // parsowanie timestamp, level, 40 linii...
    return entry; // free na wywołującym
}

Zig (~35 linii):

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

    return LogEntry{...};
}

Brak malloc/free/strtok. Sygnatura !LogEntry gwarantuje obsługę błędów.

Praca z plikami: defer w akcji

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 zamyka zasoby przy każdym wyjściu. W C wymagane było fclose przed każdym return.

Benchmarki i metryki

Test: 5 mln linii, 800 MB.

| Metryka | C | Zig |

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

| Czas (sek) | 4.2 | 2.8 |

| Binarka (KB) | 47 | 32 |

| Pamięć (MB) | 124 | 89 |

| Kompilacja (sek) | 3.1 | 1.4 |

| Linii kodu | 3147 | 1089 |

Przyspieszenie dzięki bufferedReader vs fgets. Kod 3 razy bardziej zwarty.

Typowe błędy migracji

  • Allocators: page_allocator wszędzie → ArenaAllocator dla CLI (deinit na końcu).
  • Slices: []const u8 bez \0, dla C-API — .ptr + null-termination.
  • Kompilator: długie błędy, czytaj pierwszą linię.
  • Zmienność: Zig 0.13, przykłady 2022–2023 nie kompilują się — docs.ziglang.org.

Co jest ważne

  • Zig redukuje kod CLI 3-krotnie dzięki unii błędów i defer.
  • Brak błędów segmentacji: typy pamięci i brak ukrytych alokacji.
  • Szybszy niż C o 30–40% przy prawidłowej buforyzacji.
  • Idealny dla narzędzi, embedded, kompilacji krzyżowej.
  • Dla dużych projektów z ekosystemem — Rust lepszy.

— Editorial Team

Advertisement 728x90

Czytaj dalej