Columns


Standard C

Wha Gang Agley, Part II

P.J. Plauger


P.J. Plauger has been a prolific programmer, textbook author, and software entrepreneur. He is secretary of the ANSI C standards committee, X3J11, and convenor of the ISO C standards committee.

Introduction

Last month, I vented a number of gripes about the ANSI Standard for C. (See "Wha Gang Agley," CUJ April 1990.) I discussed several aspects of the C language that I feel did not get properly cleaned up. I identified a few areas where committee X3J11 actually broke parts of the language in small ways. And I listed several additions that might have made C a better language to use.

This column extends that discussion to the Standard C library. My previous remarks dealt only with the language proper, including preprocessing. Each of the two is sufficiently large as to warrant separate treatment. Besides, we tend to take a different attitude toward the library than to the language itself. It is psychologically easier, for example, to add functions to the library than to add features to the language. That leads to a different mix of shortcomings in the two areas.

<signal.h>

One of the biggest messes we inherited involved the handling of signals. The two functions signal and kill came from the UNIX system interface. You call signal to specify the handling of certain exceptional conditions, such as overflow or the striking of an attention key. You call kill to report a signal and stimulate whatever form of handling was earlier specified.

Both names are misnomers. The function signal does specify the handling of software generated "signals." It also deals with hardware traps, caused by exceptional conditions that arise when your program executes. And it deals with asynchronous events, such as attention key strikes. Similarly, kill was originally intended primarily to report the software "kill" signal. It was generalized almost from the start to report all signals, however.

The names of many signals came straight out of the PDP-11 hardware reference manual. SIGSEGV refers to a trap caused by the memory management hardware, which centers around sets of "segmentation registers" on the PDP-11. And SIGFPE refers to a floating point exception as reported by the optional PDP-11 floating point processor.

The committee did clean up some of the naming. The function kill eventually became raise, in the process of losing some of its peculiar UNIXisms. The signals got generalized and some of the most PDP-11 specific ones got dropped. Each implementation can now contrive a more or less sensible mapping from its hardware to the Standard C signals.

That's only the cosmetic layer, however. What's really difficult about signals is that they introduce the concept of multiple threads of execution within a single program. Nothing else in Standard C requires such semantics. True, the type qualifter volatile is a foot in the door, but it doesn't require more than the notion that "other agencies" may be at work between certain sequence points.

Leaving signals in Standard C exposes the language to two dirty truths. The first is that the semantics of signals weren't very good from the start. Under UNIX, they are a profoundly unsafe mechanism for synchronizing separate activities. Signals are too easily reordered or lost. Generalizing signals to all operating systems only makes them weaker. Next to nothing is promised about what you can count on if your program tries to handle signals.

The second dirty truth is that it's next to impossible to write a portable signal handler. The standard does include a type definition for sig_atomic_t. An integer data object of this type is supposedly read or written in one atomic operation. That should make such a data object a safe candidate for holding a flag that is set and tested by separate threads of execution. Presumably, you can write a signal handler that merely alters the value stored in such a safe data object and returns. I wouldn't want to put all those presumptions to the test in a serious program.

I wanted the signal handling functions omitted from Standard C from the outset. Too many other people felt we needed the machinery, bad as it was. We spent a lot of committee time trying to pin down semantics that were at once usable and good standards language. I think we failed.

<errno.h>

Another spongy area of the C library concerns how various functions report errors. In many cases, functions reports exceptional conditions "in channel." That is, a function signals the occurrence of something out of the ordinary by returning a strange value. The value is sufficiently strange that it is unlikely to be confused with a "normal" result. A null pointer is a good example of a strange value. The macro EOF is another, since it is guaranteed to be negative and hence easily distinguishable from a valid character code.

But not all errors are reported this way. Sometimes there are too many possible error codes to report all of them in channel. Sometimes there is strong historical precedent for reporting errors a different way. In either case, a traditional channel for reporting back errors was by writing an error code in a data object named errno.

As I recall, errno originated with the UNIX system interface. You make a system call by calling one of fifty-odd different functions in the C library. If the system call fails, the function returns a generic failure code as the value of the function. The actual error code returned by the system is tucked away in errno should you wish to learn more details about the nature of the failure. Now, that's not my idea of a perfect convention by any means, but at least it's consistent and easily understood.

The same machinery later got commandeered for additional purposes, however. When the stream I/O library was added atop the UNIX system calls, it essentially passed any system call failures through via the errno channel. To find the detailed cause of a stream operation failure, you had to first store a zero in errno then call the stream function. A failure return told you to inspect the value stored in errno. It should now be nonzero and more or less indicative of what went wrong. The more different system calls that arise out of a single stream operation, the less you can be sure exactly what the error code means.

Then other functions took to storing nonzero values in errno. That meant you could never be sure whether a particular code was stored by a system call or by some agency acting entirely within the library. It also opened up the set of potential codes you had to check for. This was more than just the underlying operating system speaking to you.

