Volver al inicio

Zig vs C: utilidad CLI en 1000 líneas sin errores

El artículo desglosa la migración del analizador de logs CLI logz de C (3147 líneas) a Zig (1089 líneas). Comparación de código para parsing de argumentos/logs, defer, benchmarks. Ventajas para utilidades del sistema: velocidad, seguridad de memoria, compacidad.

Zig reemplaza 3000 líneas de C: caso real logz
Advertisement 728x90

Zig para utilidades CLI: Migrando de 3000 líneas de C a 1000 líneas sin segfaults

Los desarrolladores de herramientas CLI a menudo enfrentan los problemas de C: gestión manual de memoria, segfaults y código inflado. Reescribir el analizador de logs logz en Zig redujo su tamaño de 3147 líneas a 1089, aceleró la ejecución un 33% y eliminó los fallos. Un análisis de la migración, ejemplos de código y benchmarks.

Los problemas reales con C

El analizador de logs logz analiza archivos access.log y error.log de nginx/apache, filtra por nivel (DEBUG, INFO, WARN, ERROR), fecha e IP, construye listas de errores principales y renderiza gráficos de barras en la terminal usando unicode.

En C, el código se infló a 3147 líneas:

Google AdInline article slot
  • La función process_file — un monolito de 400 líneas.
  • Segfaults por desbordamientos de búfer (256 bytes vs líneas de 300 bytes).
  • Fugas de memoria, depuradas con Valgrind.
  • Un analizador de fechas con errores off-by-one.

El manejo de errores mediante -1/NULL/errno oscurecía el contexto. El Makefile a menudo se olvidaba, y las compilaciones eran lentas sin -MMD.

Analizar una línea simple como 2026-01-17 ERROR [nginx] 192.168.1.100 500 requería 50 líneas: strtok, comprobaciones NULL, strncpy, liberación de búferes.

Ventajas de Zig sobre C y Rust

Zig conserva el control de C pero añade herramientas potentes:

Google AdInline article slot
  • defer file.close() — limpieza garantizada sin flujo de control oculto.
  • !T en firmas — uniones de error, el compilador obliga a manejarlas.
  • Compilación cruzada: zig build -Dtarget=x86_64-windows sin toolchains.
  • Compilaciones rápidas para proyectos pequeños.

El borrow checker de Rust es excesivo para una herramienta CLI que lee un archivo y escribe en stdout.

La arquitectura de logz es un pipeline de 5 módulos:

  • Análisis de argumentos.
  • Lectura de archivos.
  • Análisis de líneas.
  • Filtrado.
  • Estadísticas y salida.

En C, todo estaba mezclado en process_file. En Zig, los datos fluyen estrictamente hacia abajo.

Google AdInline article slot

Comparación de código: Argumentos CLI

Versión C (~80 líneas):

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

Versión Zig (~40 líneas):

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 reemplaza 3 comprobaciones. El error está tipado.

Análisis de logs: De malloc a uniones de error

C (~65 líneas):

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 líneas):

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

    return LogEntry{...};
}

Sin malloc/free/strtok. La firma !LogEntry garantiza el manejo de errores.

Trabajando con archivos: defer en acción

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 cierra recursos en cualquier salida. En C, fclose era necesario antes de cada return.

Benchmarks y métricas

Prueba: 5 millones de líneas, 800 MB.

| Métrica | C | Zig |

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

| Tiempo (seg) | 4.2 | 2.8 |

| Binario (KB) | 47 | 32 |

| Memoria (MB) | 124 | 89 |

| Compilación (seg)| 3.1 | 1.4 |

| Líneas de código | 3147 | 1089 |

Aceleración debido a bufferedReader vs fgets. El código es 3 veces más compacto.

Errores comunes en la migración

  • Allocators: Usar page_allocator en todas partes → ArenaAllocator para CLI (deinit al final).
  • Slices: []const u8 sin \0, para API-C — .ptr + terminación nula.
  • Compilador: Mensajes de error largos, leer la primera línea.
  • Volatilidad: Zig 0.13, ejemplos de 2022–2023 pueden no compilar — consultar docs.ziglang.org.

Conclusiones clave

  • Zig reduce el código CLI 3x gracias a uniones de error y defer.
  • Sin segfaults: seguridad de memoria y sin asignaciones ocultas.
  • 30–40% más rápido que C con buffering adecuado.
  • Ideal para utilidades, sistemas embebidos, compilación cruzada.
  • Para proyectos grandes con ecosistema — Rust es preferible.

— Editorial Team

Advertisement 728x90

Leer después