EXCEPTION HANDLING WITH SETJMP/LONGJMP Adrian Perez de Castro, 2oo3 Lixoo team, 2oo3 -- all your base are belong to us! ============================================================================= It is possible to tweak out with the standard ANSI C functions setjmp() and longjmp() in order to provide an exception handling mechanism in C programs like other languages do (C++, Java). This document discusses possible ways of implementing exception handling using these functions, some focus is put in thread awareness, too. A. Exceptions. An exception is an unpredicted success that occurs when running a program's code, that may be handled properly with an ``exception handler''. Usually, code that will be monitored to check wether exceptions occur is enclosed in a special block, called the ``try'' block. In C++: ... try { /* some spurious code here */ if (error) throw 1; } catch (int i) { /* handle the exception here */ } ... The execution of this code is: - The code enclosed in the ``try'' block is executed. - If no exception was thrown, execution continues normally after the ``catch'' block. - If an exception is thrown (using the ``throw'' keyword), the control of the program goes to the start of the ``catch'' block, and when that block ends, execution continues after the exception was handled. Issues: - Multiple ``catch'' blocks may appear with a ``try'' block, each one handling a type of exceptions. - ``try'' blocks may be nested, initially with no restriction in depth. - An exception handler may rethrow an exception, so it may be handled within an upper-level ``catch''. - If an exception does not get handled, an user-configurable default exception handler must provide actions for uncaught exceptions. B. Discussion. Exceptions are a powerful system of error control and recovery, but the way they work require some support from the compiler --like catching exceptions of different types with a number of ``catch'' blocks--. Full implementation of exceptions conforming to our description is not possible using just an ANSI C compiler. Jumping onwards and backwards on the code --and even from between different functions-- is provided by the standard setjmp() and longjmp() functions. Jumping within the same piece of code is possible using a ``goto'' sentence but it's not recomended as it breaks the essence of structured programming. Using ``goto'' may be considered if details of its use are hidden to final developers. Nesting of multiple ``try ... catch'' sets may be emulated with some kind of ``exception context stack''. Exceptions get unhandled when the exception context stack is empty, so they're passed to the default handler. If the stack is static --i.e: has a fixed amount of maximum entries-- depth level of ``try'' blocks is restricted, so the stack must grow as needed. Catching exceptions by type is nearly impossible without compiler support. At least it's not possible using only ANSI features of C compilers. C. Exception catching with setjmp() and longjmp(). Let's consider the following piece of code: ... { jmp_buf buf; if (!setjmp(buf)) { /* code being monitored */ } else { /* code to handle exceptions */ } } ... This code saves the execution status in ``buf'', with setjmp(), and then the code in the ``if'' block starts its execution. If longjmp() is NOT called in that block, execution ``falls off'' the bottom, and status buffer ``buf'' is destroyed automatically. Now, think about some code from within the ``if'' block that calls longjmp() passing as second argument a non-zero value: execution is restored just when the value returned by setjmp() is evaluated; the nonzero value passed to the longjmp() function causes the condition to be FALSE, so the ``else'' block gets executed. C. Passing the ``exception object'' to the handler and the ``jmp_buf'' to other execution contexts. This is something like exceptions work. Now we'll need some programming trick in order to pass the ``exception object'' to the exception handler. This can be done by using our ``exception status stack''. Every entry in there stores: - Execution context status (the ``jmp_buf'' struct). - The exception being handled by that execution context status. The exception handler pops the last element from the stack to get the object that describes the exception. The code throwing an exception gets the top of the stack to get the ``jmp_buf'' that will be passed to longjmp(). Now, providing a global exception stack and some functions to manipulate it we're almost done, and only a way of ``throwing'' exception objects is needed in order to have real exception handling in our C code. D. Throwing exceptions. Throwing an exception involves calling longjmp() with the saved ``jmp_buf'' --that can be read from the top of the exception stack-- and a nonzero value so the handler is triggered. Throwing an exception should also modify the element at the top of the stack so the appropiate exception object is passed along to the exception handler. Rethrowing an exception is possible because of the existence of an exception status stack: nested values for contexts (variables of type ``jmp_buf'') and exceptions are pushed and popped in the stack following a LIFO policy. E. Pseudocode of exception handling and throwing. We'll suppose a stack containing pointers to ``except_stack_item'' structs, with the usual push/pop and top operations, as well as an isEmpty() function that checks if the stack is empty. We'll define the struct as: typedef struct except_stack_item { jmp_buf exec_status; Exception_Type *exception; } except_stack_item; Then we define the macros ``try'', ``catch'' and ``end'': #define try \ { \ except_stack_item _seItem; \ except_stack_push(&_seItem); \ if (!setjmp(_seItem.exec_status)) { #define catch \ except_stack_pop(); \ } \ else { \ except_stack_item *_se = except_stack_pop(); \ Exception_Type *exception = _se->exception; #define end \ } \ } And now we can use our ``try ... catch'' block as follows: try /* spurious code */ catch /* handling code */ end To throw an exception, we can either define a macro or a function. We'll try to simplify things by using a function. A default handler to manege uncaught exception is also provided: void UncaughtExceptionHandler(Exception_Type *e) { fprintf(stderr, "Uncaught exception - execution aborted!\n"); fflush(stderr); exit(EXIT_FAILURE); /* you might want to use abort() as well */ } void throw(Exception_Type *e) { except_stack_item *se = except_stack_top(); se->exception = e; if (except_stack_isEmpty()) { UncaughtExceptionHandler(e); } else { longjmp(se->exec_status, 1); } } Now we may try our code with the following example: ... try throw(NULL); catch if (exception == NULL) printf("The NULL exception was catched!\n"); end ... If we run the following code, the default handler for uncaught exceptions will act: #include int main(void) { init_exception_system(); /* we suppose this function exists and must be called before start using exceptions */ throw(NULL); return 0; } F. Making exceptions thread-aware Using a global exception stack makes this implementation fail when threads throw exceptions simultaneously. The status of the exception stack might be corrupted if two or more threads try to modify it at the same time. We could protect acces to the global exception stack by means of a mutex, but imagine that the following steps occur: 1. Thread `A' enters a ``try'' block: the status is pushed on the stack. 2. Thread `B' enters a ``try'' block: the status is pushed on the stack. 3. Thread `A' throws an exception: the last pushed status is popped, the status from thread `B', not `A'!! So thread `A' is catching an exception raised from thread `B'... that sounds harmful. So we need some tricks to make a thread receive only exceptions that are thrown by that one. The obvious way of arranging things is creating a per thread exception stack, and the following changes to the implementation are needed: o Functions that manipulate the stack need to be fully reentrant. This should not be a problem, as they are quite simple. o The function used to throw exceptions must be thread-safe, too. o The setjmp() and longjmp() functions must be thread-safe, or atomic. If they aren't, wrappers using a global mutex might be used. Also, these functions must save and restore a minimal portion of the exe- cution context; some systems provide a _setjmp() and _longjmp() that save less context information and may be used. o If the threading system supports thread-specific storage, it can be used to safely store a pointer to the per-thread stack; if not, we need a global structure holding all the stacks, and select the stack based upon the thread's ID (it might need to be protected by a mutex too). Whay happens when there are `any' threads? Nearly all implementations treat non threaded applications as if they had only one thread, so even with uni- threaded applications the same trick can be used: the specific storage of the `main' will be used for the first created stack.