Exception Handling in C
setjmp and longjmp can be used to support exception handling in C. This
is an attempt to implement java style exception handling in C using these functions. The interface
should have try-catch-finally blocks.
The basic form of exception handing is achieved by a set of setjmp and
longjmp calls. Consider this example:
int func3(int x) {
longjmp(ctx, -50);
}
int func2(int x) {
func3(x);
}
int func1(int x) {
int err;
if(!(err = setjmp(ctx))) }
func2(x);
}
else {
printf("Exception %d caught\n", err);
}
}
Many functions can have a try-catch blocks and the longjmp needs to
jump to the closest function that has a try-catch block. This can be
achieved by stacking up the jmp_buf structures of each such registering
function (a function having a try-catch block). The jmp_buf along with
other required information can be created as a local variable (note
that we need this only within the scope of the function) and all such
structures are linked as a stack. This ensures that the exception data
of the last function is available on the top of the stack.
typedef struct ExCtx {
struct ExCtx *next;
jmp_buf buf;
}
static ExCtx *stack;
#define EXC_BEGIN \
{ \
ExCtx ctx; \
ctx.next = stack; \ /* Push */
stack = &ctx; \
switch(setjmp(ctx.buf))
#define EXC_END \
stack = ctx.next; \ /* Pop */
}
#define THROW_EXCEPTION(exception) \
longjmp(stack->ctx, exception)
We push the exception data on to the stack at the beginning of the
exception handling and pop it at the end of the handling. To avoid
writing this code every time, we use macros to achieve this.
We have not differentiated between different types of exceptions till
now. Since we cannot determine what exceptions the immediate exception
handler can handle, we cannot jump to the exact handler which can
handle that exception. For example, in the above example, if func2 has
a handler for exception e1 and func1 has a handler for e2 and func3
throws exception e2, we dont have a mechanism to jump to func1.
The entry in the stack indicates that there is a try-catch block in
that function which can handle some exceptions. If the function doesn't
have the catch block for the thrown exception, it needs to be passed
upward so that the function having the correct handler can catch it.
Supporting the 'finally' semantics complicates the logic further. Since
the exception handling can bypass intermediate functions and this can
lead to problems if those functions need to perform some cleanup
activities. The finally block ensures that the code gets executed
'finally'. So, even if there is no exception handler in a
function, the finally block has to be executed before passing the
exception to the next handler. Note that this 'finally' code is
optional.
We need to maintain some state information so that we can support all
the above cases. We use a special exception number to handle
the 'finally' code. Every catch block will have code to throw this
exception. Since we need to throw the original exception 'upward' to
the next function after handling the finally, we need to store the
original exception and restore it.
When a function doesnt have the exact exception handler but has
a finally block, it will have to throw the exception to handle
the finally block and form the finally block, we need to throw the
original exception upward so that the correct handler can catch it.
This is the final code. Please send your comments/improvements to
uvsravikiran (at) gmail (dot) com.
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* Exception handlers are stored as a stack. To throw an
* exception, call the handler (longjmp to the context) with
* the exception. The function may or may not have a catch
* block for the thrown exception. In that case, the exception
* has to be passed to the upper exception handlers.
* If there is an exception handler, the execution should
proceed
* normally after the handling.
* From the point where the exception is thrown and to the
point
* where there is a catch block, the 'finally' block should be
* called before leaving the function.
* This is achieved by using a special exception -1 which will
be
* thrown by every block of the exception handler before
leaving.
* This complicates the code.
*
* The 'begin' part creates a structure and stores the context
in
* it. It is added to the stack of such structures (many nested
* functions may do this).
* When the function leaves the code, it removes itself from
the
* stack.
*/
#define EXCEPTION_FINALLY -1
typedef struct ExCtx {
struct ExCtx *next;
jmp_buf jmp;
char funcname[128]; /* for debugging only */
int handled;
} ExCtx;
static ExCtx *stack = NULL;
static int exception = 0;
static int saved_exception; // saved before raising finally
#define EXCEPTION exception
#define EXC_BEGIN \
{ \
ExCtx ctx = {}; \
strcpy(ctx.funcname, __FUNCTION__); \
ctx.next = stack; \
stack = &ctx; \
switch(setjmp(ctx.jmp)) { \
case 0:
#define PRINT_EXINFO \
printf("funcname = %s, handled = %d\n", stack->funcname,
stack->handled);
#define THROW_EXCEPTION(ex) \
do { \
exception = ex; \
longjmp(stack->jmp, ex); \
} while(0)
/* This is called from the 'default' case of the exception
* handler. This is because, the thrown exception jumps to
* the nearest function that registered an exception handler.
* But it may not have the exception handler for the thrown
* exception. In this case, the exception has to be passed
* to the next exception handler.
*/
#define PASS_EXCEPTION \
if(stack = ctx.next) \
longjmp(stack->jmp, exception);
#define EXC_FINALLY \
ctx.handled = 1; \
saved_exception = exception; \
THROW_EXCEPTION(EXCEPTION_FINALLY); \
break; \
case EXCEPTION_FINALLY: \
#define EXC_CATCH(ex) \
ctx.handled = 1; \
saved_exception = ex; \
THROW_EXCEPTION(EXCEPTION_FINALLY); \
break; \
case ex: \
#define EXC_END \
if(exception != EXCEPTION_FINALLY) { \
ctx.handled = 1; \
saved_exception = exception; \
THROW_EXCEPTION(EXCEPTION_FINALLY); \
} \
if(exception == EXCEPTION_FINALLY &&
!ctx.handled) {\
exception = saved_exception; \
PASS_EXCEPTION; \
} \
break; \
default: \
if(exception !=
EXCEPTION_FINALLY) { \
saved_exception = exception; \
THROW_EXCEPTION(EXCEPTION_FINALLY); \
} \
else { \
if(!ctx.handled) { \
exception = saved_exception; \
PASS_EXCEPTION; \
} \
} \
} \
stack = ctx.next; \
}
int func3(int x, int err) {
int y = x + x;
if(x < 0)
THROW_EXCEPTION(err);
return y;
}
int func2(int x, int err) {
int y;
EXC_BEGIN {
func3(x, err);
}
EXC_CATCH(-50) {
printf("Exception %d caught\n",
EXCEPTION);
}
EXC_FINALLY {
printf("%s finally\n", __FUNCTION__);
}
EXC_END;
return y;
}
int func1(int x, int err) {
int y;
EXC_BEGIN {
y = func2(x, err);
printf("func1 as expected\n");
}
EXC_CATCH(-100) {
printf("Exception %d caught\n",
EXCEPTION);
}
EXC_FINALLY {
printf("%s finally\n", __FUNCTION__);
}
EXC_END;
return y;
}
int func0(int x, int err) {
return func1(x, err);
}
int test(int x, int err) {
EXC_BEGIN {
func0(x, err);
}
EXC_CATCH(-100) {
printf("Exception %d caught\n",
EXCEPTION);
}
EXC_FINALLY {
printf("%s finally\n", __FUNCTION__);
}
EXC_END;
printf("\n");
}
int main() {
test(0, -100);
test(-1, -50);
test(-1, -100);
}