2 pontos por GN⁺ 2025-08-25 | 1 comentários | Compartilhar no WhatsApp
  • Na versão 0.15 do Zig, foi introduzida a nova interface de IO (std.Io.Reader, std.Io.Writer)
  • O objetivo era melhorar a complexidade e os problemas de performance da forma anterior de IO, mas isso acabou gerando confusão no uso real
  • No uso de tls.Client e buffers, a forma inconsistente de passar parâmetros aumenta ainda mais a confusão
  • Mesmo ao implementar exemplos básicos de uso, há exigências complexas como definir vários tamanhos de buffer e campos de opções
  • Pela falta de documentação oficial, exemplos de código e funções utilitárias, não é algo intuitivo para iniciantes

A nova interface de IO introduzida no Zig 0.15 e seu contexto

  • Na versão 0.15 do Zig, foram introduzidos os novos tipos de IO std.Io.Reader e std.Io.Writer
  • A interface de IO anterior gerava complexidade por causa de problemas de performance, mistura de tipos e uso excessivo de anytype
  • Na nova estrutura de IO, os principais objetivos são separar claramente os tipos entre interfaces e melhorar o desempenho

Problemas práticos no uso de tls.Client e da interface de IO

  • Durante a atualização de uma biblioteca SMTP existente, surgiu confusão no uso da função tls.Client.init
  • Na documentação, a função init é descrita como recebendo ponteiros para Reader e Writer, além de um conjunto de opções como argumentos
  • Em Zig, net.Stream retorna Stream.Reader/Stream.Writer por meio dos métodos reader() e writer()
    • Porém, Stream.Reader/Stream.Writer e std.Io.Reader/std.Io.Writer não são exatamente o mesmo tipo, então é preciso converter
    • Para Reader, é necessário chamar o método interface(), enquanto para Writer é preciso usar o campo &interface, o que mostra falta de consistência

Problemas na configuração de buffers e campos de opções

  • stream.writer e stream.reader recebem, cada um, um buffer como argumento
    • O buffer é enfatizado como um elemento essencial da nova interface de IO
  • Ao chamar tls.Client.init, são obrigatórios quatro campos de opção: ca_bundle, host, write_buffer e read_buffer
    • A regra de separar o que vai nos parâmetros de opção e o que vai diretamente nos argumentos parece pouco 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,
  },
)
  • Na prática, se os ponteiros de buffer não forem passados corretamente, o programa pode não funcionar direito, travar ou até falhar de várias formas

Problemas de intuitividade ao usar Reader

  • Embora o campo reader de tls.Client seja em si um "stream descriptografado", std.Io.Reader não possui um método read comum
  • Em vez disso, ele oferece apenas métodos menos intuitivos como peek, takeByteSigned e readSliceShort
  • A API mais próxima do uso esperado parece ser a leitura de dados para um buffer por meio do 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));

Exemplo completo de código e problemas no uso real

  • Mesmo para montar um exemplo mínimo totalmente funcional, há muitos detalhes para cuidar, como opções, tamanho de buffers e conversões de tipo
  • A falta de testes, documentação e exemplos aumenta a dificuldade de aprendizado e a barreira de entrada
  • Para quem não entende bem a consistência interna da linguagem Zig ou o design subjacente, há muitos pontos que parecem estranhos
  • Mesmo dentro da biblioteca padrão, esse padrão ainda não é muito usado, então faltam materiais práticos de referência

Experiência e conclusão

  • Mudanças como a renomeação de std.fmt.printInt e alterações no design da API fazem com que o processo de migração em si não seja simples
  • Foram relatadas dificuldades repetidas com reader.interface(), a forma &writer.interface, o modo de passar opções e a necessidade de vários buffers
  • Para quem não está acostumado com protocolos de rede e segurança como TLS, entender exatamente o que é exigido fica ainda mais difícil
  • No geral, ainda existem muitas limitações em termos de clareza, documentação e conveniência em comparação com a abordagem anterior

