Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Exception Handling in C Without C++

Tom Schotland and Peter Petersen

, November 01, 2000


Nov00: Exception Handling in C Without C++

Tom and Peter are software engineers at On Time Software. Tom can be contacted at [email protected], Peter at [email protected].


Register Variables and longjmp()


Error handling is an important issue in embedded systems, and it can account for a substantial portion of a project's code. We were faced with this issue during the design of RTFiles, the embedded filesystem component of On Time RTOS-32, our Win32-compatible RTOS for 32-bit x86 targets. The core filesystem is portable with a C function API to the application and a device-driver interface below it. Typically, errors can occur in device drivers and must be reported to the application with suitable return codes, so errors must travel through the complete core filesystem.

The classic C approach to this problem is return codes. Each function returns a value indicating success or failure. However, with a nontrivial function call hierarchy, this approach clutters the code significantly. Every function must check the return code of every function call it makes and take care of errors. In most cases, the function will merely pass any errors back up to its caller. RTFiles has several hundred internal functions and a call hierarchy up to about 15 levels deep, so this approach would have been a nightmare to maintain.

Programming languages such as Ada or C++ address this issue with exceptions. Exceptions make it easy to separate error handling from the rest of the code. Intermediate functions can completely ignore errors occurring in functions they call, if they can't handle them anyway. Exceptions are much easier to maintain than error return codes, so we definitely wanted to use them for RTFiles. Unfortunately, we had to write RTFiles in C, and not C++ or Ada, for portability. RTFiles must support compilers without C++ support.

Another issue is overhead and reliability. C++ exception handling needs a lot of run-time system-support routines, which might add too much code to a small embedded system. C++ exceptions are objects dynamically allocated from the heap, but many embedded systems do not want to use any dynamic memory allocation to avoid heap fragmentation and out-of-heap-space problems. For example, what would happen if an RTFiles device driver throws a disk-write-protection exception, and the heap allocation called by throw throws an out-of-memory exception?

The solution to the problem is to implement a simple exception-handling library in C with the following goals:

  • No dynamic memory allocation.

  • Robust (the exception-handling library itself must not fail).

  • Must support both exception-handlers and finally-handlers.

  • Reentrant for multitasking applications.

In this article, we describe how we designed and implemented our exception- handling library, demonstrate how it is used, and compare it to C++ exception handling. The complete source code is freely available electronically (see "Resource Center," page 5).

Handling Exceptions in C

The basic function of exception handling is to transfer control to an exception-handler when an error occurs, where the handler resides somewhere higher up in the current function call hierarchy. Standard C has a mechanism to accomplish this: setjmp() and longjmp(). Example 1 shows a simple implementation of error handling based on setjmp()/longjmp(). However, this example is a little too simple. It relies on a single global variable called "jumper," which contains the information where the exception-handler is. However, we need many different exception-handlers in different functions, so how will SomeFunction() know which jumper to use? Another issue is multitasking. Each task will have its own call hierarchy and thus needs its own jumper or set of jumpers.

What we really need is a dynamically linked list of exception-handler records. Such a record will contain a jmp_buf structure and supplemental information (for example, whether an exception been raised, has it been handled, what is its error code, and so on). Each function that defines an exception-handler adds such a record to the list and removes it from the list when it returns. The exception-handler records are allocated in the function's stack frame since we do not want to use the heap.

To make the whole thing reentrant, a separate list root is maintained per task. Multitasking operating systems will usually provide some mechanism to maintain per-task values. For example, Win32 has Task Local Storage (TLS), and RTKernel-32, the real-time kernel component of On Time RTOS-32, has both Win32 TLS and its own Task User Data. TLS allows an application to allocate a pointer value for every task (even for those that have not been created yet). At run time, this value can be set and retrieved by the current task. Our exception-handling library uses TLS to store the exception-handler list root pointers. If used in a single-task environment, the list root can simply be implemented as a single global variable.

Example 2 shows an improved version using exception-handler records on the stack. XLinkExceptionRecord() puts the given exception-handler record onto the current task's exception-handler list and initializes it. XUnLinkExceptionRecord() removes the exception-handler record from the list. XRaise() throws an exception, which is a negative error code in RTFiles. XRaise() retrieves the top-level exception-handler record of the current task and then calls longjmp() with the given error code. This transfers control to the correct handler.

