- 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
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 comoWriter.Fixednão implementasendFile, 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 websocketIsso 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
cImportsfuncionam bem, então também dá para usar com facilidade quando dá preguiça de criar definições em ZigPara 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
nullem 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çãoIsso é 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 primeiroO 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
A observação do OP de que, para converter
Stream.Readeremstd.Io.Reader, é preciso chamar o métodointerface(), enquanto para obterstd.io.WriterdeStream.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 altaRust 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
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