[ Home | Library | Contents | Prev | Next ]


Bitwise Operator

by Matt Slot


This month I want to discuss probably the most important programming practice I've adopted: internal state validation and error propogation. If you've read the books "Code Complete" and "Writing Solid Code" from Microsoft Press (or work in the same code base as someone who has), you may already be familiar with these concepts -- for you, I'm going to evangelize the techniques and provide some useful snippets that will help you get started.

The primary concept is that every possible compile or run time error should be flagged as soon as possible in the development process. By adding extra sanity checks and wrapping library functions, you wil reduce the number of stupid mistakes in an implementation, identify run-time errors faster, and cut your overall debugging time.

Code Validation

Whenever you write code, you make numerous assumptions about the inputs and run-time environment. Of course, while you're implementing it, the code is crystal clear and you've added plenty of comments: around specific lines, for code blocks, above whole functions, and even in the source or API header file.

But any real project takes months to complete, may have several new programmers, and require several stages of evaluation and redesign. In such an environment, it's quite easy for NULL pointers to propogate down or error codes to get brushed aside over time. This is a frequent source of obscure or hard-to-reproduce errors ("well, it works on my machine").

Generous use of parameter validation and internal validation is a good way to reduce such "evolutionary" problems. For each function, check every parameter that is passed against NULL or valid range of values. Every time a global variable is accessed, validate that it was properly initialized. For functions that must be called in a specific order, test that the process is performed properly (typically tracking a state variable).

Error Propogation

Another way for problems to crop up are unexpected errors returned from library functions. Most developers quickly write up and unit test a batch of functions to "bootstrap" a specific feature, then go back and implement better error testing when everything works. In such a case, it's easy to disregard a reported error or fail to propogate it to the caller.

It's important to test the result of *every* function which can fail, even those that should simply never fail. For example, printf() can fail due to disk full errors (yes, it does happen). When I was first implementing such tests, I wrapped a call to the MacOS function Dequeue() (which returns an error if the specified element isn't found) to remove the first element. Obviously it should work as long as there is an element in the lsit, but in the end, I saved several hours of debugging time because the error check caught me passing the *address* of the element pointer.

Functions can be grouped into 4 categories. The first kind performs an action which may fail in the course of normal operation (allocating memory, writing to disk); such functions should always inform the caller if it fails. The second kind are functions which never fail (deallocating memory, zeroing a block of memory); these functions are typically declared void.

An abstraction layer is composed of functions which "wrap" another library, remapping parameters and error codes from one range (system-defined errors) into another (application-defined errors). Finally, event handler functions invoke several other functions (which may or may not fail), but have to handle the user's request from start to finish. This means that it anticipates any errors and displays an appropriate message ("couldn't save file, disk is full"), but doesn't pass it up because the problem has been handled.

Writing an Error Library

While it's great to write implement robust error checking and code validation in the source, it's a drag because of the impact on performance (not to mention spurious error messages). For this reason, it's wise to compile 2 entirely different versions of my application or library, one for debugging (larger and slower) and one for shipping (smaller and faster).

Now, writing 2 separate implementations is simply a waste of time, so most programmers compile the code base twice. Using the preprocessor to define DEBUG let's them distinguish between versions, so that each version is identical save the error code.

In the process of experimenting with rigorous error checking, I implemented several macros to aid testing and propogation. Because I'm a C programmer who likes a few C++-isms, I actually adopted terminology and design similar to the throw/catch metaphor.

Like the standard C library, my error library contains an Assert function for performing sanity checking of function parameters and internal state. Because such problems quickly appear during the implementation and unit testing process, Assert exists only in the DEBUG library and compiles out to an empty declaration in the non-DEBUG version. However, due to the importance of such problems, an Assert will force the application to quit immediately (and spur the programmer to fix it on the spot).

Next is the Throw declaration, which aborts execution of the current function by jumping to cleanup code at the end (using the nasty goto construct). This is useful for functions like saving documents, where a single failure in the process should simply cancel its execution. In the DEBUG version, throwing an error displays a complete error description before aborting, but unlike the Assert, non-DEBUG versions still perform the test and abortive cleanup (an error saving a file needs to be handled, even in shipping applications).

While some errors indicate critical problems with program execution, others can be considered "soft errors". For example, after exchanging some network data an application normally releases the network endpoint, however on some systems the function to do that returns an error code. By wrapping the call with a Trace declaration, the DEBUG version displays an error message without affecting program flow, but because the effect is generally benign the non-DEBUG application remains silent.

Finally, in an abstraction layer, the key is simply determining the error code and how that value should map into the application's own numbering scheme. For this reason, I added the Remap declaration which works the exactly the same as Throw except that it reports two error values in the DEBUG version -- the old (system) and the new (remapped) code.

Basically, then, we have several sets of functions which can be sprinkled through a source file, which will perform several types of validation and error propogation in DEBUG mode, but compile into simplified handlers for non-DEBUG binaries.

To make these 4 types of functions more useful (and more readable), I've implemented variations that flag NULL pointers, true conditions, or false (zero) conditions. Finally, a Catch declaration is placed at the end of the function, right before cleanup code, as the target for abortive Throw declarations.

Sample Implementation

I have placed some sample code online, consisting of:
http://www.AmbrosiaSW.com/~fprefect/bitwise/stddebug.h

The main set of declarations, consisting of macros that wrap a standard Debug() function. These macros record the affected line and file, error code (or codes), a brief error description, and the desired behavior (to assert, throw, or trace).

http://www.AmbrosiaSW.com/~fprefect/bitwise/stddebug.c
http://www.AmbrosiaSW.com/~fprefect/bitwise/macdebug.c

Each contains an implementation of Debug() appropriate to the platform. The standard file uses printf() to display the information to stderr, and the MacOS version uses DebugStr() to record a message in Macsbug. Feel free to write your own version of these functions, to record to file or display error dialogs -- but keep in mind that an application may call this function repeatedly while propogating an error up the calling stack, or even from software interrupt time.

There are several tricks used in this implementation. First, because it's convenient to simply wrap error-prone functions with the Throw or Trace macro, we have to be careful not to evaluate the conditional twice. For this reason, we declare a temporary error placeholder and use that through the rest of the declaration.

Given that we needed a temporary variable declaration, we take advantage of a clever C construct. It executes exactly once, lets us declare a variable within the braces, and even avoids the nested if-else problem.

	#define MyMacro(x)  do { long y; if (y=(x)) DoSomething(y); } while(0)
	
	if (SimpleFunction()) MyMacro(1);
	  else MyMacro(0);
Anyway, you get the basic idea. I hope this inspires you to implement some rigorous error checking, and to cut your debugging time dramatically. One thing that I'd really like to see is an implementation that propogates a complete error structure instead of a simple value, much like the actual C++ throw/catch implementation.

Matt Slot, Bitwise Operator


[ Home | Library | Contents | Prev | Next ]