selectboostboost-asiostdinncurses

Schedule an asynchronous event that will complete when stdin has waiting data in boost::asio?


I'm using boost::asio with ncurses for a command-line game. The game needs to draw on the screen at a fixed time interval, and other operations (e.g. networking or file operations) are also executed whenever necessary. All these things can be done with async_read()/async_write() or equivalent on boost::asio.

However, I also need to read keyboard input, which (I think) comes from stdin. The usual way to read input in ncurses is to call getch(), which can be configured to either blocking (wait until there is a character available for consumption) or non-blocking (return a sentinel value of there no characters available) mode.

Using blocking mode would necessitate running getch() on a separate thread, which doesn't play well with ncurses. Using non-blocking mode, however, would cause my application to consume CPU time spinning in a loop until the user presses their keyboard. I've read this answer, which suggests that we can add stdin to the list of file descriptors in a select() call, which would block until one of the file descriptors has new data.

Since I'm using boost::asio, I can't directly use select(). I can't call async_read, because that would consume the character, leaving getch() with nothing to read. Is there something in boost::asio like async_read, but merely checks the existence of input without consuming it?


Solution

  • I think you should be able to use the posix stream descriptor to watch for input on file descriptor 0:

    ba::posix::stream_descriptor d(io, 0);
    input_loop = [&](error_code ec) {
        if (!ec) {
            program.on_input();
            d.async_wait(ba::posix::descriptor::wait_type::wait_read, input_loop);
        }
    };
    

    There, program::on_input() would call getch() with no timeout() until it returns ERR:

    struct Program {
        Program() {
            initscr();
            ESCDELAY = 0;
            timeout(0);
            cbreak(); 
    
            noecho();
            keypad(stdscr, TRUE); // receive special keys
    
            clock   = newwin(2, 40, 0, 0);
            monitor = newwin(10, 40, 2, 0);
    
            syncok(clock, true);    // automatic updating
            syncok(monitor, true);
    
            scrollok(monitor, true); // scroll the input monitor window
        }
        ~Program() {
            delwin(monitor);
            delwin(clock);
            endwin();
        }
    
        void on_clock() {
            wclear(clock);
    
            char buf[32];
            time_t t = time(NULL);
            if (auto tmp = localtime(&t)) {
                if (strftime(buf, sizeof(buf), "%T", tmp) == 0) {
                    strncpy(buf, "[error formatting time]", sizeof(buf));
                }
            } else {
                strncpy(buf, "[error getting time]", sizeof(buf));
            }
    
            wprintw(clock, "Async: %s", buf);
            wrefresh(clock);
        }
    
        void on_input() {
            for (auto ch = getch(); ch != ERR; ch = getch()) {
                wprintw(monitor, "received key %d ('%c')\n", ch, ch);
            }
            wrefresh(monitor);
        }
    
        WINDOW *monitor = nullptr;
        WINDOW *clock = nullptr;
    };
    

    With the following main program you'd run it for 10 seconds (because Program doesn't yet know how to exit):

    int main() {
        Program program;
    
        namespace ba = boost::asio;
        using boost::system::error_code;
        using namespace std::literals;
    
        ba::io_context io;
        std::function<void(error_code)> input_loop, clock_loop;
    
        // Reading input when ready on stdin
        ba::posix::stream_descriptor d(io, 0);
        input_loop = [&](error_code ec) {
            if (!ec) {
                program.on_input();
                d.async_wait(ba::posix::descriptor::wait_type::wait_read, input_loop);
            }
        };
    
        // For fun, let's also update the time
        ba::steady_timer tim(io);
        clock_loop = [&](error_code ec) {
            if (!ec) {
                program.on_clock();
                tim.expires_after(100ms);
                tim.async_wait(clock_loop);
            }
        };
    
        input_loop(error_code{});
        clock_loop(error_code{});
        io.run_for(10s);
    }
    

    This works:

    enter image description here

    Full Listing

    #include <boost/asio.hpp>
    #include <boost/asio/posix/descriptor.hpp>
    #include <iostream>
    #include "ncurses.h"
    
    #define CTRL_R    18
    #define CTRL_C    3
    #define TAB       9
    #define NEWLINE   10
    #define RETURN    13
    #define ESCAPE    27
    #define BACKSPACE 127
    #define UP        72
    #define LEFT      75
    #define RIGHT     77
    #define DOWN      80
    
    struct Program {
        Program() {
            initscr();
            ESCDELAY = 0;
            timeout(0);
            cbreak(); 
    
            noecho();
            keypad(stdscr, TRUE); // receive special keys
    
            clock   = newwin(2, 40, 0, 0);
            monitor = newwin(10, 40, 2, 0);
    
            syncok(clock, true);    // automatic updating
            syncok(monitor, true);
    
            scrollok(monitor, true); // scroll the input monitor window
        }
        ~Program() {
            delwin(monitor);
            delwin(clock);
            endwin();
        }
    
        void on_clock() {
            wclear(clock);
    
            char buf[32];
            time_t t = time(NULL);
            if (auto tmp = localtime(&t)) {
                if (strftime(buf, sizeof(buf), "%T", tmp) == 0) {
                    strncpy(buf, "[error formatting time]", sizeof(buf));
                }
            } else {
                strncpy(buf, "[error getting time]", sizeof(buf));
            }
    
            wprintw(clock, "Async: %s", buf);
            wrefresh(clock);
        }
    
        void on_input() {
            for (auto ch = getch(); ch != ERR; ch = getch()) {
                wprintw(monitor, "received key %d ('%c')\n", ch, ch);
            }
            wrefresh(monitor);
        }
    
        WINDOW *monitor = nullptr;
        WINDOW *clock = nullptr;
    };
    
    int main() {
        Program program;
    
        namespace ba = boost::asio;
        using boost::system::error_code;
        using namespace std::literals;
    
        ba::io_context io;
        std::function<void(error_code)> input_loop, clock_loop;
    
        // Reading input when ready on stdin
        ba::posix::stream_descriptor d(io, 0);
        input_loop = [&](error_code ec) {
            if (!ec) {
                program.on_input();
                d.async_wait(ba::posix::descriptor::wait_type::wait_read, input_loop);
            }
        };
    
        // For fun, let's also update the time
        ba::steady_timer tim(io);
        clock_loop = [&](error_code ec) {
            if (!ec) {
                program.on_clock();
                tim.expires_after(100ms);
                tim.async_wait(clock_loop);
            }
        };
    
        input_loop(error_code{});
        clock_loop(error_code{});
        io.run_for(10s);
    }