Tech News
← Back to articles

I'm too dumb for Zig's new IO interface

read original related products more articles

I'm too dumb for Zig's new IO interface

You might have heard that Zig 0.15 introduces a new IO interface, with the focus for this release being the new std.Io.Reader and std.Io.Writer types. The old "interfaces" had problems. Like this performance issue that I opened. And it relied on a mix of types, which always confused me, and a lot of anytype - which is generally great, but a poor foundation to build an interface on.

I've been slowly upgrading my libraries, and I ran into changes to the tls.Client client used by my smtp library. For the life of me, I just don't understand how it works.

Zig has never been known for its documentation, but if we look at the documentation for tls.Client.init , we'll find:

pub fn init ( input : * std . Io . Reader , output : * std . Io . Writer , options : Options ) InitError ! Client Initiates a TLS handshake and establishes a TLSv1 . 2 or TLSv1 . 3 session .

So it takes one of these new Readers and a new Writer, along with some options (sneak peak, which aren't all optional). It doesn't look like you can just give it a net.Stream , but net.Stream does expose a reader() and writer() method, so that's probably a good place to start:

const stream = try std . net . tcpConnectToHost ( allocator , "www.openmymind.net" , 443 ) ; defer stream . close ( ) ; var writer = stream . writer ( & . { } ) ; var reader = stream . reader ( & . { } ) ; var tls_client = try std . crypto . tls . Client . init ( reader . interface ( ) , & writer . interface , . { } , ) ;

Note that stream.writer() returns a Stream.Writer and stream.reader() returns a Stream.Reader - those aren't the types our tls.Client expects. To convert the Stream.Reader to an *std.Io.Reader , we need to call its interface() method. To get a *std.io.Writer from an Stream.Writer , we need the address of its &interface field. This doesn't seem particularly consistent. Don't forget that the writer and reader need a stable address. Because I'm trying to get the simplest example working, this isn't an issue - everything will live on the stack of main . In a real word example, I think it means that I'll always have to wrap the tls.Client into my own heap-allocated type; giving the writer and reader have a cozy stable home.

Speaking of allocations, you might have noticed that stream.writer and stream.reader take a parameter. It's the buffer they should use. Buffering is a first class citizen of the new Io interface - who needs composition? The documentation does tell me these need to be at least std.crypto.tls.max_ciphertext_record_len large, so we need to fix things a bit:

var write_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var writer = stream . writer ( & write_buf ) ; var read_buf : [ std . crypto . tls . max_ciphertext_record_len ] u8 = undefined ; var reader = stream . reader ( & read_buf ) ;

... continue reading