<!-- Canonical: https://lucioduran.com/es/blog/zig-language-comptime-system-programming
Language: es
Content-SHA256: 67146ac37935ca8f2207648cb77bd01ed44d67751dc926d57c4ab9b7fb12f3a9
-->

Systems Programming

# El Comptime de Zig: Un Enfoque Unificado para la Programación de Sistemas en Compile-Time

2026-02-0411 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/zig-language-comptime-system-programming>)

## Qué Es Realmente Comptime

La mayoría de los lenguajes tienen alguna forma de evaluación en compile time. C tiene macros ` #define ` y compilación condicional ` #ifdef `. C++ tiene ` constexpr ` y templates. Rust tiene ` const fn `, macros y generics. Cada uno de estos es un mecanismo separado con su propia sintaxis, reglas y limitaciones.

[Zig](<https://ziglang.org/>) tiene un solo mecanismo: ` comptime `. Cualquier expresión puede evaluarse en compile time, y las mismas construcciones del lenguaje funcionan tanto en compile time como en runtime. No hay lenguaje de macros separado, no hay sintaxis de templates, no hay anotaciones de generics. Se escribe código Zig normal y se marcan partes como ` comptime `.

```zig
// Esta es una función regular que puede correr en compile time O runtime
fn fibonacci(n: u64) u64 {
 if (n <= 1) return n;
 return fibonacci(n - 1) + fibonacci(n - 2);
}

// Evaluación en compile time
const fib_20 = comptime fibonacci(20); // Computado en compile time: 6765

// Evaluación en runtime
fn getfib(n: u64) u64 {
 return fibonacci(n); // Computado en runtime
}
```

Esto se ve simple, tal vez hasta trivial. Pero las implicaciones se propagan en cascada. Un ejemplo real ilustra el punto.

## Reemplazando Generics con Comptime

En C, crear una estructura de datos genérica requiere void pointers y macros:

```c
// C: Array dinámico genérico via macros
#define DEFINE_VEC(T) \
 typedef struct { \
 T* items; \
 size_t len; \
 size_t cap; \
 } Vec_##T; \
 \
 void vec_##T##_push(Vec_##T* v, T item) { \
 if (v->len >= v->cap) { \
 v->cap = v->cap ? v->cap * 2 : 8; \
 v->items = realloc(v->items, sizeof(T) * v->cap); \
 } \
 v->items[v->len++] = item; \
 }

DEFINE_VEC(int)
DEFINE_VEC(float)
// Genera Vec_int, Vec_float, etc.
```

Esto es ilegible, imposible de debuggear, y produce mensajes de error espantosos. En Zig, el mismo concepto usa tipos comptime:

```zig
fn ArrayList(comptime T: type) type {
 return struct {
 items: []T,
 len: usize,
 capacity: usize,
 allocator: std.mem.Allocator,

 const Self = @This();

 pub fn init(allocator: std.mem.Allocator) Self {
 return Self{
 .items = &[_]T{},
 .len = 0,
 .capacity = 0,
 .allocator = allocator,
 };
 }

 pub fn append(self: *Self, item: T) !void {
 if (self.len >= self.capacity) {
 const new_cap = if (self.capacity == 0) 8 else self.capacity * 2;
 self.items = try self.allocator.realloc(self.items, new_cap);
 self.capacity = new_cap;
 }
 self.items[self.len] = item;
 self.len += 1;
 }

 pub fn deinit(self: *Self) void {
 self.allocator.free(self.items);
 }
 };
}

// Uso, el compilador genera un struct especializado para cada tipo
var list = ArrayList(u32).init(allocator);
defer list.deinit();
try list.append(42);
```

El parámetro ` comptime T: type ` significa que ` T ` debe ser conocido en compile time. La función devuelve un ` type `, literalmente genera una definición de struct. Esto no es una instanciación de template; es código Zig real ejecutándose que produce un tipo. El cuerpo puede contener sentencias ` if `, loops, o cualquier otra construcción:

```zig
fn Matrix(comptime T: type, comptime rows: usize, comptime cols: usize) type {
 // Validación en compile time
 if (@typeInfo(T) != .Float and @typeInfo(T) != .Int) {
 @compileError("Matrix requiere un tipo numérico, recibió " ++ @typeName(T));
 }

 if (rows == 0 or cols == 0) {
 @compileError("Las dimensiones de la matrix deben ser non-zero");
 }

 return struct {
 data: [rows][cols]T,

 const Self = @This();

 // Generar multiplicación optimizada basada en dimensiones
 pub fn multiply(self: Self, other: Matrix(T, cols, comptime other_cols: usize)) Matrix(T, rows, other_cols) {
 var result: Matrix(T, rows, other_cols) = undefined;
 inline for (0..rows) |i| {
 inline for (0..other_cols) |j| {
 var sum: T = 0;
 inline for (0..cols) |k| {
 sum += self.data[i][k] * other.data[k][j];
 }
 result.data[i][j] = sum;
 }
 }
 return result;
 }
 };
}
```

El ` inline for ` desenrolla los loops en compile time. Para una multiplicación de matrices 4x4, el compilador genera 64 instrucciones multiply-add sin overhead de loop. Esto es el tipo de optimización que requeriría intrinsics o assembly escrito a mano en C.

## El Parser de Paquetes: Un Ejemplo Real

Una demostración convincente del poder de comptime surge al construir un parser de paquetes DNS. Una implementación C típica involucra aritmética de punteros, bounds checking manual, y lógica condicional para distintos tipos de registros, código notoriamente frágil y difícil de mantener.

La versión Zig usa comptime para generar parsers para distintos tipos de registros DNS:

```zig
const DnsRecordType = enum(u16) {
 A = 1,
 AAAA = 28,
 CNAME = 5,
 MX = 15,
 TXT = 16,
 SRV = 33,
};

fn RecordData(comptime rtype: DnsRecordType) type {
 return switch (rtype) {
 .A => struct { addr: [4]u8 },
 .AAAA => struct { addr: [16]u8 },
 .CNAME => struct { name: []const u8 },
 .MX => struct { priority: u16, exchange: []const u8 },
 .TXT => struct { texts: []const []const u8 },
 .SRV => struct {
 priority: u16,
 weight: u16,
 port: u16,
 target: []const u8,
 },
 };
}

fn parseRecord(comptime rtype: DnsRecordType, data: []const u8) !RecordData(rtype) {
 var result: RecordData(rtype) = undefined;

 switch (rtype) {
 .A => {
 if (data.len < 4) return error.TruncatedPacket;
 result.addr = data[0..4].*;
 },
 .AAAA => {
 if (data.len < 16) return error.TruncatedPacket;
 result.addr = data[0..16].*;
 },
 .MX => {
 if (data.len < 2) return error.TruncatedPacket;
 result.priority = std.mem.readInt(u16, data[0..2], .big);
 result.exchange = try decompressName(data[2..]);
 },
 // ... otros tipos
 }

 return result;
}
```

En compile time, si se llama ` parseRecord(.A, some_data) `, el compilador genera una función que solo maneja registros A, las ramas del switch para AAAA, MX, etc. se eliminan completamente. Cero overhead en runtime para el dispatch de tipos.

Pero el poder real es la validación en compile time:

```zig
comptime {
 // Verificar en compile time que todos los tipos de registro tienen parsers
 inline for (std.meta.fields(DnsRecordType)) |field| {
 const rtype: DnsRecordType = @enumFromInt(field.value);
 // Esto fuerza al compilador a instanciar el parser
 // para cada tipo de registro, atrapando errores en compile time
 _ = RecordData(rtype);
 }
}
```

Si se agrega un nuevo tipo de registro al enum pero falta el case correspondiente en el switch de ` RecordData `, el código no compila. En C, eso sería un crash en runtime con el primer paquete del nuevo tipo de registro, probablemente a las 3 AM.

## Manejo de Errores: La Feature Subestimada

Las discusiones sobre Zig tienden a enfocarse en comptime, pero el manejo de errores es igual de importante. Zig usa error unions, un valor es o el tipo esperado o un error:

```zig
const ParseError = error{
 TruncatedPacket,
 InvalidHeader,
 LabelTooLong,
 CompressionLoop,
 BufferOverflow,
};

fn parseDnsMessage(buf: []const u8) ParseError!DnsMessage {
 if (buf.len < 12) return error.TruncatedPacket;

 const header = parseHeader(buf[0..12]);

 var offset: usize = 12;
 var questions: [MAX_QUESTIONS]Question = undefined;

 for (0..header.qdcount) |i| {
 // La keyword `try` propaga errores automáticamente
 questions[i] = try parseQuestion(buf, &offset);
 }

 return DnsMessage{
 .header = header,
 .questions = questions[0..header.qdcount],
 };
}
```

El ` ! ` en el tipo de retorno significa "esta función puede fallar." La keyword ` try ` es equivalente a "si esto devuelve un error, devolvelo de la función actual." Sin excepciones, sin códigos de error que se puedan olvidar de chequear, sin estado global ` errno `.

Comparación con el equivalente en C:

```c
// C: Cada caller tiene que acordarse de chequear el valor de retorno
// Nada impide ignorar errores
int parse_dns_message(const uint8_t* buf, size_t len, dns_message_t* out) {
 if (len < 12) return DNS_ERR_TRUNCATED;

 int err = parse_header(buf, &out->header);
 if (err) return err; // Fácil olvidarse de esta línea

 size_t offset = 12;
 for (int i = 0; i < out->header.qdcount; i++) {
 err = parse_question(buf, len, &offset, &out->questions[i]);
 if (err) return err; // Y de esta también
 }

 return 0;
}
```

En la práctica, los codebases C frecuentemente contienen retornos de error sin chequear, una categoría de bug que Zig elimina estructuralmente. Si una función devuelve un error union, debe manejarse.

## Interop con C: La Killer Feature Práctica

Zig puede importar headers C directamente. No a través de FFI bindings, no a través de un paso de generación, directamente:

```zig
const c = @cImport({
 @cInclude("pcap/pcap.h");
 @cInclude("netinet/ip.h");
 @cInclude("netinet/udp.h");
});

fn capturePackets(device: [*:0]const u8) !void {
 var errbuf: [c.PCAP_ERRBUF_SIZE]u8 = undefined;

 const handle = c.pcap_open_live(
 device,
 65535, // snaplen
 1, // promiscuo
 1000, // timeout ms
 &errbuf,
 ) orelse return error.PcapOpenFailed;

 defer c.pcap_close(handle);

 // Setear filtro BPF para tráfico DNS
 var filter: c.struct_bpf_program = undefined;
 if (c.pcap_compile(handle, &filter, "udp port 53", 1, 0) == -1) {
 return error.FilterCompileFailed;
 }
 defer c.pcap_freecode(&filter);

 if (c.pcap_setfilter(handle, &filter) == -1) {
 return error.FilterSetFailed;
 }

 // Loop de captura
 while (true) {
 var header: *c.struct_pcap_pkthdr = undefined;
 var data: [*]const u8 = undefined;

 const result = c.pcap_next_ex(handle, &header, &data);
 if (result < 0) break;
 if (result == 0) continue; // timeout

 const packet = data[0..header.caplen];
 processPacket(packet) catch |err| {
 std.log.warn("Error parseando paquete: {}", .{err});
 };
 }
}
```

Esto llama funciones de libpcap directamente. Zig parsea los headers C en compile time (usando su parser C built-in, no libclang), resuelve los tipos y genera las llamadas ABI correctas.

Un benchmark de una implementación de parser DNS revela la siguiente comparación:

| Métrica | C (gcc -O2) | Zig (ReleaseFast) |
| --- | --- | --- |
| Tamaño binario | 47 KB | 52 KB |
| Parsear 1M paquetes | 1.82s | 1.79s |
| Memoria (RSS) | 2.1 MB | 2.3 MB |
| Líneas de código | 1,847 | 1,203 |

Performance prácticamente idéntica, pero 35% menos código. El tamaño extra del binario viene del panic handler de Zig y soporte de stack trace (que se puede eliminar con ` --strip `).

## Donde Zig se Queda Corto

Una evaluación honesta requiere reconocer las limitaciones actuales:

**El ecosistema es inmaduro.** El manejo de paquetes es built-in (` build.zig.zon `), pero el ecosistema de paquetes es escaso. ¿Se necesita un parser JSON? Hay tres, todos con estilos de API distintos y niveles de madurez diferentes. ¿Un cliente HTTP? La librería estándar de Zig tiene uno, pero todavía está verde. En C, hay décadas de librerías battle-tested.

**Tiempos de compilación.** El uso intensivo de comptime puede hacer lenta la compilación. Un parser DNS con validación comptime agresiva tarda 4.2 segundos en compilar. El código C equivalente compila en 0.3 segundos. El compilador de Zig es single-threaded (el compilador self-hosted está mejorando esto, pero no llegó todavía).

**Documentación.** La documentación oficial es una referencia del lenguaje, no un tutorial. Desarrolladores que vienen de C pueden pasar tiempo significativo entendiendo cómo funcionan los allocators, cómo manejar slices correctamente (no son punteros), y cómo funciona ` async ` (se está removiendo en 0.14, lo que indica el estado de estabilidad del lenguaje).

**Churn pre-1.0.** Upgradeando de 0.12 a 0.13 puede requerir cambiar 40+ líneas en un codebase. Firmas de funciones cambian, APIs de la librería estándar se mueven, y algunas features comptime se retrabajan. Es esperable para un lenguaje pre-1.0, pero es un costo real.

## El Caso para un Optimismo Cauteloso

A pesar de estas limitaciones, Zig presenta una opción convincente para nuevos proyectos de sistemas, como agregadores de logs custom que necesitan parsear millones de líneas de log estructuradas por segundo. Varios factores soportan esta evaluación:

1. **Comptime elimina clases enteras de bugs de runtime.** Validaciones que serían assertions en C (o panics en Rust) se convierten en errores de compilación en Zig.
2. **La interop con C permite adopción incremental.** Se pueden llamar librerías C existentes desde el día uno y gradualmente reemplazarlas con implementaciones Zig.
3. **El modelo de manejo de errores es mejor que el de C.** No tan comprehensivo como el ` Result ` de Rust, pero más ergonómico. La keyword ` try ` sola elimina docenas de bugs de "olvidarse de chequear el código de retorno".
4. **Sin allocaciones ocultas, sin flujo de control oculto.** Cada alocación es explícita (se pasa un allocator), y no hay sobrecarga de operadores, conversiones implícitas, ni llamadas a funciones ocultas. Leer código Zig revela exactamente qué va a hacer la CPU.

Zig no va a reemplazar a Rust para sistemas memory-safe. No va a reemplazar a C en codebases legacy. Pero para código de sistemas nuevo donde se necesita performance nivel C, interop con C, y un lenguaje que respeta la inteligencia del desarrollador sin ahogarlo en teoría de tipos, es una de las opciones más sólidas disponibles. El comptime solo ya vale la curva de aprendizaje.

## Preguntas frecuentes

### ¿Zig está listo para producción?

Depende de la definición de producción. Zig se usa en producción en Uber (para su toolchain Hermetic CC++), TigerBeetle (base de datos financiera), y Bun (runtime JavaScript). El lenguaje es pre-1.0 y todavía hay breaking changes entre releases, pero el compilador y la librería estándar son notablemente estables.

### ¿Cómo difiere el comptime de Zig del constexpr de C++?

C++ constexpr restringe qué operaciones se pueden hacer en compile time. El comptime de Zig no tiene tales restricciones, permite alocar memoria, hacer I/O (sobre datos de compile time), usar loops, condicionales y llamar cualquier función. El lenguaje completo está disponible en compile time, no un subset restringido.

### ¿Debería aprender Zig o Rust para programación de sistemas?

Resuelven problemas distintos. Rust ofrece garantías de memory safety en compile time a través de su sistema de ownership, al costo de una curva de aprendizaje empinada y anotaciones de lifetime complejas. Zig ofrece manejo manual de memoria con mejor ergonomía que C, manejo de errores explícito, y metaprogramación comptime. Para memory safety demostrable, Rust es la opción más sólida. Para interop con C, simplicidad y poder de compile time, Zig vale la pena evaluar.

## Tips

1. **Tip:** Usar ` @compileLog() ` durante el desarrollo permite inspeccionar valores comptime, imprime en compile time e intencionalmente causa un error de compilación para que no quede en código de producción. Funciona como ` printf ` debugging para el compilador.
2. **Tip:** Al llamar C desde Zig, conviene usar ` @cImport ` con ` @cInclude ` en vez de declaraciones extern manuales. El parser de headers C de Zig maneja la mayoría de los headers del sistema, incluyendo ramas ` #ifdef ` específicas de plataforma.
3. **Tip:** La interfaz ` std.mem.Allocator ` de Zig permite intercambiar allocators en los call sites. Usar ` std.testing.allocator ` en tests habilita la detección automática de leaks y double-frees.
4. **Tip:** Si el uso intensivo de comptime causa tiempos de compilación lentos, separar la lógica comptime en archivos aparte y usar ` @import ` ayuda. Zig cachea imports, así que el código comptime compartido corre una sola vez.

- zig
- comptime
- programación-de-sistemas
- interop-c
- seguridad-de-memoria
- compile-time
- bajo-nivel
- infraestructura

[Volver al Blog](<https://lucioduran.com/es/blog>)
