Implementing user input that does not block program execution

Recently, I had some free time, so I decided to improve my knowledge of the C language. So far, they are limited by the level of simple institute laboratory work and S. Prat’s book “C Programming Language,” so for starters I set myself the simple task of writing a console snake. By the time when the display of the playing field and the snake itself on the screen and the part responsible for moving it around the field was written, two problems appeared:
  • data was sent to the program only after pressing Enter
  • the program stopped reading user input

You could solve them by using getch () from curses, but it was boring and not interesting.
About how these problems were solved under the cut.
First, we had to cope with reading the user's keystrokes. I implemented this through a call to the getc () function, but when this function was called, the program began to read characters only after Enter was pressed. During a short search, it was found that this is not a getc () problem, but a Linux terminal problem, more precisely a terminal driver, which by default works in canonical mode, i.e. this terminal driver waits for the end of the line input by pressing Enter, and then sends the given line to the stdin of the program. A solution to this problem was also found: putting the driver in non-canonical mode. It is implemented very simply - the header file termios.h is connected and two functions are written:
The first function to transfer to non-canonical mode:
void set_keypress(void)
{
	struct termios new_settings;
	tcgetattr(0,&stored_settings);
	new_settings = stored_settings;
	/* 
		Отключение канонического режима и вывода на экран 
		и установка буфера ввода размером в 1 байт 
	*/
	new_settings.c_lflag &= (~ICANON);
	new_settings.c_lflag &= (~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;
	tcsetattr(0,TCSANOW,&new_settings);
	return;
}

The second function to return to its original state:
void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}

The stored_settings variable must be declared global:
static struct termios stored_settings;

Then, at the beginning of the program, the first function is called, and at the end, respectively, the second is called.
In finished form, the test example looks like this:
#include 
#include 
#include 
#include 
static struct termios stored_settings;
void set_keypress(void)
{
	struct termios new_settings;
	tcgetattr(0,&stored_settings);
	new_settings = stored_settings;
	new_settings.c_lflag &= (~ICANON & ~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;
	tcsetattr(0,TCSANOW,&new_settings);
	return;
}
void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}
int main(void)
{
	set_keypress();
	printf("Test: ");
	while(1)
	{	
		// putchar здесь вызывается для того, чтобы проверить работоспособность
		putchar(getchar()); 
	}
	return 0;
}

Now my snake was able to read user input character by character. But a second problem appeared: the program still stopped and waited for the user to press any key. The search on the Internet began again, during which I often came across a “solution” to this problem in the form of what was described above. The result of the search was the advice to use the select () function.
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);

Of the parameters accepted by this function, we are only interested in n, readfds and utimeout:
  • n - this parameter must not exceed the maximum file descriptor in any of the sets. In other words, you must determine the maximum integer value for all your descriptors, increase it by 1, and pass the result as parameter n.
  • readfds - this set is monitored for read data in one or more descriptors. After returning from select, the readfs set will be cleared of all descriptors, except those that have data available for immediate reading by recv () (for sockets) or read () functions (for pipe channels, files and sockets).
  • utimeout - the maximum time that select will wait for a status change. If this parameter is set to NULL, select will be blocked indefinitely, waiting for events in descriptors. When set to 0 seconds, select returns immediately.

Therefore, calling select () will look like this:
select(1, &rfds, NULL, NULL, &tv);

To indicate that events will be considered only for stdin, call
FD_SET(0, &rfds);

The struct timeval structure is defined as follows:
struct timeval {
   time_t tv_sec;    /* секунды */
   long tv_usec;     /* микросекунды */
};

As a result, we get such a test example (do not forget to put the terminal in non-canonical mode, which was already discussed above):
#include 
#include 
#include 
#include 
#include 
#include 
static struct termios stored_settings;
void set_keypress(void)
{
	struct termios new_settings;
	tcgetattr(0,&stored_settings);
	new_settings = stored_settings;
	new_settings.c_lflag &= (~ICANON & ~ECHO);
	new_settings.c_cc[VTIME] = 0;
	new_settings.c_cc[VMIN] = 1;
	tcsetattr(0,TCSANOW,&new_settings);
	return;
}
void reset_keypress(void)
{
	tcsetattr(0,TCSANOW,&stored_settings);
	return;
}
int main(void) 
{
	fd_set rfds;
	struct timeval tv;
	int retval;
	set_keypress();
	while(1)
	{
		FD_ZERO(&rfds);
		FD_SET(0, &rfds);
		tv.tv_sec = 0;
		tv.tv_usec = 0;	
		retval = select(2, &rfds, NULL, NULL, &tv);
		if (retval)
		{
			printf("Data is available now.\n");
			getc(stdin);
		} 
		else
		{
			printf("No data available.\n");
		}
		usleep(100000);
	}
	reset_keypress();
	exit(0);
}


Material Used:

Also popular now: