2 puntos por GN⁺ 2025-08-25 | 1 comentarios | Compartir por WhatsApp
  • En la versión 0.15 de Zig se introdujo la nueva interfaz de IO (std.Io.Reader, std.Io.Writer)
  • El objetivo era mejorar la complejidad del enfoque anterior de IO y sus problemas de rendimiento, pero en la práctica ha generado confusión sobre cómo usarla
  • En el uso de tls.Client y los buffers, la forma inconsistente de pasar parámetros aumenta aún más la confusión
  • Incluso al implementar ejemplos básicos de uso, hay requisitos complejos como definir varios tamaños de buffer y campos de opciones
  • La falta de documentación oficial, ejemplos de código y funciones de conveniencia hace que no sea intuitiva para principiantes

La nueva interfaz de IO introducida en Zig 0.15 y su contexto

  • En la versión 0.15 de Zig se introdujeron los nuevos tipos de IO std.Io.Reader y std.Io.Writer
  • La interfaz anterior de IO generaba complejidad por problemas de rendimiento, mezcla de tipos y uso excesivo de anytype
  • Los objetivos principales de la nueva estructura de IO son una separación clara de tipos entre interfaces y una mejora del rendimiento

Problemas reales al usar tls.Client y la interfaz de IO

  • Durante la actualización de una librería SMTP existente surgió confusión con la forma de usar la función tls.Client.init
  • Según la documentación, la función init recibe como argumentos punteros a Reader y Writer, además de un conjunto de opciones
  • En Zig, net.Stream devuelve Stream.Reader y Stream.Writer mediante los métodos reader() y writer()
    • Sin embargo, Stream.Reader/Writer y std.Io.Reader/Writer no son exactamente el mismo tipo, por lo que hace falta una conversión
    • En Reader hay que llamar al método interface(), mientras que en Writer se debe usar el campo &interface, lo que muestra una falta de consistencia

Problemas con la configuración de buffers y campos de opciones

  • stream.writer y stream.reader reciben un buffer como argumento, respectivamente
    • Se resalta que el buffer es un elemento esencial en la nueva interfaz de IO
  • Al llamar a tls.Client.init, se requieren obligatoriamente cuatro campos de opciones: ca_bundle, host, write_buffer y read_buffer
    • La regla para separar qué valores se pasan dentro del parámetro de opciones y cuáles se pasan directamente como argumentos se siente poco clara
var tls_client = try std.crypto.tls.Client.init(
  reader.interface(),
  &writer.interface,
  .{
    .ca = .{.bundle = bundle},
    .host = .{ .explicit = "www.openmymind.net"; } ,
    .read_buffer = &read_buf2,
    .write_buffer = &write_buf2,
  },
)
  • En la práctica, si no se proporcionan correctamente los punteros a los buffers, el programa puede dejar de funcionar bien o presentar problemas como bloqueos o fallos

Problemas de intuición al usar Reader

  • Aunque el campo reader de tls.Client en sí representa un "flujo desencriptado", en realidad std.Io.Reader no tiene un método read convencional
  • En su lugar, solo ofrece métodos menos intuitivos como peek, takeByteSigned y readSliceShort
  • La API más cercana a un uso práctico termina siendo leer datos a un buffer mediante el método stream
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const n = try tls_client.reader.stream(&w, .limited(buf.len));

Ejemplo de código completo y problemas en la práctica

  • Incluso al intentar crear un ejemplo mínimo completamente funcional, hay muchos detalles a considerar, como opciones, tamaños de buffer y conversiones de tipos
  • La falta de pruebas, documentación y ejemplos eleva la dificultad de aprendizaje y la barrera de entrada
  • Si no se comprende bien la consistencia interna del lenguaje Zig o su diseño subyacente, hay muchos puntos que pueden sentirse extraños
  • Incluso dentro de la librería estándar este enfoque no se usa demasiado, así que faltan referencias prácticas para casos reales

Experiencia y conclusión

  • Cambios de nombres como std.fmt.printInt y variaciones en el diseño de la API hacen que el proceso de migración en sí no sea sencillo
  • Se repiten varias dificultades, como el uso de reader.interface(), &writer.interface, la forma de pasar opciones y la necesidad de múltiples buffers
  • Desde la perspectiva de alguien que no está familiarizado con protocolos de red y seguridad como TLS, entender los requisitos se vuelve aún más difícil
  • En conjunto, todavía existen varias carencias en términos de claridad, documentación y mejora de la comodidad de uso frente al enfoque anterior

