[vox-tech] a better scanf?? (C-programming question)

Ken Herron vox-tech@lists.lugod.org
Fri, 18 Apr 2003 14:32:15 -0700


--On Friday, April 18, 2003 04:48:50 -0700 Andy Campbell 
<atcampbell@ucdavis.edu> wrote:

>>   Non-blocking with select/poll/busy-looping, reading into a
>> buffer, followed by sscanf when a return key is found will certainly
>> handle the second case... examples if that's what you want.
>>   Otherwise you will have to go with some curses or raw mode input
>> to get character by character input from a user, and opens a whole
>> can of worms...
>>
>>     TTFN,
>>       Mike
>
> I am trying to implement this but am unsuccessfull. Could you show me
> where  some examples are?? Thank you!!

When you're developing for unix, you have two different sets of functions 
for doing file I/O:

* The unix operating system provides a set of functions which use file 
descriptors: open(), close(), read(), write(), etc. These functions are 
generally provided by the operating system kernel.

* The C language standard defines a different set of functions that use 
file handles (not descriptors): fopen(), fclose(), printf(), etc. These 
are commonly called stdio or "standard I/O". These functions are normally 
implemented as library functions (i.e. code running outside of the 
kernel, just like the functions you write) which call the 
operating-system routines in order to perform actual I/O.

stdin, stdout, and stderr are file handles, not descriptors. They're 
strictly used with the stdio functions, not the OS functions. Further, 
they're generally already open when your program starts up so it isn't 
necessary to open() or fopen() them.

Now let's talk about ttys. If your program is interacting with a human 
than it's probably through a tty device. This is a unix OS construct that 
provides things like the backspace key and killing a program by typing 
^C.  If you want to do non-blocking reads from a tty you have to 
understand a bit about how ttys work.

ttys have three operating modes: line-at-a-time (or "cooked"), cbreak, or 
raw. In raw mode, every character typed by the user is immediately 
available to the program. cbreak mode is much like raw mode, but some 
characters receive special handling, e.g. ^C. In cooked mode, the tty 
driver buffers data typed by the user (and handles line-editing such as 
backspace, ^U, etc.) until the user hits return or ^D. When the user hits 
one of these, the entire line of input becomes available for the program 
to read.

You generally don't use stdio routines to do nonblocking input. The stdio 
library doesn't provide any standard means to see if data is waiting to 
be read without blocking. For example, getchar() always returns either a 
character or an EOF indication. There's no getchar return value that 
means "not EOF but not valid, either". Similarly, when you use fgets() to 
read a line, fgets will block until it reads an entire line. Even reading 
from a tty in cbreak mode, with characters dribbling in as the user types 
them, fgets() won't return control until it gets that newline character.

So if you want to do non-blocking reads you have to use the OS routines 
to do it, rather than the stdio routines. The os routine is called 
read(), but it also blocks until data becomes available. There are two 
ways around that:

1) Use select() or poll() to test the file descriptor first; perform the 
read() only if select/poll indicates you can do it without blocking.

2) Set the file descriptor to nonblocking at open() time or by using 
fcntl(). Then, if you call read() and no data is waiting, then read() 
will return -1 and errno will be set to EWOULDBLOCK.

Either way, every time you read in some data, you'll need to save it in a 
buffer somewhere until you've read a newline (or whatever marks the end 
of the input). Then you can call sscanf() to parse the buffer contents if 
you like.

Since you'll be using read() and select()/poll(), you'll need to know 
what file descriptor to use. You can do any of three things:

1) call fileno(stdin). fileno() returns the file descriptor associated 
with a file handle.

2) Just use 0. File descriptors are just numbers. 0 is standard input, 1, 
is standard output, and 2 is standard error.

3) Forget about using stdin. Call open("/dev/tty", O_RDWR) instead. This 
avoids the question of what to do if standard input isn't actually a tty 
device for some reason (e.g., because the user redirected input when he 
launched the program).

By now you're probably wishing there were an easier way. There are at 
least three alternatives to doing all of this:

1) Use curses/ncurses. Curses is a library for doing fancy operations on 
terminals, such as positioning the cursor or printing boldface text. 
Among its features, it includes a set of functions for doing nonblocking 
character input.

2) Use separate processes. Call fork() and let the child process perform 
the operations that shouldn't be interrupted while the parent process 
interacts with the user.

3) Use separate threads. A thread is your execution context, i.e. the 
part of the program being executed at any given time and its local 
variables. Under the classic program model that you're probably used to, 
a running program only has one thread, but it's possible to start 
additional threads. The benefit is that a single thread can perform an 
operation that blocks (for example, using stdio to read a line from the 
user) without preventing other threads from executing.

Threads have a reputation for being difficult to work with. It's true 
there's a bit of a learning curve, but personally I've never had much 
trouble with them. They're definitely the wave of the future; java 
contains sophisticated thread support, and most complex GUI-driven 
programs are multithreaded. If you aspire to do serious programming 
you'll probably end up learning them anyway; better to do it now before 
you develop a lot of bad programming habits.

-- 
Kenneth Herron  Kherron@newsguy.com     916-366-7338