Elegance of Reason

Monday, May 15, 2006

The Good, the Bad and the Ugly, part 4

If you thought the code in part 3 was ugly, this installment will make you reconsider: we'll discuss the use of errno.

Apparently, there is little to say about errno; when a function returns failure, it sets the global variable1 errno to some numeric value which is supposed to give more details about what went wrong. When it returns success, you should not look at the value of errno because it might have changed anyway. Suppose we write a mightily uninteresting frobble() function:


/* The frobble() function returns 0 on success and -1 on failure */
int frobble() {
return -1;
}

Now, frobble() is documented to fail but there is no sign of it affecting errno in any way; the handling of errno is a convention, which dates back to the beginnings of C and UNIX, so functions have to be explicitly written to follow this convention and should be documented accordingly: system calls and library functions often do, but the only way to tell is to read the documentation.

The reason we're discussing errno lies in a problem with our toy program. Suppose we had the following run:


[db@centaur ~] ./popen-test /bin/true
[db@centaur ~] ./popen-test: popen("/bin/true") failed.

We get no clue about the reason which caused popen() to fail; whatever information errno might have conveyed is not made available. All too often, when programs fail they do not bother to tell you why, which makes the life of your system administrator friends either very simple (just blame you, the developer) or very complicated.

To rectify this, our task is conceptually simple: whenever we invoke a function which sets errno on failure, just make sure its value is included in the error message. Since the value is just an integer, which is not very user-friendly, we might want to carry this a step further and provide some kind of explanatory message using perror() (we'll get more sophisticated later). Note that we're not referencing errno in our code, just invoking perror().


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

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: ",
argv[0]);
perror("");
}
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: ",
argv[0], argv[1]);
perror("");
}
}
else {
(void)fprintf(stderr,
"%s: fflush(NULL) failed: ",
argv[0]);
perror("");
}
}
else {
(void)fprintf(stderr,
"Usage: %s <command> [<arg>]...\n", argv[0]);
}

return rc;
}

/*
vim:sw=2:nowrap
*/


The more observant among you might notice that we're blindly ignoring failures, and the attendant value of errno, resulting from the invocations of fputs() in the for loop. This illustrates why casting away error handling to void is slightly better than silently ignoring error conditions: it is easier to spot even after some time. In the code above, we should probably leave the loop as soon as the first fputs() fails and report the value of errno:


...

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

for (i = 2; i < argc; i++) {
if (fputs(argv[i], sink) == EOF) {
int errno_from_fputs = errno;
(void)fprintf(stderr, "%s: fputs(\"%s\") failed: ",
argv[0],
argv[i]);
errno = errno_from_fputs;
perror("");
break;
}
}

status = pclose(sink);

...


As you can see, in ugliness contests global variables such as errno have a head start: because fprintf() might affect errno even when not failing, we have to save it and then restore it back for the use of perror().

Note that we had to change a few \n in our fprintf() format strings to a colon followed by a space, so that perror() output follows the message on the same line; if supplied an non-empty argument perror() would do just that on its own, but this would require us to format the message as a string, with all the attendant complications.

Next time, our toy program will attempt to climb back up from the pits of ugliness it sank to.

Note [1]: this is not entirely correct, because errno is documented as an lvalue and might be a macro which expands to some funny construct intended to carry over all the above in the world of POSIX threads without too much pain. For this reason, errno should be accessed only after explicitly including <errno.h> even if perror() in all likelyhood already includes it.

0 Comments:

Post a Comment

Links to this post:

Create a Link

<< Home