To simplify the syntax, the exception- handler library's header file defines a few macros to encapsulate building and destroying an exception-handler block. Example 3 shows the same program using these macros.

Semantic Details

One big advantage of a home-grown exception-handling library is that we can define the semantics to best fit the needs of our application. For example, the most common case in RTFiles is that a function cannot handle an error, but some cleanup action is required. Typically, some global data structures of the filesystem are protected on entry to the filesystem using semaphores. We must ensure that such semaphores are released no matter how the function is left (through a normal return statement or through an exception). We thus reserved a special exception code (-1, defined as XFINALLY), which shall always be raised exactly once when an XTRY block is left. Such a finally-handler is not supported by C++ exception handling.

Another difference from C++ exceptions is that executing an exception-handler does not automatically complete handling of the exception. If the handler does not explicitly call function XHandled(), the exception-handling library will continue to pass the exception to handlers higher up in the list (called "exception propagation" or "stack unwinding"). We decided on these semantics because they reflect the typical case in RTFiles. However, in other applications, this could be handled differently, such as by using C++ semantics where an exception is considered handled once an exception-handler has been invoked.

The RTFiles API consists of C functions that all return integer values. Positive values indicate success and negative values indicate errors. For example, the function to open a file, RTFOpen(), will either return a valid, positive file handle or an error code such as RTF_FILE_ NOT_FOUND, which is defined as -9. To keep things simple, we use the standard RTFiles error codes as exception values. This lets us automate returning correct error codes to the application. Because all RTFiles API functions contain a top-level XTRY block, when leaving the block, we can simply return the current exception value. Basically, we propagate exceptions out of RTFiles by simply returning the exception value. This is implemented by the macro XEND, which returns the error code if it finds that the last exception-handler record has been removed and that a still unhandled exception is being propagated.

To implement these semantics, the exception-handling library must know the current state of processing, stored in the current top-level exception-handling record. Three states are distinguished:

  • XCode. The code body of the try block is being executed.
  • XHandling. An exception-handler is being executed.

  • XFinally. The finally block is being executed.

When an XTRY block is entered, the initial state is set to XCode by XLinkExceptionRecord(). If the code section completes without raising any exceptions, XUnLinkExceptionRecord() is called next, which will then set the state to XFinally and execute the finally-handler. If, however, an exception is raised, XRaise() sets the state to XHandling and calls the appropriate handler. If the handler does not call XHandled() to indicate that the exception is now handled, XUnLinkExceptionRecord() executes the finally-handler and moves on to the next handler in the handler chain. This means that any code following the XTRY block will never get executed, and the current execution frame is abandoned.

Of course, this process has to stop somehow. If an exception-handler can handle the error, it will call XHandled(), and normal execution will continue after the current XTRY block. If the outermost exception-handler has not handled the exception, we just pass the exception code (which is an RTFiles error code) back to the application. However, most applications will probably prefer to abort the program with a fatal error (just like C++, for example).

As long as the OS-dependent functions for TLS data are defined (in our example, we just use a single global variable), the exception library can be compiled with any ANSI C compiler. However, our goal to become independent of the C++ run-time systems has not been reached yet. Looking at the source code of typical implementations of longjmp() reveals that longjmp() references a lot of the C++ exception-handling support routines. This makes sense because C++ must ensure that all objects with a local scope are destroyed when the respective scope is left. As longjmp() can leave a local scope, it must also call the destructors of all objects going out of scope. Because we only use C in RTFiles, this functionality is not required, and we do not want longjmp() to pull in so much code we would never need. Thus, we implemented our own versions of setjmp() and longjmp(); see Listing One. The two functions, XSaveContext() and XRestoreContext(), only have to save/ restore the stack frame registers, the return address, and any registers the compiler might use for register variables. By defining symbol XWIN32, our alternate functions are used by the exception-handling library instead of setjmp()/longjmp().

Using the C Exception-Handling Library

XTRY blocks can be nested to any depth, either within a single function or across function calls. The XTRY block has a code body, any number of exception-handlers to handle specific exceptions, a single default handler to catch all other exceptions, and a finally-handler. Example 4 shows the basic structure of an XTRY block.

