- 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
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 comoWriter.Fixedno implementasendFile, 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 websocketEsto 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
cImportsfuncionan bien, así que incluso cuando da flojera crear definiciones en Zig, se puede usar fácilmenteDesde 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 pullrepentino. 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, yluajitincluso 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 queluajit. 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
nullen 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ónEsto 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ónEl 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
Creo que la observación del OP sobre lo inconsistente que resulta tener que llamar al método
interface()para convertirStream.Readerastd.Io.Reader, mientras que para obtenerstd.io.WriterdesdeStream.Writerse 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 calidadRust 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
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