Probably the worst addition to this collection of agents was the set of math functions. It seemed only natural to report domain and range errors by adding EDOM and ERANGE codes to the existing errno machinery. But once faster floating point hardware came along, the machinery began to get in the way. Numeric coprocessors like the Intel 80X87 chips and the Motorola 68881 don't coexist well with errno. They want to handle exceptions with in-channel code values such as plus infinity or some kind of NAN (for "not a number"). Or they want to set some on-chip error indicators for later inspection.

To meet the reporting requirements implied by errno, compilers have to generate suboptimal code. You have to keep checking for errors in-channel or on-chip and mapping them into code values stored in errno. That makes numerical analysts shun C for FORTRAN, for example. Or it encourages the proliferation of nonstandard libraries and code generation options. It does not encourage the wider adoption of Standard C by programmers.

The committee did tidy things up a bit. They decreed that errno is an lvalue macro, not the name of a data object within the library. That gives implementations more latitude in how errno gets handled under the hood. An implementation can even call a function every time errno is accessed, to update its location or status at the last possible moment. The committee also better clarified just when errno can get values stored in it. And they clarified that no library function can ever store a zero in errno.

Those are small improvements. All the basic shortcomings of the machinery are still there. Please note that the committee wrestled with these issues over many meetings. A number of us have long been unhappy with leaving errno in the library. Sadly, we could never contrive an alternative that won the support of the committee.

<setjmp.h>

Another small area of leftover dirt involves the functions setjmp and longjmp. This pair provides C's answer to the nonlocal goto you can find in languages as diverse as PL/I and Pascal. You call setjmp to memorize a calling environment in a data object of type jmp_buf. A later call to longjmp specifying the same data object whangs the calling environment back to its earlier state. Suddenly, your program finds itself returning from setjmp much as it did on the original call. Any intervening calls to functions that have not yet returned are simply forgotten.

You need machinery like this sometimes. It lets you unwind from an arbitrary situation when a nasty error occurs. Thus, it's just what you need to build up exception handling machinery like you find in Ada. It's also what you need to translate those nonlocal gotos of PL/I and Pascal into C.

The only trouble is, the machinery is not well integrated into the language. By pushing the problem out to the library, C limits the ability of translators to do the job completely right. The major screw up occurs in the handling of data objects stored in registers. Many implementations can only whang all registers back to their state at the time of the original setjmp call. That can be fine for temporaries put in registers by the translator. It is less fine for data objects that the programmer knows something about.

You'd think that the standard could lay down a simple rule. Data objects declared with the register storage class keyword get reset when your program calls longjmp. All others remain unchanged. Only problem is, not all requests to put data objects in registers need be honored. And good translators know to promote some heavily used data objects into fast registers. There is no simple rule you can state that doesn't cause some sort of trouble.

One kind of trouble could cause translators to avoid many juicy optimizations, on the off chance that setjmp may get called. Another kind permits more optimizations, but practically requires that all translators know that setjmp and longjmp have magic properties. Even then, C could lose its long-standing ability to be translated in a single pass. There is no satisfactory solution.

So the compromise was to leave the setjmp machinery dirty. The standard contains additional and clearer warnings about the dangers present. You should call setjmp only from particularly simple expressions. You should declare auto data objects volatile if you want them not to get whanged. You should confine the machinery to fairly small function bodies to isolate the uncertainties. That's not a good answer from a linguistic standpoint. It's the one we got, however.

<stdarg.h>

The macros that let you walk varying length argument lists have similar shortcomings. Once again, the committee had to deal with a mechanism that probably belongs in the language proper. And once again, they opted to tidy up existing practice a bit rather than take on a complete fix.

In the earliest days of C, this was a nonproblem. Everyone knew how arguments were laid out on the stack on a PDP-11. With a bit of clever pointer manipulation, you could easily walk from argument to argument. It was only when diverse implementations proliferated that problems became apparent. You'd be surprised at the number of different ways C implementations use to pass arguments on a function call.

One of the first relatively clean fixes to the problem came out of Berkeley. They developed a set of macros, in the header <varargs.h>, that encapsulated the various ways for walking argument lists. Committee X3J11 used these as a starting point. Because we changed them along the way, we decided to introduce a new header name, <stdarg.h>, to hold the revised macros.

Nevertheless, we stuck with macros as a way of hiding the underlying machinery. That constrains the language in a number of ways. For example, some implementations must be told the name of the rightmost argument that is always required. That eliminates the possibility of defining a function with zero or more required arguments. A fairly small number of implementations need to execute the va_close macro before the function can safely return. That requires all portable programs to include va_close calls if they use these macros.

Perhaps the worst aspect of this machinery is that it barely works. There's all sorts of funny qualifiers in the standard to ensure that you use them only in ways that have been known to work. It's not good language design, which results in not good standards language.

Allocating Empty Objects

One place where we lost ground was in the memory allocation functions. It is a pet peeve of mine that malloc(0) once worked fine, but now is labeled as undefined behavior. We ended up in this sorry state as a compromise between two camps with conflicting views. It is a bad compromise, however, because everybody lost.