1 comentarios

 
GN⁺ 2025-08-25
Opinión en Hacker News
  • Declara ser el autor. Por fin logró hacerlo funcionar correctamente. Tanto el writer de cifrado como el stream writer requerían un proceso de flush, y al mismo tiempo también había un problema del lado de lectura. El streaming sí funciona, pero como Writer.Fixed no implementa sendFile, en la primera lectura siempre devuelve 0. Después de la primera llamada, internamente cambia del modo streaming al modo lectura y de repente todo empieza a funcionar (enlace al código relacionado: Zig File.zig #L1318). Ahora está intentando volver a activar la compresión en la librería de websocket

    • Esto me recuerda al meme de YouTube de "No olviden hacer flush" (video de YouTube)

    • Me pregunto a dónde se fue el principio de menor sorpresa

    • Se siente impresionante haber pasado de la interfaz anterior a la situación actual. La sorpresa sí que es grande

  • No soy PM de Zig, pero la primera solución obvia al problema que experimentó el OP es una mejor documentación y más ejemplos de uso (aunque sean muchísimos). También podría ser una buena oportunidad para, al hacer ese trabajo, reflexionar si no le están exigiendo demasiado al usuario. Si la meta era rendimiento absoluto o evitar introducir abstracciones que degraden el rendimiento, parece que sí se logró, pero la DX (experiencia de desarrollo) se siente como si se hubiera ido a otra galaxia

    • Parece que no conoces bien la cultura de la comunidad de Zig. Si te quejas de la falta de documentación, cualquiera debe prepararse para una lluvia de comentarios diciendo "lee tú mismo el código de la stdlib". La mayoría de las API son difíciles de usar, como en esta publicación, y si no estás familiarizado ni siquiera con tareas básicas como HTTP o el sistema de archivos, de verdad se vuelve muy duro. Por eso solo sobrevive la gente realmente capaz

    • Escribir documentación cuesta y toma tiempo. Ese tiempo también podría usarse para mejorar otras partes de Zig. Si es código en desarrollo, posponer la documentación hasta que esté completamente asentado también es una decisión razonable. Claro, documentar es bueno, pero cuando hay que priorizar entre funciones nuevas, correcciones críticas de bugs o trabajo de documentación, no siempre se puede tener todo

    • Parece que Zig está demasiado enfocado en indicar qué no debe hacerse. Ojalá evolucionara más hacia recopilar, organizar y enseñar distintas formas de hacer las cosas y ejemplos de uso. La ausencia de documentación para esta interfaz es un ejemplo representativo

    • Hace falta mucho esfuerzo para escribir buena documentación o buenos ejemplos. Viendo la magnitud de los cambios que están ocurriendo en Zig, incluso si haces documentación antes de que todo se asiente bien, pronto deja de servir

    • No soy desarrollador de Zig, pero creo que una de las razones por las que su documentación es tan breve es que el lenguaje aún es joven y sigue evolucionando. Entiendo que sea difícil invertir tiempo y energía sabiendo que lo que escribas ahora pronto podría quedar incorrecto en el futuro

  • El lenguaje Zig en sí mismo está muy bien, pero la biblioteca estándar todavía está bastante incompleta, sigue cambiando, le faltan muchas cosas, y algunas partes son excesivamente abstractas mientras que otras son demasiado de bajo nivel. Ahora mismo me parece mejor usar directamente las API del sistema operativo en vez de la biblioteca estándar. Si no estás dispuesto a actuar como beta tester, recomendaría evitar la biblioteca estándar

    • De hecho, yo también cuando uso Zig suelo apoyarme principalmente en las API del sistema operativo. Los cImports funcionan bien, así que incluso cuando da flojera crear definiciones en Zig, se puede usar fácilmente

    • Desde mi punto de vista, Zig intenta hacer demasiadas cosas al mismo tiempo, y por eso ni siquiera alcanza el umbral mínimo de calidad que yo esperaría. Obliga a los usuarios a aceptar cambios bruscos y experimentación, en una situación donde suficiente gente ya invirtió en la ilusión de que "antes de la 1.0 está bien que esté roto, algún día mejorará" (conclusión: me parece que ese día nunca llegará). No me parece deseable trasladarle a otros el peso de tus propios experimentos. Aunque se avise de antemano que es inestable, aunque se diga que no hay que depender de ello, sigue siendo un problema para quien termina sufriendo un rug pull repentino. No tengo claro qué se supone que sea Zig. Matklad lo llama un machine level language (entrevista relacionada: lobste.rs - entrevista a Matklad), mientras que la página oficial lo describe como un lenguaje robust, optimal, reusable y de propósito general. Ambas cosas se contradicen. Y hay muchos problemas que no requieren gestión manual de memoria, así que Zig nunca será un lenguaje de propósito general. Al final, todo este caos se refleja en la inestabilidad de Zig y en su biblioteca estándar sobredimensionada. Afirmar simplicidad y propósito general mientras se tiene una librería tan grande es contradictorio. Async también se prometió como si fuera una solución universal, a pesar de ser una función que no puede implementarse de manera universal y eficiente en todas las plataformas. Antes se promocionaba diciendo que resolvía el problema del function coloring, pero ese intento ya fue abandonado. La idea de que ahora debemos volver a creer que esta vez sí lo lograrán suena extraña. En realidad, para implementar un compilador en todas las plataformas bastan las instrucciones básicas de ensamblador, y luajit incluso implementó su parser completamente en ensamblador y funciona bien en todas partes. Yo programo sobre todo en Lua y casi nunca me he topado con bugs en el intérprete. Tampoco se me ocurre qué problema resolvería Zig mejor que luajit. Incluso si hubiera algo que solo pudiera resolverse con Zig, bastaría con integrarlo dentro de código Lua y enlazarlo vía FFI. La mayor parte del código no necesita realmente ese nivel de optimización de bajo nivel. Adoptar Zig solo trae más dolores de cabeza. Últimamente las expectativas exageradas alrededor de Zig ya alcanzan una desconexión con la realidad al nivel de la IA. Para creer en Zig hay que creer en la esperanza vacía de que algún día tendrá capacidades que hoy no existen. Sin un plan de ejecución real, todo suena a "espera un poco más"

  • No entiendo que una librería o interfaz me exija asignar buffers de mi tipo. Si de todos modos yo voy a parsearlo, realmente no necesito la librería; y si la uso, esto incluso puede romper el intercambio. La interfaz peculiar de Go se debe a que algunas interfaces extienden la interfaz de writer (véase la interfaz hijacker), o a que el objeto request se reutiliza de distintas formas entre varios middleware. En resumen, la petición (request) no necesita extenderse, pero la respuesta (response) sí puede transformarse en muchas formas distintas, como websocket, wrappers de TCP, etc.

    • No me parece raro que una librería exija asignación de buffers externos. Te da flexibilidad a cambio de más trabajo manual. Por ejemplo, si ya tienes un pool de buffers creado, quizá quieras reutilizarlo. Si el tipo asigna internamente por su cuenta, eso no sería posible. O en entornos donde todos los recursos deben asignarse por adelantado, más tarde ya no se puede asignar. La desventaja es que solo el 10% de los usuarios necesita realmente esa flexibilidad, y el 90% simplemente va a asignar un buffer y pasarlo, pero todos terminan haciendo algo más complejo. El mejor enfoque sería ofrecer mucha flexibilidad y, al mismo tiempo, hacer que los casos simples sean fáciles. Por ejemplo, si pasas un buffer de longitud 0 (o null en Zig), el tipo podría asignarlo por su cuenta; además, también podría ofrecerse un constructor simple para crear el tipo sin buffer. Claro, es evidente que eso complica bastante la documentación

    • Esto se parece a la diferencia de convenciones que elige cada lenguaje (como radianes/grados). Cualquier IO puede transformarse libremente. En un lado lo llaman mock, en otro lenguaje podría llamarse unsafeFoo. Andrew Kelley redescubrió por su cuenta, en un live stream, patrones que la comunidad de Haskell lleva 30 años discutiendo. Por eso el futuro es Zig. Él fue quien llegó primero a la iluminación

    • El significado de un buffer externo es que la función omite asignar el buffer

  • No pienso actualizar mi proyecto secundario en Zig a 0.15.x. Respeto por qué Andrew eligió hacer el release y poner el nuevo IO en manos de los early adopters. Pero apenas han pasado unos días desde el gran cambio en readers/writers. Puede ser algo bueno para quienes trabajan en la biblioteca estándar, pero para alguien como yo, que usa Zig como hobby, parece más sensato esperar hasta después de que se estabilice en 0.16.0

    • Si el nombre del lenguaje es Zig, entonces a veces también debería hacer Zag, me da risa pensarlo

    • Loris Cro, miembro central de Zig, también mencionó en una entrevista reciente que está posponiendo la actualización de Zig en sus propios proyectos hasta que se asienten las réplicas del cambio de IO. Pero el panorama posterior es positivo. Tanto Andrew como Loris creen que este será el último gran cambio, así que existe la expectativa de que la 1.0 no esté tan lejos. El único gran factor variable por ahora es el impacto de las stack-less coroutines que se volverían a introducir

  • Después de leer la publicación sobre la nueva interfaz de IO, elegí mantenerme alejado de Zig. Siento que por suerte mi instinto estaba en lo correcto. Aunque por razones distintas, al final transmite una complejidad parecida a la verbosidad previa a C++11. Se repite ese patrón ya conocido donde un lenguaje nuevo intenta reemplazar a los existentes y termina volviéndose igual de complejo

    • Aclaro que mi publicación también es una de esas. No creo que haya que asustarse de intentar usar Zig por leer cosas así. El equipo de Zig está dispuesto a cambiar con decisión si ve una solución mejor. Si piensas en Zig como una apuesta para el futuro, estos cambios quizá no te convengan, pero para individuos o equipos pequeños ya es un lenguaje genial, con propósito claro y buenas herramientas
  • Creo que la observación del OP sobre lo inconsistente que resulta tener que llamar al método interface() para convertir Stream.Reader a std.Io.Reader, mientras que para obtener std.io.Writer desde Stream.Writer se necesita la dirección del campo &interface, habría sido rechazada de plano por la comunidad de Go. Go suele tomar decisiones incluso para cambios pequeños solo después de un análisis extremadamente profundo. Mi caso favorito de issue de Go: Go github issue #45624. Discuten durante 4 años y luego llegan a una conclusión. Puede ser lento, pero revisan con cuidado la consistencia, el diseño y el uso en código real. Es lento, pero creo que esa es exactamente la velocidad necesaria. Y las decisiones que salen de ahí terminan siendo de muy alta calidad

    • Rust también es así. Hay muchas funciones útiles que existen solo en nightly rust y no en stable (por ejemplo, generator). Es desesperante, pero las funciones que entran a stable pasan por una validación profundísima. Yo soy impaciente, pero creo que el enfoque del equipo de Rust es el correcto

    • Antes de Go 1.0 no era tan lento. Hubo cambios grandes frecuentes, aunque no necesariamente más fundamentales (como quitar los punto y coma, cambiar el tipo de error, etc.), y también daban soporte con herramientas de conversión automática. Fue a partir de 1.0 cuando prometieron estabilidad y adoptaron la forma actual

  • Zig es lo primero que se me viene a la mente como lenguaje para trabajo de bajo nivel. También es muy genial que Zig pueda usarse como compilador cruzado para C/C++

  • La mayoría de los problemas parece venir simplemente de documentación insuficiente o deficiente

    • Como demasiadas partes de Zig cambian con mucha frecuencia, da la impresión de que documentar no es la prioridad. Incluso los tutoriales de Zig se sienten casi como una "colección de código de ejemplo" (y muchos ejemplos ni siquiera corren con el compilador más reciente), y para muchas definiciones de la biblioteca estándar hay que ir a leer directamente el código fuente. Si ya conoces todos los trucos de sintaxis de Zig, las funciones simples son cortas, lógicas y tienen nombres claros, así que para el autor es fácil. El concepto de allocators tampoco es tan difícil a nivel conceptual, aunque no me gustaría implementarlos yo mismo. Pero con conceptos complejos sus límites se vuelven evidentes. El nuevo sistema de IO de Zig está envuelto en múltiples capas, como la estructura de Streams/Readers/Writers de Java. Quiere permitir que para una salida sencilla simplemente escribas algo como output.write("hello"), pero en la práctica genera confusión porque falta explicación de cómo usarlo. También me pregunto si de verdad hace falta expresar este sistema de tipos tan complejo en la biblioteca estándar. Todo Zig está compuesto de métodos claros, concisos y fáciles de leer, pero el nuevo sistema de IO está lejos de eso y no resulta intuitivo
  • (El nuevo sistema de Zig) es problemático porque tomó un concepto que solo servía para dividir fronteras de ejecución y lo mezcló dentro del motor completo de runtime, sin mostrar claramente cómo se conectan ambos lados