Retour à l'accueil

Zig contre C : utilitaire CLI en 1000 lignes sans erreurs

L'article détaille la migration de l'analyseur de logs CLI logz de C (3147 lignes) vers Zig (1089 lignes). Comparaison de code pour parsing des arguments/logs, defer, benchmarks. Avantages pour les utilitaires système : vitesse, sécurité mémoire, compacité.

Zig remplace 3000 lignes de C : cas réel logz
Advertisement 728x90

Zig pour les utilitaires CLI : Migration de 3000 lignes de C à 1000 lignes sans segfaults

Les développeurs d'outils CLI font souvent face aux pièges du C : gestion manuelle de la mémoire, segfaults et code gonflé. Réécrire l'analyseur de logs logz en Zig a réduit sa taille de 3147 à 1089 lignes, accéléré l'exécution de 33 % et éliminé les plantages. Un détail de la migration, des exemples de code et des benchmarks.

Les problèmes réels du C

L'analyseur de logs logz analyse les fichiers access.log et error.log d'nginx/apache, filtre par niveau (DEBUG, INFO, WARN, ERROR), date et IP, construit des listes d'erreurs principales et affiche des graphiques en barres dans le terminal en utilisant des caractères unicode.

En C, le code a gonflé à 3147 lignes :

Google AdInline article slot
  • La fonction process_file — un monolithe de 400 lignes.
  • Segfaults dus à des débordements de tampon (256 octets vs lignes de 300 octets).
  • Fuites de mémoire, déboguées avec Valgrind.
  • Un analyseur de dates avec des erreurs de décalage.

La gestion des erreurs via -1/NULL/errno masquait le contexte. Le Makefile était souvent oublié, et les compilations étaient lentes sans -MMD.

Analyser une simple ligne comme 2026-01-17 ERROR [nginx] 192.168.1.100 500 nécessitait 50 lignes : strtok, vérifications NULL, strncpy, libération des tampons.

Les avantages de Zig par rapport à C et Rust

Zig conserve le contrôle du C mais ajoute des outils puissants :

Google AdInline article slot
  • defer file.close() — nettoyage garanti sans flux de contrôle caché.
  • !T dans les signatures — unions d'erreur, le compilateur impose la gestion.
  • Cross-compilation : zig build -Dtarget=x86_64-windows sans chaînes d'outils.
  • Compilations rapides pour les petits projets.

Le vérificateur d'emprunt de Rust est excessif pour un outil CLI qui lit un fichier et écrit sur stdout.

L'architecture de logz est un pipeline de 5 modules :

  • Analyse des arguments.
  • Lecture des fichiers.
  • Analyse des lignes.
  • Filtrage.
  • Statistiques et sortie.

En C, tout était mélangé dans process_file. En Zig, les données circulent strictement vers le bas.

Google AdInline article slot

Comparaison de code : Arguments CLI

Version C (~80 lignes) :

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

Version Zig (~40 lignes) :

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

orelse return error.MissingValue remplace 3 vérifications. L'erreur est typée.

Analyse des logs : De malloc aux unions d'erreur

C (~65 lignes) :

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

    // analyse timestamp, level, 40 lignes...
    return entry; // la libération est la responsabilité de l'appelant
}

Zig (~35 lignes) :

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

    return LogEntry{...};
}

Pas de malloc/free/strtok. La signature !LogEntry garantit la gestion des erreurs.

Travailler avec les fichiers : defer en action

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 ferme les ressources à toute sortie. En C, fclose était requis avant chaque return.

Benchmarks et métriques

Test : 5 millions de lignes, 800 Mo.

| Métrique | C | Zig |

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

| Temps (sec) | 4,2 | 2,8 |

| Binaire (Ko) | 47 | 32 |

| Mémoire (Mo) | 124 | 89 |

| Compilation (sec) | 3,1 | 1,4 |

| Lignes de code | 3147 | 1089 |

Accélération due à bufferedReader vs fgets. Le code est 3 fois plus compact.

Pièges courants de migration

  • Allocateurs : Utiliser page_allocator partout → ArenaAllocator pour CLI (deinit à la fin).
  • Slices : []const u8 sans \0, pour API C — .ptr + terminaison nulle.
  • Compilateur : Messages d'erreur longs, lire la première ligne.
  • Volatilité : Zig 0.13, les exemples de 2022–2023 peuvent ne pas compiler — vérifier docs.ziglang.org.

Points clés à retenir

  • Zig réduit le code CLI par 3 grâce aux unions d'erreur et defer.
  • Pas de segfaults : sécurité mémoire et pas d'allocations cachées.
  • 30–40 % plus rapide que C avec un tamponnage approprié.
  • Idéal pour les utilitaires, systèmes embarqués, cross-compilation.
  • Pour les grands projets avec un écosystème — Rust est préférable.

— Editorial Team

Advertisement 728x90

Lire ensuite