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);
}

Hosted by www.Geocities.ws

1