1 comentários

 
GN⁺ 2025-08-25
Comentários no Hacker News
  • Declarando que é o autor. Finalmente consegui fazer funcionar direito. Tanto o writer criptografado quanto o stream writer precisavam do processo de flush, e ao mesmo tempo também havia um problema no lado da leitura. O streaming até funciona, mas como Writer.Fixed não implementa sendFile, na primeira leitura sempre retorna 0. Depois da primeira chamada, internamente muda do modo streaming para o modo leitura e, de repente, tudo começa a funcionar (link para o código relacionado: Zig File.zig #L1318). Agora estou tentando reativar a compressão na biblioteca de websocket

    • Isso me lembrou o meme do YouTube “não esqueça o flush” (vídeo no YouTube)

    • Fico me perguntando para onde foi o princípio da menor surpresa (principle of least surprise)

    • É impressionante ter ido da interface anterior para a situação atual. O nível de surpresa é enorme

  • Não sou PM de Zig, mas a primeira solução óbvia para o problema que o OP enfrentou é criar documentação melhor e mais exemplos de uso (não tem problema se forem muitos). Fazer esse trabalho também pode ser uma boa oportunidade para refletir se não estão exigindo demais do usuário. Se o objetivo era desempenho absoluto ou evitar a introdução de abstrações que causassem perda de performance, parece que isso foi alcançado, mas a DX (experiência do desenvolvedor) foi parar em outra galáxia

    • Parece que você não conhece bem a cultura da comunidade Zig. Se reclamar da falta de documentação, precisa estar pronto para uma chuva de comentários dizendo “leia você mesmo o código da stdlib”. A maioria das APIs é difícil de usar como neste post, e até tarefas básicas como HTTP ou sistema de arquivos ficam realmente duras se você não estiver familiarizado. Por isso, só os realmente muito bons sobrevivem

    • Escrever documentação tem custo e leva tempo. Esse tempo também poderia ser usado para melhorar outras partes do Zig. Se for código ainda em desenvolvimento, também é razoável adiar a documentação até ele estar mais consolidado. Claro que documentar é bom, mas quando é preciso priorizar entre novas funcionalidades, correções importantes de bugs ou trabalho de documentação, nem sempre dá para ter tudo

    • O Zig parece focado demais em dizer o que não se deve fazer. Seria bom evoluir para reunir, organizar bem e mostrar vários jeitos e exemplos de uso. A falta de documentação desta interface é um exemplo representativo disso

    • Dá muito trabalho escrever boa documentação ou bons exemplos. Pelo ritmo e pela amplitude das mudanças que estão acontecendo no Zig agora, mesmo que a documentação seja feita antes de tudo se assentar direito, ela logo deixa de servir

    • Não sou desenvolvedor de Zig, mas acho que uma das razões para a documentação do Zig ser tão minimalista é que a linguagem ainda é jovem e continua evoluindo. Entendo a dificuldade de investir tempo e energia sabendo que o que foi escrito agora pode logo estar errado no futuro

  • A linguagem Zig em si é realmente boa, mas a biblioteca padrão ainda está muito inacabada, muda o tempo todo, tem muitas lacunas e em parte é abstrata demais ou então de baixo nível demais. Neste momento, acho melhor usar diretamente a API do sistema operacional em vez da biblioteca padrão. A menos que você esteja disposto a ser beta tester, eu recomendaria evitar a stdlib

    • Na prática, quando uso Zig também costumo usar mais as APIs do sistema operacional. Os cImports funcionam bem, então também dá para usar com facilidade quando dá preguiça de criar definições em Zig

    • Para mim, o Zig tende a tentar fazer coisas demais ao mesmo tempo e, por isso, não consegue nem atingir o que considero uma linha mínima de qualidade. Ao forçar os usuários a aguentar mudanças bruscas e experimentação, parece depender de gente suficiente investindo na ilusão de que “antes do 1.0 tudo bem estar quebrado, um dia vai melhorar” (minha conclusão: esse dia provavelmente nunca vai chegar). Acho ruim transferir o peso dos próprios experimentos para os outros. Mesmo que avisem antes que é instável, mesmo que digam para não depender disso, para quem já levou um rug pull o problema continua sendo problema. Eu nem sei direito o que o Zig é. Matklad o chama de machine level language (entrevista relacionada: lobste.rs - entrevista com Matklad), enquanto a página oficial diz que é uma linguagem robust, optimal, reusable general-purpose. Essas duas descrições se contradizem. E há muitos problemas que nem exigem gerenciamento manual de memória, então o Zig de forma alguma é uma linguagem de propósito geral. No fim, toda essa confusão aparece na instabilidade do Zig e na sua biblioteca padrão inchada. Dizer que é simples e de propósito geral, mas ter uma biblioteca desse tamanho, é contraditório. O async também foi prometido como se fosse solução universal, mesmo sendo algo que não pode ser implementado de forma universal e eficiente em todas as plataformas. Antes divulgavam que isso resolvia o problema da coloração de funções, mas essa tentativa já foi abandonada. É estranho pedir que acreditemos de novo que agora vai dar certo. Na prática, para implementar um compilador em todas as plataformas, basta ter as instruções básicas de assembly necessárias; o LuaJIT implementou até o parser em assembly puro e funciona bem em todo lugar. Eu programo principalmente em Lua e quase nunca encontrei bugs no interpretador. Também não me vem à cabeça que problema o Zig resolveria melhor do que o LuaJIT. E se houver algo que só o Zig resolva, dá para embutir isso em código Lua e integrar via FFI. A maior parte do código simplesmente não precisa desse nível de otimização de baixo nível. Adotar Zig só traz mais dor de cabeça. Hoje em dia a expectativa inflada em torno do Zig está chegando ao mesmo descolamento da realidade que se vê com IA. Para acreditar no Zig, é preciso acreditar na esperança vazia de que ele um dia terá capacidades que hoje não existem. Sem um plano de execução real, fica só no “espera mais um pouco”

  • Não entendo bibliotecas ou interfaces exigirem que eu aloque um buffer do meu tipo. Se sou eu quem vai fazer o parsing, então nem preciso da biblioteca; e se vou usá-la, isso também pode quebrar a intercambialidade. A interface peculiar do Go existe porque algumas interfaces expandem a interface de writer (veja a interface hijacker), ou porque o objeto request é reaproveitado de várias formas por múltiplos middlewares. Em resumo: request não precisa ser extensível, mas response pode se transformar de várias formas, como websocket, wrappers TCP etc.

    • Não acho estranho que uma biblioteca exija alocação de buffer externa. Isso dá flexibilidade em troca de mais trabalho manual. Por exemplo, se você já tiver um pool de buffers pronto, talvez queira reutilizá-lo. Se o tipo fizer a alocação por conta própria internamente, isso fica impossível. Ou, em ambientes em que todos os recursos precisam ser alocados antecipadamente, alocar depois pode nem ser permitido. O lado ruim é que só 10% dos usuários realmente precisam dessa flexibilidade, enquanto 90% provavelmente só vão alocar um buffer e passar adiante, então todo mundo acaba tendo mais complexidade. O ideal seria oferecer alta flexibilidade e, ao mesmo tempo, deixar os casos simples fáceis. Por exemplo, poderia aceitar um buffer de tamanho zero (ou null em Zig) para o próprio tipo fazer a alocação, além de oferecer um construtor simples para criar sem buffer. Claro, esse tipo de coisa certamente dá trabalho de documentação

    • Isso é parecido com a diferença de convenções que cada linguagem escolhe (radianos/graus etc.). Qualquer IO pode ser convertido livremente. De um lado chamam de mock, em outra linguagem pode se chamar unsafeFoo. Andrew Kelley redescobriu por conta própria, em live stream, um padrão que a comunidade Haskell discutiu por 30 anos. Então o futuro é Zig. Ele percebeu primeiro

    • O significado de buffer externo é que a função pula a alocação do buffer

  • Não pretendo atualizar meu projeto paralelo em Zig para 0.15.x. Entendo por que o Andrew escolheu lançar e respeito colocar o novo IO nas mãos dos early adopters. Mas só se passaram alguns dias desde a grande mudança em readers/writers. Para quem trabalha na biblioteca padrão isso é bom, mas para quem usa Zig como hobby, como eu, parece sensato esperar até a estabilização no 0.16.0

    • Se o nome da linguagem é Zig, às vezes ela não deveria também dar um Zag?

    • Loris Cro, membro central do Zig, também mencionou em uma entrevista recente que está adiando atualizar seus próprios projetos em Zig até a poeira das mudanças de IO baixar. Mas a perspectiva depois disso é positiva. Tanto Andrew quanto Loris veem isso como a última grande mudança, então existe a expectativa de que o 1.0 talvez não esteja tão longe. O único grande fator variável no momento é o impacto da reintrodução de corrotinas sem pilha (stack-less coroutines)

  • Depois de ler o post sobre a nova interface de IO, escolhi evitar Zig. Acho que felizmente meu instinto estava certo. Mesmo que por motivos diferentes, no fim sinto uma complexidade parecida com a verbosidade do C++ antes do C++11. Esse padrão de uma linguagem nova tentar substituir outra e acabar ficando tão complexa quanto a antiga está se repetindo de forma familiar

    • Declaro que meu post é um deles. Não acho que seja preciso ter medo de experimentar Zig por causa de posts assim. A equipe do Zig está disposta a mudar com coragem quando vê uma solução melhor. Se você pensa em Zig como aposta para o futuro, essas mudanças talvez não combinem com isso, mas para indivíduos ou equipes pequenas ele já é uma linguagem ótima, com propósito claro e bom tooling
  • A observação do OP de que, para converter Stream.Reader em std.Io.Reader, é preciso chamar o método interface(), enquanto para obter std.io.Writer de Stream.Writer é necessário pegar o endereço do campo &interface, parece o tipo de inconsistência que a comunidade Go teria rejeitado de imediato. O Go costuma decidir até mudanças pequenas só depois de análises extremamente profundas. Meu caso favorito de issue do Go: Go github issue #45624. É o tipo de discussão que leva 4 anos até chegar a uma conclusão. Pode ser lento, mas eles verificam com muito cuidado a consistência, a reflexão de design e o uso em código real. É lento, mas acho que é a velocidade necessária. E as decisões que saem disso acabam tendo qualidade muito alta

    • Rust também é assim. Há muitos recursos úteis que existem só no nightly Rust e não no stable (por exemplo, generator). É frustrante, mas os recursos que entram no stable passam por validação muito profunda. Sou impaciente, mas acho desejável a abordagem da equipe do Rust

    • Antes do Go 1.0 não era tão lento assim. Havia mudanças grandes com frequência, mesmo que não fossem tão fundamentais (remoção de ponto e vírgula, mudança no tipo de erro etc.), e também havia ferramentas automáticas de migração. A partir do 1.0, com a promessa de estabilidade, virou esse modelo atual

  • Zig é a primeira linguagem que me vem à cabeça para trabalho de baixo nível. Também é muito legal que o Zig possa ser usado como compilador cruzado de C/C++

  • A maioria desses problemas parece simplesmente vir de documentação insuficiente ou ruim

    • Como muita coisa no Zig muda com frequência, parece que documentação não é prioridade. Os tutoriais de Zig também quase passam a sensação de “coleção de códigos de exemplo” (e muitos nem funcionam no compilador mais recente), e várias definições da biblioteca padrão exigem ler o código-fonte diretamente. Se você conhece todos os truques da sintaxe do Zig, as funções simples são curtas, lógicas e têm nomes claros, então para quem escreve isso é fácil. O conceito de allocators também não é conceitualmente difícil, embora eu não queira implementar um. Mas, em conceitos complexos, os limites ficam evidentes. O novo sistema de IO do Zig parece um empilhamento de várias camadas, como a estrutura de Streams/Readers/Writers do Java. Ele tenta permitir saídas simples como output.write("hello"), mas na prática gera confusão por falta de explicação suficiente sobre como usar. Também é questionável se um sistema de tipos tão complexo precisa mesmo ser expresso na biblioteca padrão. O Zig inteiro é feito de métodos claros, concisos e fáceis de ler, mas o novo sistema de IO está longe disso e não é intuitivo
  • (O novo sistema do Zig) mistura no mecanismo de runtime inteiro um conceito que antes servia apenas para dividir fronteiras de execução, sem mostrar claramente como conectar os dois lados, e esse é o problema