The XTRY block is closed with macro XENDX or XEND. XENDX is used in functions that cannot return the error code as a return value. This is frequently the case for functions internal to RTFiles, declared as static void. If XENDX finds no outer exception-handler, it reports a fatal error. Basically, this means that a function containing an XENDX block may only be called while an XTRY block already resides on the call stack. This is the case in RTFiles, since all API functions (the only entry points to RTFiles) have XTRY blocks.

Within an XTRY block, a few exception-management functions are available. XRaise(int XValue) raises an exception with the given error code. XValue must be a value less than or equal to -2, because positive values are not considered errors, and -1 is reserved for the finally-handlers. XHandled() stops propagating an exception. It may only be called from an exception-handler. XReExecute() can be called by an exception-handler to execute the code body of the current XTRY block again. For example, this can be used for retries. Finally, macro XVALUE returns the value of the exception currently being processed.

There are also a few restrictions that must be observed. Due to the implementation of setjmp() and longjmp(), all local variables used within more than one part of an XTRY block (code body, exception-handler, finally-handler, or outside the XTRY block) must be declared as volatile. This is required by the C ANSI Standard, which is explained in more detail in "Register Variables and longjmp()." Additionally, this C exception-handling library should not be mixed with C++ code within the same source file, because it could cause destructors not to be called.

How Does It Compare To C++ Exceptions?

Although C++ was never an option for RTFiles, we do want to check that our goals have been met. In particular, our C exception-handling library should have little run-time overhead, and it should need less code space than C++ exceptions. Listings Two and Three show a C and a C++ version of a benchmark program, respectively. One thing difficult to compare is the finally-handler, which is not supported by C++. In the benchmark, the finally-handler should merely increment an integer. In the C++ version, this statement has been placed in the destructor of a local class object. C++ guarantees that such a destructor is called when its object goes out of scope, regardless of the method to leave the scope.

Table 1 lists the execution times and code sizes of both programs. Execution times are given for the case where no exceptions are thrown and for one throw per iteration. The C program has been linked with a stripped-down run-time system with C++ exception-handling support removed while the C++ version is linked with an unmodified run-time system. The tests were performed with Borland C++ Builder 4.0 for Win32 under Windows NT. While there is only a small difference in execution times when no exceptions occur, the C exception library easily outperforms C++ in all other disciplines.

Conclusion

This simple exception-handling library has been a great help in implementing RTFiles. The mechanism is easy to use, portable, uses no dynamic memory allocation, and is efficient. When used correctly (that is, when you do not call XRaise() while no XTRY block is present on the call stack), it cannot fail. In particular for embedded systems, where low resources preclude the use of C++, using the C exception-handling library can radically reduce the complexity of error handling.

DDJ

Listing One

; RTFEX32.ASM
; Copyright (c) 1998,99 On Time
; http://www.on-time.com
; Custom context save/restore functions for C Exception Handling Library 
; This is what we want to implement:
;   typedef struct {
;      unsigned long esi, edi, ebx, ret, ebp, esp;
;   }  XContext;
;   int  __stdcall XSaveContext(XContext * C);
;   void __stdcall XRestoreContext(XContext * C, int Value); 

 .386

XContext STRUC
  _esi DD ?
  _edi DD ?
  _ebx DD ?
  _ret DD ?
  _ebp DD ?
  _esp DD ?
XContext ENDS

_TEXT SEGMENT DWORD USE32 PUBLIC 'CODE'

ASSUME CS:_TEXT

PUBLIC XSaveContext
PUBLIC _XSaveContext@4
PUBLIC XRestoreContext
PUBLIC _XRestoreContext@8

XSaveContext proc near
_XSaveContext@4 label near
   pop ecx                 ; ret address
   pop edx                 ; parameter C
   mov [edx+_esi], esi     ; save all required registers
   mov [edx+_edi], edi
   mov [edx+_ebx], ebx
   mov [edx+_ret], ecx
   mov [edx+_ebp], ebp
   mov [edx+_esp], esp
   xor eax, eax            ; return code is zero
   jmp ecx                 ; and return
XSaveContext endp

