How to handle window events while waiting for terminal input?

668 views Asked by At

I've got a cross-platform (windows and unix+xcb) terminal+graphics_window application and it mostly works ok, until you wait too long at the input prompt, where under heavy load the image may disappear. :(

I've got a mainloop (REPL) for the interpreter (postscript interpreter) which calls an event handler function each time around its loop. The event handler performs one iteration of what would normally be the window's message/event loop. But input is processed with normal C i/o and so the event handler never gets called when blocked in fgetc().

The graphics window is output-only. It has no buttons and only needs to respond to events like Raise, Map, Expose, etc.

How can I arrange to have the event handler called during input reading loops deeper in the call stack? This needs to be possible to implement with both POSIX and win32 APIs.

The options appear to be

  • Non-blocking i/o
    relatively simple in unix. looks like a pain in windows
  • Polling
  • Input thread
    pthreads?
  • Window thread
    pthreads?

Are any of these likely to be less painful than the others?

If I could just stay on unix, then this would seem to do the whole trick:

#include <errno.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

void idleproc () {  /* simulates calling the event handler 
                        (ie. one slice of the window loop) */
    //printf("idle\n");
    putchar('.');
}

int idlefgetc (FILE *stream) {
    int ret;

    do {
        ret = fgetc(stream);
        idleproc();
    } while(ret == EOF && 
            (errno == EAGAIN || errno == EINTR));

    return ret;
}

int setraw (FILE *stream) {
    struct termios tbuf;
    if (tcgetattr(fileno(stream), &tbuf) == -1)
        return -1;
    tbuf.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
            | INLCR | IGNCR | ICRNL | IXON);
    tbuf.c_oflag &= ~OPOST;
    tbuf.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tbuf.c_cflag &= ~(CSIZE | PARENB);
    tbuf.c_cflag |= CS8;
    if (tcsetattr(fileno(stream), TCSANOW, &tbuf) == -1)
        return -1;
    return 0;
}

int setnonblocking (FILE *stream) {
    int flags;

    if (setraw(stream) != 0)
        return -1;
    if (!((flags = fcntl(fileno(stream), F_GETFL)) & O_NONBLOCK)) {
        flags |= O_NONBLOCK;
        fcntl(fileno(stream), F_SETFL, flags);
    }
    return 0;
}

int main (int argc, char **argv) {
    if (setnonblocking(stdin)) {
        perror(argv[0]);
        return 0;
    }
    printf("'%d'\n", idlefgetc(stdin));
    system("stty sane");
    return 0;
}
2

There are 2 answers

1
jpmuc On BEST ANSWER

Under Windows you need to use the Console API. You can perform asynchronous, non-blocking, reading of characters with ReadFileEx. Another possibility is ReadConsoleInput, and poll continously for input, without blocking. With SetConsole you can decide, what events to capture.

For more details on asynchronous I/O under Windows, see here.

0
luser droog On

Another solution seems possible due to the fact that this is a programming-language interpreter after all.

It should be possible to re-implement the code that uses fgetc to instead use the ps primitive: -file-readint bool. This postscript operator itself uses the stdio call, but it can be called by the other file-reading functions with a continuation-passing style by pushing on the exec stack and returning. This would naturally interleave more calls to the event handler since it returns to the main loop more often.

I may still need to use a non-blocking read. But this will be easier to manage if it is called in only one place. Continuation-passing has the advantage of breaking-up larger functions into separate stages and has been successfully used to implement the window devices themselves by overriding methods in base class (the base class is implemented as a postscript dictionary). But it's still fairly new to me, so it's not my go-to duct-tape just yet. :)

I'll re-work this prototype to illustrate the approach after work. :)

Edit: Took a few days. But here's the new idea. For windows, it will need to do the nonblocking call differently, but the call can be isolated to this one place. And with the continuation-passing, the file-reading function does not need access to (or knowledge of) the event-handler, so better encapsulation.

This program behaves the same as the one in the question, it prints . repeatedly until a keypress, then it prints the ascii code of the keypress. The . simulates repeatedly calling the event handler while waiting for a keystroke. I had to mock-up a bit of the interpreter guts: an object type, some stacks, and an eval() function. So this illustrates the REPL better as well.

#include <errno.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

int set_raw_term (FILE *stream) {
    struct termios tbuf;
    if (tcgetattr(fileno(stream), &tbuf) == -1) 
        return -1; 
    tbuf.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
            | INLCR | IGNCR | ICRNL | IXON);
    tbuf.c_oflag &= ~OPOST;
    tbuf.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tbuf.c_cflag &= ~(CSIZE | PARENB);
    tbuf.c_cflag |= CS8;
    if (tcsetattr(fileno(stream), TCSANOW, &tbuf) == -1) 
        return -1; 
    return 0;
}

int set_nonblocking (FILE *stream) {
    int flags;

    if (set_raw_term(stream) != 0)
        return -1; 
    if (!((flags = fcntl(fileno(stream), F_GETFL)) & O_NONBLOCK)) {
        flags |= O_NONBLOCK;
        fcntl(fileno(stream), F_SETFL, flags);
    }   
    return 0;
}

int event_handler() {
    putchar('.');
}

enum { null, integer, file, operator };
typedef union {
    short tag;
    struct {
        short tag;
        int val;
    } int_;
    struct {
        short tag;
        FILE *f; 
    } file_;
    struct {
        short tag;
        int (*fp)();
    } oper_;
} object; /* object union allows multiple types on the stacks */

object os[100];   /* operand stack */
object *tos = os; /* top of operand stack */
object es[100];   /* execution stack */
object *tes = es; /* top of execution stack */

int eval () {
    if (tes == es) /* execution stack is empty */
        return -1; /* return "finished" */

    event_handler();

    switch(tes[-1].tag) { /* type of object on top of execution stack */
        case integer:
        case file:
            *tos++ = *--tes;     /* push file or integer to operand stack */
            break;

        case operator:
            (--tes)->oper_.fp(); /* call operator function */
            break;
    }
    return 0; /* return "not finished" */
}

int file_read_byte () {
    int ret;
    object arg;

    arg = *--tos; /* pop argument from operand stack */
    ret = fgetc(arg.file_.f);
    if (ret == EOF && (errno == EAGAIN || errno == EINTR)) { /* if no data */
        *tos++ = arg;   /* restore argument to operand stack */
        *tes++ = (object){ .oper_.tag = operator, .oper_.fp = file_read_byte }; /* push continuation to execution stack */
        return 0;
    } else {
        *tos++ = (object){ .int_.tag = integer, .int_.val = ret }; /* push result to operand stack */
        return 0;
    }
}


int main(int argc, char **argv) {
    int ret;

    if (set_nonblocking(stdin) != 0) {
        perror(argv[0]);
        return 0;
    }

    //printf("'%d'\n", file_read_byte(stdin));
    *tos++ = (object){ .file_.tag = file, .file_.f = stdin }; /* push file argument to operand stack */
    *tes++ = (object){ .oper_.tag = operator, .oper_.fp = file_read_byte }; /* push operator object to execution stack */

    ret = 0;
    while (ret == 0) { /* call eval until execution is "finished" */
        ret = eval();
    }
    printf("'%d'\n", (--tos)->int_.val); /* pop returned value */

    system("stty sane");
    return 0;
}