Search code examples
windowsperliononblocking

Making STDIN unbuffered under Windows in Perl


I am trying to do input processing (from the console) in Perl asynchronously. My first approach was to use IO::Select but that does not work under Windows.

I then came across the post Non-buffered processor in Perl which roughly suggests this:

binmode STDIN;
binmode STDOUT;
STDIN->blocking(0) or warn $!;
STDOUT->autoflush(1);

while (1) {
    my $buffer;
    my $read_count = sysread(STDIN, $buffer, 4096);

    if (not defined($read_count)) {
        next;
    } elsif (0 == $read_count) {
        exit 0;
    }
}

That works as expected for regular Unix systems but not for Windows, where the sysread actually does block. I have tested that on Windows 10 with 64-bit Strawberry Perl 5.32.1.

When you check the return value of blocking() (as done in the code above), it turns out that the call fails with the funny error message "An operation was attempted on something that is not a socket".

Edit: My application is a chess engine that theoretically can be run interactively in a terminal but usually communicates via pipes with a GUI. Therefore, Win32::Console does not help.

Has something changed since the blog post had been published? The author explicitely claims that this approach would work for Windows. Any other option that I can go with, maybe some module from the Win32:: namespace?


Solution

  • The solution I now implemented in https://github.com/gflohr/Chess-Plisco/blob/main/lib/Chess/Plisco/Engine.pm (search for the method __msDosSocket()) can be outlined as follows:

    1. If Windows is detected as the operating system, create a temporary file as a Unix domain socket with IO::Socket::Unix for writing.

    2. Do a fork() which actually creates a thread in Perl for Windows because the system does not have a real fork().

    3. In the "parent", create another instance of IO::Socket::Unix with the same path for reading.

    4. In the "child", read from standard input with getline(). This blocks, of course. Every line read is echoed to the write end of the socket.

    5. The "parent" uses the read-end of the socket as a replacement for standard input and puts it into non-blocking mode. That works even under Windows because it is a socket.

    From here on, everything is working the same as under Unix: All input is read in non-blocking mode with IO::Select.

    Instead of a Unix domain socket it is probably wiser to route the communication through the loopback interface because under Windows it is hard to guarantee that a temporary file gets deleted when the process terminates since you cannot unlink it while it is in use. It is also stated in the comments that IO::Socket::UNIX may not work under older Windows versions, and so inet sockets are probably more portable to use.

    I also had trouble to terminate both threads. A call to kill() does not seem to work. In my case, the protocol that the program implements is so that the command "quit" read from standard input should cause the program to terminate. The child thread therefore checks, whether the line read was "quit" and terminates with exit in that case. A proper solution should find a better way for letting the parent kill the child.

    I did not bother to ignore SIGCHLD (because it doesn't exist under Windows) or call wait*() because fork does not spawn a new process image under Windows but only a new thread.

    This approach is close to the one suggested in one of the comments to the question, only that the thread comes in disguise as a child process created by fork().

    The other suggestion was to use the module Win32::Console. This does not work for two reasons:

    1. As the name suggests, it only works for the console. But my software is a backend for a GUI frontend and rarely runs in a console.

    2. The underlying API is for keyboard and mouse events. It works fine for key strokes and most mouse events, but polling an event blocks as soon as the user has selected something with the mouse. So even for a real console application, this approach would not work. A solution built on Win32::Console must also handle events like pressing the CTRL, ALT or Shift key because they will not guarantee that input can be read immediately from the tty.

    It is somewhat surprising that a task as trivial as non-blocking I/O on a file descriptor is so hard to implement in a portable way in Perl because Windows actually has a similar concept called "overlapped" I/O. I tried to understand that concept, failed at it, and concluded that it is true to the Windows maxim "make easy things hard, and hard things impossible". Therefore I just cannot blame the Perl developers for not using it as an emulation of non-blocking I/O. Maybe it is simply not possible.