`std.Io.Reader.takeByte` behavior compared to `std.c.read`

Hi guys!
I’ve been getting my hands dirty on the new Io reader and I think I need some help understanding the behavior of the takeByte function. Long story short, I’m trying to read from stdin byte by byte using the takeByte function, and it unexpectedly throws EndOfStream.

but if I were to use either std.posix.read or std.c.read the code works like I expected it too, am I missing something important here?

while (true) // this works
    var buf: [1]u8 = undefined;
    _ = c_api.read(c_api.STDIN_FILENO, &buf, 1);
}
while (true) // this does not (stdin is my std.Io.Reader)
    const c = try stdin.takeByte();
}

any help is appreciated!

Hi @elyfheim, welcome to Ziggit!

Can you share what the input for stdin you expect? It would also help to see how you set up the stdin Reader.

Reader.takeByte will return EndOfStream when you reach the end of the file, or in this case, when stdin gets closed. So you may want to catch the error here and use it to break out of the loop.

i’m assuming that the stdin is a pipe input from a different process, that error will be returned if there’s no more data cuz there’s no other way to comunicate that info, think of it like EOF. so you may have to do something like this to ignore that one error

while (true) {
    const c = stdin.takeByte() catch |err| {
        // here we're returning an error only if it's an error other than error.EndOfStream
        if (err != error.EndOfStream) return err;
        break;
    };
}

It’s actually terminal input (keypresses), and I’m waiting for each of the byte from there so I’m not really sure why it immediately goes to EndOfStream (because the program should technically be blocking while waiting for the keypress)

I’m working with uncooked terminal IO here, as mentioned above I’m basically trying to read each keypresses as bytes so I can proceed accordingly.

var stdin_buffer: [1024]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buffer);
const stdin = &stdin_reader.interface;

here is how I setup my stdin

This setup works for me. I

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const stdin_file = std.Io.File.stdin();
    const stdout_file = std.Io.File.stdout();
    var stdin_buf: [64]u8 = undefined;
    var stdout_buf: [64]u8 = undefined;
    var stdin = stdin_file.reader(io, &stdin_buf);
    var stdout = stdout_file.writer(io, &stdout_buf);
    defer stdout.flush() catch {};

    while (true) {
        const c = try stdin.interface.takeByte();
        log.debug("C: {d}", .{c});
    }
}

My guess is that there is something in how you are setting the tty to raw mode that is causing a mismatch. Maybe you need to change the file from positional to streaming mode (stdin_file.readerStreaming(io, &stdin_buf);)?

this is what i tried and it works as intended. i got that error only after hitting ctrl-d

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    var buf: [1024]u8 = undefined;
    var stdin = std.Io.File.stdin().reader(io, &buf);

    while (true) {
        const c = stdin.interface.takeByte() catch |err| {
            if (err != error.EndOfStream) return err;
            break;
        };

        std.log.debug("read: {c}", .{c});
    }
}

what os are you using

I also have a feeling that the raw mode flags do have some sort of weird interaction I’m not aware of, sadly the streaming mode also does not work :confused:. I’m still reading the reader implementation on the std documentation hopefully I can find something that can help.

I tried catching the error just like in your code snippet and it does catch the error but the issue is the EndOfStream here is unexpected, it’s not supposed to exit that early :thinking:. I’m using NixOS (and Zig 0.16.0 for more context)

Can you share the code for setting the raw mode flags? That can help the rest of us also look at it.

what output are you getting when runing this snippet?

const stdin = std.Io.File.stdin();
const stat = try stdin.stat(io);
std.log.debug("{t}", .{stat.kind});

if it’s anything other than character_device when it’s not piped in, then there’s a bug in zig (or somewhere (or there’s something i don’t know))

If I understand correctly, this means you actually want to read one byte from the syscall each time, correct? So you actually want unbuffered input, right?

Why do you want std.Io.Reader in the first place? It seems like the wrong tool for the job to me. What reusable code are you passing *std.Io.Reader to? If you’re just passing it around to your own application, then you can store the File and read from it directly without this unnecessary abstraction.

4 Likes

correct, I was under the impression that the std.Io.Reader is also supposed to be the standard for IO operations (including unbuffered), since I heard that the posix stuff is gonna get deprecated. And I think that std.c.read() is dependent on libc, so I was basically trying to find a way to handle unbuffered input without any dependency I guess.

well for that, you can call the readStreaming function from the File directly

const stdin = std.Io.File.stdin();

var buf: [1024]u8 = undefined;
const bytes_read = try stdin.readStreaming(io, &.{&buf});
const input = buf[0..bytes_read];

std.log.debug("read: {s}", .{input});

or if you want to read it byte by byte

while (true) {
    var buf: [1]u8 = undefined;

    _ = stdin.readStreaming(io, &.{&buf}) catch |err| {
        if (err != error.EndOfStream) return err;
        break;
    };

    const c = buf[0];

    std.log.debug("read: {c}", .{c});
}

i’d write a helper function for this

fn takeByteUnbuffered(io: std.Io, file: std.Io.File) !u8 {
    var buf: [1]u8 = undefined;

    const bytes_read = try file.readStreaming(io, &.{&buf});

    // bytes_read could be zero as per the docs
    // and this is not the best way to handle it
    std.debug.assert(bytes_read == 1);

    return buf[0];
}
while (true) {
    const c = takeByteUnbuffered(io, stdin) catch |err| {
        if (err != error.EndOfStream) return err;
        break;
    };

    std.log.debug("read: {c}", .{c});
}
6 Likes

this works, but there was some issue with the flags I was setting on termios, it seems like assigning std.c.termios.cc[std.c.V.MIN] to 0 forced the program to go to EndOfStream, and while this is okay for the std.posix.read it seems like it has a different interaction with readStreaming from std.Io.File, well for now I think I can work with this (need to learn more about VMIN and VTIME though), thank you all for the help! @Southporter @andrewrk

2 Likes

finally managed to get it working with std.c.termios.cc[std.c.V.MIN] equal to 0. Now it’s a non blocking read as well and the behavior is exactly the same as std.posix.read and std.c.read, here is an example snippet if anyone is interested:

const std = @import("std");
const c_api = std.c;

pub fn main(init: std.process.Init) !void {
    const io = init.io;

    const stdin = std.Io.File.stdin();
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_file_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
    const writer = &stdout_file_writer.interface;

    // setup uncooked terminal stuff here
    var termios: c_api.termios = undefined;
    if (c_api.tcgetattr(c_api.STDIN_FILENO, &termios) == -1) {
        std.debug.print("failed", .{});
    }
    var temp_termios = termios;
    temp_termios.lflag.ECHO = false;
    temp_termios.lflag.ICANON = false;
    temp_termios.cc[@intFromEnum(c_api.V.MIN)] = 0;
    temp_termios.cc[@intFromEnum(c_api.V.TIME)] = 1;
    if (c_api.tcsetattr(c_api.STDIN_FILENO, c_api.TCSA.FLUSH, &temp_termios) == -1) {
        std.debug.print("failed", .{});
    }

    while (true) {
        var buf: [1]u8 = [_]u8{'0'};
        _ = stdin.readStreaming(io, &.{&buf}) catch |err| {
            // don't return the error if it's EOS
            if (err != error.EndOfStream) {
                return err;
            }
        };
        try writer.print("{c}\r\n", .{buf[0]});
        try writer.flush();
        if (buf[0] == 'q') {
            break;
        }
    }

    if (c_api.tcsetattr(c_api.STDIN_FILENO, c_api.TCSA.FLUSH, &termios) == -1) {
        std.debug.print("failed", .{});
    }
}
3 Likes