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();
}
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)
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);)?
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 . 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 . I’m using NixOS (and Zig 0.16.0 for more context)
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.
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.
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});
}
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
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", .{});
}
}