One camp believes that a zero-sized object is patent nonsense. You certainly cannot declare one statically, not even an array of zero elements. (Don't confuse this with a declaration such as char a[], which is an incomplete type that you later complete.) So a call such as malloc(0) is probably a programming error. The best thing to do is diagnose it, or at least return a null pointer to signal failure.

The other camp believes that arrays with zero elements occur dynamically all the time. Even if you can't declare one statically, you should be able to ask malloc to make you one. If you do, you want to get a non-null pointer back. That tells your program that the runtime has not run out of heap space, which is the normal meaning of a null pointer return from malloc. Of course, you can't access any storage using that pointer, but your program won't even try. It will process all zero elements of the array in a while loop and go on to other business.

Both camps have defensible arguments. I argued heatedly for the latter, however, for several reasons. It was certainly the status quo in both the UNIX and Whitesmiths C libraries. It lets you write more elegant programs. And it fits (my idea of) the spirit of C, by letting you do something that might be useful without complaint.

Whatever, neither side prevailed in the end. The committee simply got tired of hearing arguments on the subject. The final vote was to make malloc(0) undefined behavior. Now a programmer can't depend on any kind of useful behavior. Grumf.

Conclusion

I could also complain about the irregularities that remain in the library. Too often, the naming conventions are inconsistent. More often than I'd like, functionality of similar functions differs in surprising ways. I see that situation, however, as inevitable in the evolution of any nontrivial set of functions. It's really hard to fix without breaking a lot of existing code.

More serious is the prevalent practice of using static storage within the C library. Some functions promise to remember information between calls. A notorious example is strtok, which helps you parse a string into a sequence of tokens. Others return pointers to internal buffers that hold large data objects. The function localtime and its brothers are typical examples. In either case, the library is messier than it should be. It is harder to implement, particularly in a shared environment. It is harder to use, because the behavior of a function changes with the history of function calls.

Once again, however, existing code made it difficult for the committee to eliminate this practice. That's forgivable. We also added a few functions that commit similar sins. The multibyte function mbtowc is an example. That's less forgivable. I confess to being a party to some of these additions. That doesn't mean that I like the outcome, however.

Finally, there is the thorny topic of functions that didn't get added. Everyone has a pet list of functions that would really make the Standard C library much better. Were we to add them all, the library would be enormous.

Some would argue that it is already much too large. All names in the library are essentially reserved. You can write a function called asin and include it in any file of your program. If you do so, however, you must give it internal linkage by writing the static storage class keyword. And you had better not include the standard header <math.h>. Sure, you can break these rules safely on some implementations of Standard C. But you never know when you might want to move the code to another system. Its Standard C translator might very well cough.

So if you want to be meticulous about portability, you have a lot of names to keep track of. The library defines hundreds of names. You can see why adding another function to the language is not as "free" as you might think. It is not entirely true that what you don't use you can safely ignore.

Having said all that, I still wish we had added a couple of functions that didn't make it. One is a standard facility for parsing flags on a command line. Whitesmiths's C library called this function getflags. (Some other libraries have a roughly similar function.) Every utility we wrote used it. It went a long way toward standardizing how you write flags and how a program gobbles them up. It even provided a standard "help" mechanism to remind you what flags a program was ready to accept.

A brother to getflags was getfiles. It helped a program loop over a list of filename arguments on the command line, so the program could do its thing with each. It also enforced the common convention that the program should process standard input in the absence of any filename arguments. And it let you uniformly write the filename argument "-" whenever you wanted to process standard input as part of a list of files. Many utilities got simpler and more uniform by writing main in terms of getflags and getfiles.

Alas, we had to stop somewhere. The committee stopped well short of entertaining such functions as these. In general, I think we stopped none too soon. We included plenty of items from many different wish lists. So my final complaint is a weak one. And it's time for me to stop complaining, at least for awhile.

ANSI Update

Committee X3J11 held a two-day meeting 5-6 March '90 in New York City. This was the first meeting since the adoption by ANSI of the C standard. Thus, the principal business was to respond to the handful of requests for interpretation that have been sent to ANSI over the past year.

One item was deferred for further study, but the remainder were answered. In many cases, part of the answer was simply, "Sorry, you're suggesting a change in the standard, and that's no longer possible until the next revision." As much as possible, the committee cited chapter and verse within the standard to support their interpretation.

The committee also discussed about a dozen less formal queries. These were not registered with ANSI and hence did not require formal replies. (In fact, by ANSI rules a formal reply was not allowed.)

In the process of interpreting the standard, the committee unearthed several places where the wording did not exactly capture the remembered sentiment of earlier votes. None of these are major flaws, but they were surprises to some. A number of committee members expressed regret that there is no mechanism for making small changes to the standard.

Nevertheless, the document is remarkably clean. ANSI complimented the committee on the relative absence of blemishes. Requests for interpretation by their very nature home in on the places where wording is vague or weak. If the initial round of queries is any indication, the C standard has few such places.