XRestoreContext proc near
_XRestoreContext@8 label near
   mov edx, [esp+4]        ; parameter C
   mov eax, [esp+8]        ; parameter Value, set as return value
   mov esi, [edx+_esi]     ; restore all required registers
   mov edi, [edx+_edi]
   mov ebx, [edx+_ebx]
   mov ebp, [edx+_ebp]
   mov esp, [edx+_esp]
   jmp [edx+_ret]          ; and jump to saved context
XRestoreContext endp

_TEXT ENDS

END

Back to Article

Listing Two

// XBENCH.C
// Copyright (c) 1998,99 On Time
// http://www.on-time.com
// Benchmark for the C Exception Handling Library

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

#include <rtfex.h>

#define DIVIDE_BY_ZERO -3 // an error code

// some global variables to count iterations, exceptions, and cleanups
int Calls, Exceptions, Finallys;

///////////////////////////////////////
int SomeFunction(int a, int b)
// functiom which can raise an exception
{
   if (b == 0)
      XRaise(DIVIDE_BY_ZERO);
   return a / b;
} 
///////////////////////////////////////
int TestFunction(int a, int b)
// test function containing an XTRY block
{
   int volatile Result;
   Calls++;
   XTRY
      case XCODE:
         Result = SomeFunction(a, b);
         break;
      case DIVIDE_BY_ZERO:
         Exceptions++;
         XHandled();
         break;
      default:
         printf("unknown exception!");
         break;
      case XFINALLY:
         Finallys++;
         break;
   XENDX
   return Result;
} 
///////////////////////////////////////
void Bench(int a, int b, int Iterations)
// benchmark function to call TestFunction in a loop
// and print timing and statistics 
{
   DWORD T0, T1;
   int i;
   Calls = Exceptions = Finallys = 0;
   T0 = GetTickCount();
   for (i=0; i<Iterations; i++)
      TestFunction(a, b);
   T1 = GetTickCount();
   printf("%10i  %10i  %10i    %10u", Calls, Exceptions, Finallys, T1-T0);
} 
///////////////////////////////////////
int main(void)
{
   printf("Interation  Exceptions    Finallys  Milliseconds"
          "------------------------------------------------");
   Bench(1, 1, 1000000);   // no exceptions
   Bench(1, 0, 1000000);   // raise one exception per loop
   return 0;
} 

Back to Article

Listing Three

// CPPBENCH.CPP
// Copyright (c) 1998,99 On Time
// http://www.on-time.com
// Benchmark for C++ Exception Handling

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>

#define DIVIDE_BY_ZERO -3 // an error code

// some global variables to count iterations, exceptions, and cleanups

int Calls, Exceptions, Finallys;

///////////////////////////////////////
int SomeFunction(int a, int b)
// functiom which can raise an exception
{
   if (b == 0)
      throw DIVIDE_BY_ZERO;
   return a / b;
} 
///////////////////////////////////////
int TestFunction(int a, int b)
// test function containing try block
{
   int Result;
   class Finally {
   public:
      Finally()  { Calls++; } 
      ~Finally() { Finallys++; } 
   }  FinallyHandler;

   try
   {
      Result = SomeFunction(a, b);
   } 
   catch (int& ErrorCode)
   {
      switch (ErrorCode)
      {
         case DIVIDE_BY_ZERO:
            Exceptions++;
            break;
         default:
            printf("unknown exception!");
            throw;
       } 
   } 
   catch (...)
   {
      printf("non integer exception!");
      throw;
   } 
   return Result;
} 
///////////////////////////////////////
void Bench(int a, int b, int Iterations)
// benchmark function to call TestFunction in a loop
// and print timing and statistics 
{
   DWORD T0, T1;
   int i;

   Calls = Exceptions = Finallys = 0;
   T0 = GetTickCount();
   for (i=0; i<Iterations; i++)
      TestFunction(a, b);
   T1 = GetTickCount();
   printf("%10i  %10i  %10i    %10u", Calls, Exceptions, Finallys, T1-T0);
} 
///////////////////////////////////////
int main(void)
{
   printf("Interation  Exceptions    Finallys  Milliseconds"
          "------------------------------------------------");
   Bench(1, 1, 1000000);   // no exceptions
   Bench(1, 0, 1000000);   // raise one exception per loop
   return 0;
} 



Back to Article


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.