Elegance of Reason

Sunday, February 26, 2006

The Good, the Bad and the Ugly, part 3

Making fprintf() to standard error an example of why not checking return codes is bad is admittedly somewhat contrived, but this installment is going to amend for that by addressing another problem in the original code.

Before calling popen(), you may notice, the code invokes (void)fflush(NULL) - ignoring the result. The reason for this is somewhat involved, but easily explained: C standard I/O is buffered, so you might output a few characters, so few that they would sit in your stdout buffer waiting for more, invoke through popen() a program which sends output to standard output on its own, and this output would appear before the output your program produced before the call, which would be still sitting in its buffer. Calling fflush(NULL) flushes all open streams, so it's a kind of blanket prevention for the problem with output; when reading from standard input and invoking a program which itself reads from standard input (an input filter), the problem gets harder and will not be discussed here, and the same goes for similar problems associated with file descriptors.

You might wonder why popen() does not perform the equivalent operation by itself; the only reason I can offer you is that the standard says it does not need to. In a few specific cases, such as ours by the way, fflush() is superfluous: at the point in code where it's called, no output has been produced yet so no output could possibly be sitting in a buffer - but in this case fflush() should complete with little fuss anyway.

The real reason, in my opinion, is not visible under the microscope we're using to dissect this toy program, because it's much larger than that. In real code you don't call fflush(NULL) and ignore the result; an error would mean that some data that you thought you output might have failed to make it from the buffer to its intended destination. Can you say data loss ? I thought you could.

In real code, we would track each stream and would know within our code whether it needs to be flushed and, most important of all, what to do in case calling fflush() on it failed; this might include charging along into popen(), but chances are you would want to do more than that, and fflush(NULL) does not even tell you which stream had a problem, nor can help you address the case when more than one had a problem, and they were not the same problem.

In our toy program, fflush(NULL) should not do anything, much less fail. In part 2, two strategies for handling "should not happen" circumstances have been shown: cast to void or assert(). Note that our toy program already handles the case of popen() not providing a stream explicitly; the focus here is on handling the "should not happen" case instead.

A cast to void results in the program charging on
as if the call succeeded (not quite, in fact, as we'll discover when discussing what happens to errno). The writer is making a statement to the effect that

  • there is no interest in any of the circumstances reported

  • any reported circumstance does not invalidate the satisfactory operation of the code which follows

and while the first is entirely within the writer's judgement, the second inevitably includes a dose of wishful thinking, unless the definition of "satisfactory operation" is very accomodating.

The above paragraph should not be construed as banning the casting approach altogether; our toy program from part 2, for example, invokes fprintf() at most once in its operation, and then exits. If the invocation fails, there is little we can do (writing an error message just failed, after all) so carrying on as if the problem did not occur is appropriate: there is no interest in the circumstance, and a fair amount of confidence that it will not impact the program exiting shortly thereafter.

In the case of fflush(NULL), however, there is no such confidence that popen() will be guaranteed to function as advertised; on the contrary, such an unlikely failure would suggest nasty problems with the execution environment, so carrying along as if nothing happened does not cut it. The acknowledged intent of assert(), however, is guarding against inconsistencies in "our" code, not in the execution environment: on failure assert() aborts execution, which is an operation tied to the execution environment if there is one. So this is a circumstance that must be handled explicitly:


/*
popen-test: experiment with popen(), Part 3.

Copyright © 2006 Davide Bolcioni <dbolcion@libero.it>

Distributed under the GPL,
see http://www.gnu.org/copyleft/gpl.html.

*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char* argv[])
{
int rc = EXIT_FAILURE; /* Optimist */

if (argc > 1) {
FILE* sink;

if (fflush(NULL) != EOF) { /* all open files */
sink = popen(argv[1], "w");

if (sink) {
int status;
int i;

/* NB: deliberately allow to test for no output sent */

for (i = 2; i < argc; i++) {
(void)fputs(argv[i], sink);
}

status = pclose(sink);

/* The return value is funny */

if (status == -1) {
(void)fprintf(stderr,
"%s: pclose() failed.\n",
argv[0]);
}
else if (WIFEXITED(status)) {
rc = WEXITSTATUS(status);
(void)fprintf(stderr,
"%s: child did an exit(%d).\n",
argv[0], rc);
}
else if (WIFSIGNALED(status)) {
(void)fprintf(stderr,
"%s: child terminated by signal %d.\n",
argv[0], WTERMSIG(status));
}
else if (WIFSTOPPED(status)) {
(void)fprintf(stderr,
"%s: child stopped with signal %d.\n",
argv[0], WSTOPSIG(status));
}
else {
(void)fprintf(stderr,
"%s: I don't think we're in Kansas anymore, Toto.\n",
argv[0]);
}
}
else {
(void)fprintf(stderr,
"%s: popen(\"%s\") failed.\n",
argv[0], argv[1]);
}
}
else {
(void)fprintf(stderr,
"%s: fflush(NULL) failed.\n",
argv[0]);
}
}
else {
(void)fprintf(stderr,
"Usage: %s <command> [<arg>]...\n", argv[0]);
}

return rc;
}

/*
vim:sw=2:nowrap
*/


The code gets uglier still, but at least it's handling the case when unforeseen external occurrences prevent it from proceeding further. One cause of badness, ignoring the result of functions called for their side effects, has been addressed; in the next installment we'll have a look at the handling of errno and at programs that don't tell you why they fail.