- Mit Zig 0.15 wurden neue IO-Schnittstellen (
std.Io.Reader, std.Io.Writer) eingeführt
- Ziel war es, die Komplexität und Performance-Probleme des bisherigen IO-Ansatzes zu verbessern, doch in der tatsächlichen Nutzung entsteht Verwirrung
- Im Zusammenhang mit der Verwendung von
tls.Client und Buffern sorgt die uneinheitliche Übergabe von Parametern für zusätzliche Verwirrung
- Schon bei der Umsetzung einfacher Anwendungsbeispiele gibt es komplexe Anforderungen wie die Angabe verschiedener Puffergrößen und Optionsfelder
- Wegen fehlender offizieller Dokumentation, Codebeispiele und Komfortfunktionen ist das System für Einsteiger nicht intuitiv
Die neue IO-Schnittstelle in Zig 0.15 und ihr Hintergrund
- In Zig 0.15 wurden die neuen IO-Typen
std.Io.Reader und std.Io.Writer eingeführt
- Die frühere IO-Schnittstelle führte durch Performance-Probleme, Typvermischung und den übermäßigen Einsatz von
anytype zu Komplexität
- Hauptziele der neuen IO-Struktur sind eine klare Typtrennung zwischen den Schnittstellen und bessere Performance
Praktische Probleme bei der Nutzung von tls.Client und der IO-Schnittstelle
- Bei der Aktualisierung einer bestehenden SMTP-Bibliothek entstand Verwirrung bei der Verwendung von
tls.Client.init
- Laut Dokumentation erwartet die
init-Funktion Zeiger auf Reader und Writer sowie ein Options-Set als Argumente
- Zigs
net.Stream liefert über die Methoden reader() und writer() jeweils Stream.Reader bzw. Stream.Writer zurück
Stream.Reader/Stream.Writer und std.Io.Reader/std.Io.Writer sind jedoch nicht exakt derselbe Typ, daher ist eine Umwandlung nötig
- Beim Reader muss die Methode
interface() aufgerufen werden, beim Writer das Feld &interface verwendet werden, was inkonsistent wirkt
Probleme bei der Konfiguration von Buffern und Optionsfeldern
stream.writer und stream.reader erwarten jeweils einen Buffer als Argument
- Buffer werden in der neuen IO-Schnittstelle als wesentlicher Bestandteil hervorgehoben
- Beim Aufruf von
tls.Client.init sind vier Optionsfelder zwingend erforderlich: ca_bundle, host, write_buffer und read_buffer
- Die Regeln dafür, welche Werte über die Optionsparameter und welche direkt als Argumente übergeben werden, wirken unklar
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,
},
)
- Wenn Buffer-Zeiger in der Praxis nicht korrekt übergeben werden, funktioniert das Programm nicht richtig oder es kommt zu Hängern, Abstürzen und anderen Problemen
Mangelnde Intuitivität bei der Nutzung des Readers
- Obwohl das Feld
reader von tls.Client selbst ein „entschlüsselter Stream“ ist, besitzt std.Io.Reader in der Praxis keine gewöhnliche read-Methode
- Stattdessen werden nur weniger intuitive Methoden wie
peek, takeByteSigned oder readSliceShort angeboten
- Die der praktischen Nutzung am nächsten kommende API ist der Weg, Daten über die Methode
stream in einen Buffer einzulesen
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
const n = try tls_client.reader.stream(&w, .limited(buf.len));
Gesamtes Codebeispiel und Probleme in der Praxis
- Selbst für ein vollständiges, lauffähiges Minimalbeispiel muss man viele Dinge beachten: Optionen, Buffergrößen, Typumwandlungen und mehr
- Durch fehlende Tests, Dokumentation und Beispiele steigen Lernaufwand und Einstiegshürde
- Ohne ein gutes Verständnis der Konsistenz innerhalb der Zig-Sprache oder des zugrunde liegenden Designs gibt es viele Stellen, die seltsam wirken
- Auch innerhalb der Standardbibliothek wird dieser Ansatz noch nicht häufig genutzt, sodass es an praxistauglichen Referenzen fehlt
Erfahrungen und Fazit
- Durch Umbenennungen wie
std.fmt.printInt und Änderungen am API-Design ist schon der Migrationsprozess selbst nicht einfach
- Die wiederkehrenden Schwierigkeiten mit
reader.interface(), &writer.interface, der Übergabe von Optionen und der Notwendigkeit mehrerer Buffer treten immer wieder auf
- Wenn man mit Netzwerk- und Sicherheitsprotokollen wie TLS nicht vertraut ist, wirken die Anforderungen noch schwerer nachvollziehbar
- Insgesamt gibt es gegenüber früher weiterhin deutliche Defizite bei Klarheit, Dokumentation und Bedienkomfort
1 Kommentare
Hacker-News-Kommentare
Ich bin der Autor. Ich habe es endlich dazu gebracht, korrekt zu funktionieren. Sowohl der verschlüsselte Writer als auch der Stream-Writer mussten im
flush-Schritt verarbeitet werden, und gleichzeitig gab es auch auf der Leseseite ein Problem. Streaming funktioniert zwar, aber weilWriter.FixedsendFilenicht implementiert, gibt der erste Leseversuch immer0zurück. Nach dem ersten Aufruf wird intern plötzlich vom Streaming-Modus in den Lesemodus umgeschaltet, und dann beginnt auf einmal alles zu funktionieren (zugehöriger Code-Link: Zig File.zig #L1318). Jetzt versuche ich, die Komprimierungsfunktion in der WebSocket-Bibliothek wieder zu aktivierenDas erinnert mich an das YouTube-Meme „Vergesst nicht zu flushen“ (YouTube-Video)
Ich frage mich wirklich, wo das Prinzip der geringsten Überraschung geblieben ist
Es ist schon bemerkenswert, vom vorherigen Interface in die jetzige Situation geraten zu sein. Die Überraschung ist jedenfalls groß
Ich bin zwar nicht Zig PM, aber die offensichtlichste erste Lösung für das Problem, das der OP erlebt hat, wäre bessere Dokumentation und mehr Anwendungsbeispiele zu erstellen (es dürfen ruhig sehr viele sein). Solche Arbeit kann auch eine gute Gelegenheit sein, darüber nachzudenken, ob man den Nutzern nicht zu viel abverlangt. Falls das angestrebte Ziel absolute Performance war oder die Vermeidung von Abstraktionen, die Performanceeinbußen verursachen, dann scheint dieses Ziel erreicht zu sein, aber die DX (Developer Experience) wirkt, als wäre sie in eine andere Galaxie verschwunden
Du scheinst die Kultur der Zig-Community nicht gut zu kennen. Wenn man sich über mangelnde Dokumentation beschwert, muss man jederzeit damit rechnen, dass die Kommentare mit „Lies doch direkt den stdlib-Code“ überflutet werden. Die meisten APIs sind wie in diesem Beitrag schwer zu benutzen, und selbst grundlegende Aufgaben wie HTTP oder Dateisystem sind wirklich hart, wenn man nicht daran gewöhnt ist. Am Ende überleben nur die wirklich Fähigen
Dokumentation kostet Aufwand und Zeit. In derselben Zeit könnte man auch andere Teile von Zig verbessern. Wenn es sich um Code in Arbeit handelt, ist es eine vernünftige Entscheidung, mit der Dokumentation zu warten, bis sich alles gesetzt hat. Natürlich ist Dokumentation gut, aber wenn man zwischen neuen Features, wichtigen Bugfixes oder Dokumentationsarbeit priorisieren muss, kann man eben nicht immer alles zugleich haben
Zig scheint sich zu stark darauf zu konzentrieren, vorzuschreiben, was man nicht tun soll. Ich würde mir wünschen, dass es sich eher dahin entwickelt, verschiedene Vorgehensweisen und Nutzungsbeispiele zu sammeln, gut zu ordnen und zu vermitteln. Die fehlende Dokumentation dieses Interface ist dafür ein typisches Beispiel
Gute Dokumentation oder Beispiele zu schreiben erfordert viel Mühe. Wenn man sieht, wie groß die Veränderungen gerade in Zig sind, verlieren Dokumente oft schnell ihren Nutzen, noch bevor sich etwas wirklich etabliert hat
Ich bin kein Zig-Entwickler, aber ich denke, einer der Gründe, warum die Dokumentation von Zig so knapp ist, liegt darin, dass die Sprache noch jung ist und sich weiterentwickelt. Ich kann nachvollziehen, dass es schwer ist, Zeit und Energie in Dokumentation zu stecken, wenn man weiß, dass sie in naher Zukunft wahrscheinlich schon wieder falsch sein wird
Die Zig-Sprache selbst ist wirklich ordentlich, aber die Standardbibliothek ist noch sehr unfertig, verändert sich ständig, hat viele Lücken, und Teile davon sind entweder übermäßig abstrakt oder im Gegenteil zu Low-Level. Ich denke, derzeit ist es besser, direkt die OS-APIs zu verwenden statt der Standardbibliothek. Wenn man nicht bereit ist, Beta-Tester zu sein, würde ich empfehlen, die Standardbibliothek zu meiden
Tatsächlich verwende ich bei Zig meistens ebenfalls direkt die OS-APIs. Dank
cImportskann man sie leicht nutzen, auch wenn man zu faul ist, eigene Zig-Definitionen dafür zu schreibenAus meiner Sicht versucht Zig, zu viele Dinge gleichzeitig zu tun, und erreicht deshalb nicht einmal die minimale Qualitätsgrenze, die ich erwarten würde. Nutzer werden gezwungen, abrupte Änderungen und Experimente mitzutragen, und dadurch entsteht eine Situation, in der nur genug Menschen investieren müssen, damit sich die Illusion halten lässt: „Vor 1.0 darf es kaputt sein, irgendwann wird es schon gut.“ Mein Fazit ist: Dieser Tag wird nie kommen. Ich halte es nicht für wünschenswert, anderen die Last der eigenen Experimente aufzubürden. Selbst wenn vorher gesagt wurde, dass es instabil ist und man sich nicht darauf verlassen sollte, bleibt es für die Betroffenen ein Problem, wenn ihnen plötzlich der Boden unter den Füßen weggezogen wird. Ich weiß nicht einmal genau, was Zig eigentlich sein will. Matklad nennt es eine machine level language (verwandtes Interview: lobste.rs - Interview mit Matklad), auf der offiziellen Seite steht dagegen robust, optimal, reusable general-purpose language. Diese beiden Aussagen widersprechen sich. Und es gibt viele Probleme, die gar keine manuelle Speicherverwaltung brauchen, also ist Zig keineswegs eine Allzwecksprache. All dieses Chaos zeigt sich letztlich in der Instabilität von Zig und seiner aufgeblähten Standardbibliothek. Einerseits Einfachheit und Allgemeinheit zu behaupten und andererseits eine Bibliothek dieser Größe zu haben, ist widersprüchlich. Async wurde ebenfalls so verkauft, als sei es eine universelle Lösung, obwohl sich so etwas nicht auf allen Plattformen gleichermaßen effizient implementieren lässt. Früher wurde es damit beworben, das Problem der function coloring zu lösen, aber dieser Versuch wurde bereits aufgegeben. Die Logik, man solle jetzt erneut glauben, dass es diesmal funktioniert, wirkt seltsam. Tatsächlich braucht man zur Implementierung eines Compilers auf allen Plattformen nur die grundlegenden Assembler-Instruktionen, und luajit hat sogar den Parser komplett in reinem Assembler umgesetzt und funktioniert praktisch überall gut. Ich programmiere größtenteils in Lua und habe kaum je Bugs im Interpreter erlebt. Mir fällt auch kein Problem ein, das Zig besser lösen würde als luajit. Selbst wenn es etwas gäbe, das nur Zig lösen kann, könnte man diesen Teil in Lua-Code einbetten und per FFI anbinden. Der Großteil des Codes braucht solche Low-Level-Optimierungen überhaupt nicht. Wenn man Zig einführt, handelt man sich eher zusätzliche Kopfschmerzen ein. Die überzogenen Erwartungen an Zig haben inzwischen ein Maß an Realitätsferne wie bei AI erreicht. Wer an Zig glauben will, muss an die leere Hoffnung glauben, dass es irgendwann Fähigkeiten entwickeln wird, die es heute nicht hat. Einen tatsächlichen Ausführungsplan gibt es nicht, nur ein ständiges „Wartet noch ein bisschen“
Ich verstehe nicht, warum eine Bibliothek oder ein Interface verlangt, dass ich selbst Puffer für ihren Typ allokiere. Wenn ich ohnehin selbst parse, brauche ich die Bibliothek nicht, und wenn ich sie benutze, kann das den Austausch sogar kaputt machen. Die eigenartigen Interfaces in Go hängen damit zusammen, dass manche Interfaces das Writer-Interface erweitern (siehe etwa das
hijacker-Interface) oder dass Request-Objekte in mehreren Middleware-Schichten unterschiedlich weiterverwendet werden. Kurz gesagt: Requests müssen nicht erweitert werden, Responses dagegen können sich in sehr verschiedene Formen verwandeln, etwa WebSocket oder TCP-WrapperDass eine Bibliothek die Allokation externer Puffer verlangt, wirkt für mich nicht ungewöhnlich. Das gibt mehr Flexibilität, erfordert aber auch mehr Handarbeit. Wenn man zum Beispiel bereits einen Buffer-Pool aufgebaut hat, möchte man ihn vielleicht wiederverwenden. Wenn ein Typ intern selbst allokiert, geht das nicht. Oder in Umgebungen, in denen alle Ressourcen vorab allokiert sein müssen, sind spätere Allokationen nicht möglich. Der Nachteil ist, dass vielleicht nur 10 % aller Nutzer diese Flexibilität brauchen und 90 % einfach nur einen Puffer allokieren und weitergeben würden, aber am Ende müssen alle mit mehr Komplexität umgehen. Am besten wäre ein Ansatz, der hohe Flexibilität bietet und zugleich die einfachen Fälle unkompliziert macht. Man könnte zum Beispiel einen Puffer der Länge 0 (oder in Zig
null) übergeben, damit der Typ selbst allokiert, und zusätzlich einen Constructor ohne Puffer für einfache Nutzung anbieten. Natürlich ist klar, dass so etwas die Dokumentation besonders mühsam machtDas ähnelt den Konventionen, die jede Sprache auf ihre Weise wählt, wie Radiant versus Grad. Jedes IO lässt sich frei transformieren. Auf der einen Seite nennt man es Mock, in einer anderen Sprache vielleicht
unsafeFoo. Andrew Kelley hat im Livestream eigenständig Muster wiederentdeckt, über die die Haskell-Community 30 Jahre lang diskutiert hat. Also gehört die Zukunft Zig. Er hat es zuerst erkanntDie Bedeutung externer Puffer ist einfach, dass die Funktion die Pufferallokation auslässt
Ich habe nicht vor, mein Zig-Side-Project auf 0.15.x anzuheben. Ich verstehe, warum Andrew sich für den Release entschieden hat, und ich respektiere es, Early Adoptern das neue IO in die Hand zu geben. Aber seit den massiven Änderungen an Readers/Writers sind erst ein paar Tage vergangen. Für Leute, die an der Standardbibliothek arbeiten, mag das gut sein, aber für Hobby-Nutzer wie mich wirkt es klüger, bis nach der Stabilisierung in 0.16.0 zu warten
Wenn die Sprache Zig heißt, sollte sie dann nicht gelegentlich auch Zag machen?
Loris Cro, ein Zig-Core-Mitglied, hat in einem aktuellen Interview ebenfalls erwähnt, dass er Updates seiner Projekte mit Zig aufschiebt, bis sich die Nachwirkungen der IO-Änderungen gelegt haben. Der Ausblick danach ist aber positiv. Sowohl Andrew als auch Loris gehen davon aus, dass dies die letzte größere Änderung sein wird, daher gibt es Hoffnung, dass 1.0 nicht mehr fern ist. Der einzige große Unsicherheitsfaktor ist derzeit die Auswirkung der wieder eingeführten stack-less coroutines
Nachdem ich den Beitrag über das neue IO-Interface gelesen habe, habe ich mich entschieden, Zig eher zu meiden. Zum Glück war mein Instinkt wohl richtig. Auch wenn die Gründe andere sind, fühlt sich das Ergebnis letztlich nach einer Komplexität an, die an die Umständlichkeit vor C++11 erinnert. Es ist wieder dieses vertraute Muster: Eine neue Sprache will Ersatz sein und wird am Ende genauso komplex wie die bestehenden Sprachen
Der Hinweis des OP, dass es inkonsistent ist,
Stream.Readerüber die Methodeinterface()instd.Io.Readerzu konvertieren, während man fürStream.Writereinstd.io.Writerüber die Adresse des Felds&interfaceerhalten muss, wäre in der Go-Community meiner Meinung nach sofort abgelehnt worden. In Go werden selbst kleine Änderungen erst nach extrem gründlicher Analyse entschieden. Mein Lieblingsbeispiel dafür ist: Go github issue #45624. Dort wird vier Jahre lang diskutiert, bevor eine Schlussfolgerung gezogen wird. Das mag langsam sein, aber es achtet sorgfältig auf Konsistenz, Designüberlegungen und die tatsächliche Code-Nutzung. Langsam, aber genau so schnell, wie es sein muss. Die so getroffenen Entscheidungen sind am Ende von sehr hoher QualitätBei Rust ist es ähnlich. Es gibt viele nützliche Funktionen, die nur in nightly Rust existieren und nicht in stable (zum Beispiel Generators). Das ist frustrierend, aber die Features, die in stable gelangen, werden sehr tiefgehend geprüft. Ich bin ungeduldig, aber ich halte den Ansatz des Rust-Teams für richtig
Vor Go 1.0 war es nicht so langsam. Es gab häufig größere Änderungen, selbst wenn sie nicht ganz so grundlegend waren (Abschaffung von Semikolons, Änderung des Error-Typs usw.), und es wurden auch automatische Migrationswerkzeuge unterstützt. Erst ab 1.0 wurde Stabilität versprochen und der heutige Ansatz eingeführt
Zig ist die erste Sprache, die mir für Low-Level-Arbeit einfällt. Es ist außerdem sehr cool, dass man Zig auch als C/C++-Cross-Compiler verwenden kann
Für mich sieht es so aus, als ließen sich die meisten Probleme hier auf fehlende oder schlechte Dokumentation zurückführen
output.write("hello"), aber in der Praxis stiftet es Verwirrung, weil die Erklärung der Nutzung fehlt. Ich frage mich auch, ob die Standardbibliothek ein derart komplexes Typsystem überhaupt ausdrücken muss. Zig als Ganzes besteht aus klaren, knappen und gut lesbaren Methoden, aber das neue IO-System passt dazu nicht und wirkt unintuiv(Das neue System von Zig) ist problematisch, weil es ein Konzept, das eigentlich nur dazu diente, Ausführungsgrenzen zu trennen, in die gesamte Runtime-Engine hineingemischt hat, ohne klar zu zeigen, wie beide Seiten miteinander verbunden werden sollen