Channels ▼
RSS

C/C++

The GEF General Exception-Handling Library

Source Code Accompanies This Article. Download It Now.


Nov98: The GEF General Exception-Handling Library

Bruce is a software developer for Xerox. He can be contacted at bbigby@alum .MIT.edu.


Sidebar:GEF and Programming by Contract

GEF is a general exception-handling and contract-programming facility for C programmers. GEF, with its special control macros, and other support functions, allows you to separate the main purpose of a function from its exception-handling, contract-validation, and resource-reclamation code. GEF even lets you isolate cleanup code to its own block -- just like Java.

In addition, GEF is also thread safe. GEF supports the 1003.1c specification of PosixThreads and UNIX signals. However, you must ensure that the PosixThreads API function (pthread_getspecific) is async-safe for your platform. If you are not using GEF to handle asynchronous operating-system signals, you need not be concerned with the issue of async safety.

I developed GEF on a Linux 2.x system, using the LinuxThreads PosixThreads-compatible library for thread support. Later, I ported GEF to a Sun Ultra1, running Solaris 2.5.1. With little effort, you should be able to port GEF to other C-development platforms as well. GEF source code and related files are available electronically from DDJ (see "Resource Center," page 3) or at http://home .rochester.rr.com/bigbyofrocny/. Also available electronically are sample programs that demonstrate the use of the GEF library.

GEF Control Macros

GEF provides seven control keyword macros: gef_try, gef_invariants, gef_preconditions, gef_catch, gef_postconditions, gef_finally, and gef_end. If you are familiar with Java or C++, you'll notice that these keywords are identical to those of the other languages, except GEF qualifies keywords with the prefix, "gef." This prevents code that uses GEF from conflicting with C++ exception handling. In fact, the names of all of GEF keywords and functions have either "gef" or "GEF" as a prefix. Control macros use the lowercase prefix, while initialization functions use uppercase.

The gef_try keyword macro introduces an exception-handling block, after which you can include the main body of your code. It is analogous to the C++ and Java try blocks. You can place any legal C statement after this block, and just like an if statement, if you are including more than one statement after the gef_try, you need to enclose the statements within braces.

GEF and Programming by Contract

GEF supports a number of control blocks that let you establish and enforce the requirements of using a function. A function may specify its contract of use via the exception-handler blocks, gef_invariant, gef_preconditions, and gef_postconditions.

GEF provides the gef_invariants block to let you specify the conditions of a function that must hold true immediately before the function begins to execute and after the function returns. In Listing One (at the end of this article), AddHighInt is a function that adds an integer i to the top end of a dynamic array. The lower and upper bounds of any dynamic array may be negative or positive integers. However, at any point in time, the upper bound of an array minus its lower bound plus one must be equal to its size. This condition is an invariant, because the condition must be true at all times.

The function also states, as a precondition, that before the function executes, the value of the array's data pointer must not be NULL. This precondition guards against the problem of trying to store a value into an unallocated memory space. As a postcondition, the new upper bound of the array must be equal to its previous value plus one.

Together, gef_invariants, gef_preconditions, and gef_postconditions specify the contract to which the function, and its clients, must adhere to ensure proper operation.

GEF Performance

Listing One simply illustrates how to use GEF's features. If you think that a routine, such as AddHighInt, will be called frequently, then you may consider implementing it in a more efficient but equally robust manner to avoid having to use the gef_try statement, which can be relatively expensive.

My measurements have revealed that the overhead of executing a gef_try/gef_end block with nothing in it except a counter, requires about 2.6 msec on a P5 133-MHz PC, running Linux 2.0.30. On a 143-MHz Ultra-1 running under Solaris 2.5.1, the cost is between 2.9 msec and 3.3 msec.

I obtained these timings by executing in a loop a gef_try/gef_end block that was relatively empty except for the existence of a counter that increments continuously for 1 second.

I computed the iterations per second, and, based on the clock speed of the machine, computed a rough estimate of the number of instructions. The overhead of entering and exiting a gef_try/gef_end block can be from 2.6 to 2.9 msec. This is not terrible, but if you are implementing a low-level routine whose clients will invoke it many times, you may want to consider explicitly checking for assertion violations via if-else statements and throw an exception when necessary.

GEF and Programming with Exceptions

GEF supports a number of control statements for dealing with exceptions: gef_throw, gef_retry, and gef_break. The names are consistent with Java and C++, with the exception of the gef_break control statement. Although the gef_retry and gef_break statements must appear within the scope of a gef_try/gef_end pair, the gef_throw statement may appear anywhere in your program.

You can use the gef_throw control statement to raise an exception and transfer control to the appropriate gef_catch block. GEF automatically raises an exception when an assertion violation occurs. If you do not specify a callback function for assertion violations, GEF throws its own predefined exception; otherwise, GEF invokes your handler function. In such a case, GEF reports the line number and a string that represents the path to the source file in which the assertion violation occurred.

GEF and Operating-System Signals

You can also register functions that GEF will call when operating-system signals occur. GEF will only handle those OS signals that you specify during initialization. Signals that GEF can handle include UNIX's SIGALRM, SIGFPE, and SIGQUIT, among others. You must be careful when using GEF to handle asynchronous signals (such as SIGALRM) in a multithreaded program.

However, you may use GEF to handle synchronous operating-system signals without concern. For example, you may use GEF to catch floating-point errors on UNIX systems. Simply register the SIGFPE signal with GEF, and, if the signal occurs, GEF catches and delivers the signal to the appropriate exception handler in your program.

Typically, you will want to correct a problem that occurs during the execution of a function and retry the main body of code. The gef_retry statement lets you unconditionally transfer control to the gef_try block.

On occasion, a thread may reach a success point while executing within a gef_catch block. In such a case, you may use the gef_break statement to transfer control to the statement that appears immediately after the nearest gef_end.

Unlike GEF, the Eiffel programming language does not support a break statement for exception handlers. Eiffel's designers preferred that all operations terminate via the try block. As a result, Eiffel programmers often have to resort to setting and testing a Boolean to determine when to actually retry a block of code or to exit. I found this restriction to be unnecessary, because what you end up implicitly implementing is a kind of break for an exception handler.

Listing Two illustrates how you might use exception handling instead of contracts. The SquareRoot function returns the square root of a number, provided that its value is not less than zero. If the value of the number is less than zero, then the SquareRoot function throws an EXCEPT_ARITHMETIC_VIOLATION exception. Actually, SquareRoot does not need to throw an exception if the number argument is less than 0.0. SquareRoot can rely upon the math library to call the global matherr function, which throws the proper exception if an arithmetic violation occurs.

The main function catches any exceptions and reports the error, if any, or the result to the console.

Unlike C++ and Java, if a thread reaches the end of a gef_catch block without handling an exception, GEF automatically rethrows the exception. In this respect, the gef_catch block is consistent with the rescue block of Eiffel. However, the difference between GEF's gef_catch and Eiffel's rescue is that Eiffel does not have the capability to report a specific error. In Eiffel, functions either succeed or fail.

The gef_finally block is simply a cleanup block. GEF always executes this block, regardless of whether an exception occurs. Listing Three, for example, illustrates the use of the gef_finally block. The function, PrintConcatenation, accepts two strings (s1 and s2) and a pointer to a FILE stream (fp). The function first acquires the memory (via malloc) into which it will store the concatenated string. It asserts that the resulting memory address not be NULL. The function continues by copying s1 to the result and then concatenating s2 to the result. The function subsequently prints a message to the specified stream (fp).

The last thing the function does is return the unneeded memory to heap storage in the gef_finally block. The code only frees the memory if the result is not NULL.

As a precondition, the function must ensure that result is NULL and that s1 and s2 are not NULL. Since the function relies upon a NULL test to determine when to free memory, the initial value of result must be NULL, indicating that it does not point to heap memory.

Every exception-handling block must end with a gef_end statement. The semicolon after the gef_end statement exists to ensure that syntax-aware editors (such as Emacs) will format your C code properly. However, it is not necessary.

Other Features of GEF

Table 1 lists the current GEF statements and functions. For example, you may specify a callback function that GEF will use to delete exception objects. You will likely take advantage of this feature if your code is object oriented in nature.

For example, you may create an exception object in the heap and throw it, and, as soon as a function handles the exception, GEF will call your callback routine to dispose of the exception.

GEF disposes of exception objects when executing the gef_retry and gef_break statements from within a gef_catch block. Currently, GEF's default callback for deleting exception objects is a function that does nothing and returns. Listing Two does not make use of this feature because it passes exceptions around as integers.

C's break and return Statements

You must be careful when you use the C language break and return statements. GEF maintains a parallel stack of checkpoints that enable it to perform nonlocal jumps when you throw an exception. If you exit the scope of an exception handler without passing through the gef_end code, GEF will become unsynchronized with your program, and your program will likely experience a most untidy fault. To avoid this problem, get in the habit of returning from a function at its end.

If you must use the C return statement, make sure that it is not within the scope of an exception handler. However, you need not worry about exception handlers of calling functions -- only the exception handlers in which you will be returning a value.

Likewise, also be careful when you use the C break statement. You may use the break statement, but make sure it doesn't transfer control of your program to a statement which resides outside of a gef_end statement. Substituting a C break statement for GEF's gef_break statement will frequently suffice. Make sure that what you are doing is safe and makes sense.

Conclusion

GEF is stable, mature, and flexible. I welcome suggestions on how to further increase its performance. Currently, however, the overhead of 2.6 msec is not bad for most applications. At the least, if you are writing C code that is not speed sensitive, you can utilize GEF to write more elegant and fault-tolerant C code.

DDJ

Listing One

typedef struct {   int lowbound;
   int highbound;
   int size;
   int **data;
   int numberOfSlots;
} *ArrayOfInt_t;
static
void
AddHighInt(ArrayOfInt_t self, int i) {
   int old_highbound = self->highbound;
   gef_try {
      self->data[size++] = i;
      self->highbound++;
   } gef_invariants {
      gef_assert(self->highbound - self->lowbound + 1 == self->size);
   } gef_preconditions {
      gef_assert(self->data != NULL);
      gef_assert(self->numberOfSlots > self->size);
   } gef_postconditions {
      gef_assert(self->highbound == old_highbound + 1);
   } gef_end;
}

Back to Article

Listing Two

#include <stdio.h>#include <stdlib.h>
#include <math.h>
#include <signal.h>
#include <GEF.h>


</p>
/* Define exception values for program */
#define EXCEPT_FAILURE              0 /* This should never occur! */
#define EXCEPT_OS_VIOLATION         1
#define EXCEPT_INSUFFICIENT_MEMORY  2
#define EXCEPT_ASSERTION_VIOLATION  3
#define EXCEPT_ARITHMETIC_VIOLATION 4


</p>
static
void
InsufficientMemory() {
   gef_throw((void*) EXCEPT_INSUFFICIENT_MEMORY);
}
static
void
AssertionViolation(char* fileName, int lineNumber) {
   /* Can only transfer fileName and lineNumber info via a pointer to a */
   /* structure.  For now, simply throw the integer value,              */
   gef_throw((void*) EXCEPT_ASSERTION_VIOLATION);
}
static
void
UnhandledException(void* exceptionID) {
   fprintf(stderr, "\tUnhandled exception, %d, occurred, while executing 
                      process, %d.\n",((int) exceptionID), (int) getpid());
}
static
void*
OSSignalToException(int signum) {
   switch(signum) {
   case SIGFPE:
      return((void*) EXCEPT_ARITHMETIC_VIOLATION);
   default:
      return((void*) EXCEPT_OS_VIOLATION);
   }
}
int
matherr(struct exception* e) {
   /* This handler will catch all IEEE math violations */
   gef_throw((void*) EXCEPT_ARITHMETIC_VIOLATION);
}
static
double
SquareRoot(double number) {
   /* Check for number < 0.0 for demonstration only. If math error occurs */
   /* math library will call matherr, which will throw exception anyway. */
   if (number < 0.0) gef_throw((void*) EXCEPT_ARITHMETIC_VIOLATION);
   return(sqrt(number));
}
int
main(int argc, char** argv) {
   double number, answer;
   int exitCode = 0;
   sigset_t osSignalSet;
   GEFAttr_t gefAttrs;
   if (argc != 2) {
      fprintf(stderr, "Usage: %s decimal\n", argv[0]);
      exit(1);
   }
   /* Configure GEF for process. */
   sigemptyset(&osSignalSet); sigaddset(&osSignalSet, SIGFPE);
   GEFInitialize(osSignalSet);
   /* Configure GEF for thread */
   GEFAttr_Init(&gefAttrs);
   GEFAttr_SetAssertionViolation(&gefAttrs, AssertionViolation);
   GEFAttr_SetUnhandledException(&gefAttrs, UnhandledException);
   GEFAttr_SetOutOfMemory(&gefAttrs, InsufficientMemory);
   GEFAttr_SetOSSignalToException(&gefAttrs, OSSignalToException);
   GEFInitializeThread(gefAttrs);
   gef_enable_assertions;
   gef_try {
      number = atof(argv[1]);
      answer = SquareRoot(number);
      printf("%f\n", answer);
   } gef_preconditions {
      gef_assert(argc == 2);
   } gef_catch(exception) {
     switch((int) exception) {
     case EXCEPT_ARITHMETIC_VIOLATION:
       fprintf(stderr,"%s: Cannot take square root of %f!\n",argv[0],number);
       exitCode = 1;
       gef_break;
     default: break;
     }
   } gef_end;
   GEFTerminate();
   exit(exitCode);
}

Back to Article

Listing Three

voidPrintConcatenation(char* s1, char* s2, FILE* fp) {
   char* result = NULL;
   gef_try {
      result = malloc(strlen(s1) + strlen(s2) + 1);
      gef_assert(result != NULL);
      strcpy(result, s1);
      strcat(result, s2);
      fprintf(fp, "Concatenation of %s and %s is %s!\n", s1, s2, result);
   } gef_preconditions {
      gef_assert(result == NULL);
      gef_assert(s1 != NULL);
      gef_assert(s2 != NULL);
   } gef_finally {
      if (result != NULL) free((void*) result);
      result = NULL;
   } gef_end;
}

Back to Article


Copyright © 1998, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video