C Language Exception Handling

Error handling

It is common for a program to manage non standard situation, like things does not work as expected, or time is expired on operation, or everything else.

Typically C (as any old language) do manage such situation with error code returned, possible errno setted and a table of string that explain errno (sys_errlist). Thus a typical error management in C require to test any error on functions that may returns error (almost all standard libc functions) and, if error occur, manage it some way.

Contrarily in C++ (and all new object oriented language) there is the exception idiom. This require runtime support that have a cost, and that make, for example, Symbian OS (aka EPOC32) developer decide to drop this support in the name of performance and memory allocation fault tolerance (see TRAPD/Leave mechanism).

Object oriented idiom is not strictly related to object oriented language, generally it is a way to structure your code using the concepts of object model (encapsulation, inheritance, polymorphism). A good example of object oriented library wroten in plain C language is the GIMP Tool Kit. I tend to consider exception handling a part of object idiom, and this part is what is missing in most object oriented libraries.

setjmp() and longjmp() functions

This article will describe what should/could be done in a exceptional _C_ase.
C language miss exception handling support and runtime, does not exists things like C++’s try .. catch, does not exist exception class definition and hierarchy, but there are nice functions like setjmp() and longjmp() that behave someway as try catch …

#include <setjmp.h>
#include <stdio.h>
int 
foo(int p)
{
  if (p) siglongjmp(env,p); /* return to sigstejmp returning p */
  return 0;
}

static sigjmp_buf env;
int
main() {
  int a,r;
  if (!(r=sigsetjmp(env, 1))) {
    for (a=0; a<10; a++) {
      fprintf(stdout,"%d\n",foo(a));
      fflush(stdout);
    }
  } else {
    fprintf(stdout,"exceptionally returned %d",r);
    fflush(stdout);
  }
}

sigsetjmp and siglongjmp are variant conforming to posix and compatible with bsd standard (see GNU Libc documentation)

… yes, it look like try{..}catch(..) in C++ except for the missing catch argument, and that there is only one level of exception

Another example of longjmp

#include <stdio.h>
#include <setjmp.h>

static sigjmp_buf env;
static sigjmp_buf env1;

int
bar(int a)
{
  fprintf(stdout, "bar a:%d\n", a);
  if (a <10) siglongjmp(env1,a);
}

int
foo(int p)
{
  int r;
  if (! (r=sigsetjmp(env1,1))) {
      fprintf(stdout, "foo = %d\n",bar(p+1));
      fflush(stdout);
  } else {
    fprintf(stdout,"some exception occurred %d\n", r);
    fflush(stdout);
    siglongjmp(env,r); /* try with this or without */
  }
  if (f(p)) {
    fprintf(stdout, "well, done");
    fflush(stdout);
  }
  return p;
}

int
f(int p)
{
  if (p>=8) siglongjmp(env,p);  
  if (!(p%2)) return 1;
  return 0;
}
int
main()
{
  int a;
  int r;
  if (! (r=sigsetjmp(env,1))) {
    for (a=0; a<10; a++) {
      fprintf(stdout, "foo = %d\n",foo(a));
      fflush(stdout);
    }
  } else {
    fprintf(stdout,"some exception occurred %d\n", r);
    fflush(stdout);
  }
}

Here f() “raise an exception” that is managed by main, it does that longjmp’ing directly to main setjmp. This is not part of standard exception idiom and in fact make the code unreadable, and this is because use of setjmp/longjmp is almost absent from good code. But position where f() is called make clean that there is a need of different idiom, just because the compiler do not stop if a throwing function is called out of try{..}catch block and here longjmp’ing is not a by pass of a try{..}catch block, contrarily it is just go to the containing try{..}catch block. In other word this other idioma just take all functions as throwing funcs.

Instead of 2 (or more) variables it is better to use a stack of exception managed by utility functions transparently.

An exception stack implementation

For implementation of the exception idiom are needed: a stack of environment and a exception structure. In order to better figure how the things should works the code is better than any graphics:

#include <setjmp.h>
..
int
test_excp()
{
  jmp_buf *my_env;
  exc_struct *my_excp;
  int r;

  my_excp=new_excp();
  my_env = new_env();
  if (!(r=setjmp(my_env))) { /* try */
    a_throwing_func();
    /* other code here
     * ..
     * if at the end all go right free stack and continue */
    free_env(my_env);
  } else { /* catch */
    switch (r) { /* given that type of exception be passed this way */
    case 1: /* 1 is trapped */
      excp_stru *l_excp;
      /* I want l_excp point to the returned exception
       * and also all allocated jmp_buf cleared until 
       * the current one my_env */
      l_excp = compact_excp(my_env);

      /* some management code */
      manage(l_excp);

      break;
    default: /* throw anything else */
      /* just does longjmp to parent level
       * (the parent level do compact exceptions and env) */
      throw_up(my_env);
    }
  }
  free_env(my_env);
  /* in case of uncatched just throw (with parent env) */
  throw_excp(gen_excp, "error");
}

Well, the code does not look so bad. But there is a consideration to take into account: at any given time there is no need of having more than one exception structure filled. This rule in hold even in C++ and Java and any language which have runtime exception: an exception stack is maintained just for tracking purpose, and only if exceptions occurs. I just ignore exception stack and I use a global variable “exception” initialised at start time.

Given the above code, the needed function are:

  • new_env() return a new *jmp_buf
  • free_env(jmp_buf *env) free env_stack till env (included)
  • compact_excp(jmp_buf *) free env_stack till env (excluded)
  • throw_up(my_env) longjmp to the jmp_buf below my_env in env_stack and update env_stack size
  • throw_excp( void(*excp_fill) (struct excp_struct *, va_list ap), … ) this fill last excp_struct exception using excp_fill function and va_list as parameter, the type of the exception will depends on excp_fill behaviour. exception is throw using the jmp_buf on top of env_stack.

Now what is missing is the data type:

typedef 
struct {
  int errno;
  int excp_type;
  void *excp_data;
} exc_struct;

static jmp_buf *env_stack;
excp_struct * exception
int enstack_sz;
int enstack_maxsz;

[enstack(_sz|_max) and env_stack are unpacked, all could be packed, as the interface aims to be transparent]

Performance problems

It seems there are some performance problem here: free_env(jmp_buf *env), compact_excp(jmp_buf *), throw_up(jmp_buf *) all require to walk on env_stack looking at the right env. But, looking at test_excp() function jmp_buf structure is never used (and should be not used) elsewhere out of these “problematic” function. The natural solution is to use an index: the index of the current environment. The same is for exc_struct * variable: throw_excp( void(*excp_fill) (struct excp_struct *, va_list ap), … ) do not need it, and, manage(l_excp) could be replaced (using a simplified c-object idioma) with excp_fun() that take the only exception naturally. Last thing is new_excp() call that is not needed anymore, and new_env() should return an int and do all tricks.

Thus test_excp() example code become:

#include <setjmp.h>
..
int
test_excp()
{
  env_p env;
  int r;

  env = new_env();
  if (!(r=setjmp(get_env(env)))) { /* try */
    a_throwing_func();
  } else { // catch
    switch (r) { /* give that type of exception be passed this way */
    case 1: /* 1 is trapped */
      compact_excp(env);

      /* some management code */
      fprintf(stdout,"%s",gen_excp_get_msg());
      fflush(stdout);

      break;
    default: /* throw anything else */
      /* just does longjmp to parent level 
       * (the parent level do compact exceptions and env) */
      throw_up(env);
    }
  }
  free_env(env);
  /* in case of uncatched just throw (with parent env) */
  gen_excp("error");
  throw_excp();
}

The utility functions

The changed utility function are:

  • new_env() return env_p (env pointer typedef’ed as int)
  • free_env(env_p env) free env_stack till env (included)
  • compact_excp(env_p env) free env_stack till env (excluded)
  • throw_up(env_p env) longjmp to the jmp_buf below env in env_stack
  • throw_excp() longjmp to the top-stack jmp_buf

Also for any exception type it is needed some utility functions:

  • xxx_excp(exc_struct *excp, …) allocate an exception of type xxx in exception (definition of exc_struct is generic enought)
  • xxx_excp_methodX() various methodXs of the exception (get, put ..)

Here the idea is to store the type of exception in the structure and return it as longjmp parameter.

Now the utility function definition:

typedef env_p int;

env_p
new_env()
{
  /* (re)allocate env_stack if needed */
  enstack_sz ++;
  if (enstack_sz > enstack_maxsz) {
    enstack_maxsz = enstack_sz;
    env_stack = (jmp_buf *) realloc(env_stack, enstack_sz * sizeof(jmp_buf));
  }
  return (enstack_sz - 1);
}

#define free_env(env) enstack_sz=env

#define compact_excp(env) enstack_sz=env+1

#define get_env(env) env_stack[env]

inline void
throw_up(env_p env)
{
  longjmp(enstack[env-1], exception->excp_type);
}

inline void
throw_excp()
{
  longjmp(env_stack[enstack_sz - 1], exception->excp_type);
}

void
gen_excp(const char * str)
{
  exception->excp_type = 1;
  exception->excp_data = str;
}

const char *
gen_excp_get_msg()
{
  return (const char *) exception->excp_data;
}

I used inline keyword of c99 standard because I could not use a macro containing longjmp

Here I followed the lazy man rule: “never do action when it is not really needed”

A complete example

#include <stdio.h>
#include <setjmp.h>

/* data type */
typedef 
struct {
  int errno;
  int excp_type;
  void *excp_data;
} exc_struct;

static jmp_buf *env_stack;
exc_struct * exception;
int enstack_sz;
int enstack_maxsz;
typedef env_p int;

void
init_excp()
{
  exception = (exc_struct*) malloc(sizeof(exc_struct));
  enstack_sz=0;
  enstack_maxsz=0;
  env_stack=NULL;
}

void
clean_excp()
{
  if (exception!=NULL) {
    free(exception);
    exception=NULL;
  }
  if (env_stack!=NULL) {
    free(env_stack);
    enstack_sz=0;
    enstack_maxsz=0;
    env_stack=NULL;
  }
}
/* utility funcs */

env_p
new_env()
{
  // (re)allocate env_stack if needed
  enstack_sz ++;
  if (enstack_sz > enstack_maxsz) {
    enstack_maxsz = enstack_sz;
    env_stack = (jmp_buf *) realloc(env_stack, enstack_sz * sizeof(jmp_buf));
  }
  return (enstack_sz - 1);
}

#define free_env(env) enstack_sz=env

#define compact_excp(env) enstack_sz=env+1

#define get_env(env) env_stack[env]

inline void
throw_up(env_p env)
{
  longjmp(enstack[env-1], exception->excp_type);
}

inline void
throw_excp()
{
  longjmp(env_stack[enstack_sz - 1], exception->excp_type);
}

void
gen_excp(const char * str)
{
  exception->excp_type = 1;
  exception->excp_data = str;
}

void
gen2_excp(const char * str)
{
  // assert(exstack_sz == enstack_sz);
  exception->excp_type = 2;
  exception->excp_data = (void *) str;
}

const char *
gen_excp_get_msg()
{
  return (const char *) exception->excp_data;
}
// the test code

void
better_sith(int a)
{
  /* do something ...
   * ..
   * .. if the case throw (after removing unusefull env) */
  if (a%2) {
    gen_excp("(I am better_sith) it is not odd");
    throw_excp();
  }
  if (a==2) {
    gen2_excp("(I am better_sith) I do not like the number 2");
    throw_excp();
  }
}

int
dosome_sith(int a)
{
  if (a<8) {
    env_p env;
    int r;
    // this function's personal exception catch
    env=new_env();

    if (!(r=setjmp(get_env(env)))) { /* try */
      better_sith(a);
    } else {
      if (r == 1) { /* what to catch: 1 gen_ exception */
	compact_excp(env);
	fprintf(stdout,"dosome_sith::exception message: %s\n",
                       gen_excp_get_msg());
	fflush(stdout);
      } else { /* throw everything else */
	throw_up(env);
      }
    }
    free_env(env);
    /* gen_excp("I'm bored, I want return");
       throw_excp();
    */ /* this works too */
  } else {
    gen_excp(
     "(I am dosome_sith) all wrong guy, you should not give me such values");
    throw_excp();
  }
}

int
main()
{
  int a;
  int r;
  env_p env;

  init_excp();

  fprintf(stdout,"INIT PHASE\n");
  fflush(stdout);

  env = new_env();

  if (!setjmp(get_env(env))) {
    for (a=0;a<=10; a++) {
      fprintf(stdout,"main:: I try with %d\n",a);
      fflush(stdout);
      dosome_sith(a);
    }
  } else {
    compact_excp(env);
    fprintf(stdout,"main:: catched exception message: %s\n",
                   gen_excp_get_msg());
    fflush(stdout);
  }
  free_env(env);

  clean_excp();
}

If I exclude the utility, startup and shutdown function, there are no need to use pointer and all is done with less than one hundred line of C code!

Having a better look, there is something that could be tweaked. The first one is: what happen if throw_excp()’ing when no exception is in the stack? This scenario in C++ will make the program to stop, in Java compiler refuse the code, here it could be added an assert() clause before longjmp is called on exception stack. The behaviour is different from C++ because error message on abort is more informative than “this function was called outside of a setjmp block”.

The second question is: why do we have to jump in a setjmp block that do not manage this type of exception? This could be solved using a bitmap of exception but the code will become more expensive than in would be (if compiler implement inline properly) two or three longjmps. (I guess it could not used C preprocessor to make the trick because of definition of setjmp/longjmp, and because of documentation warnings on the use of these “functions”).

Some conclusions

Recalling the begin of this article, from the point of view of a object oriented library wrote in C there is no performance lost on using longjmp instead of just returning an error code, also with a number of exception type working with such library could become very comfortable, as saw there are no need to use pointer out of exception definition and after coding the missing parts (i.e. setting errno when relevant) you can still manage error just asking errno to a generic exception method (i.e. excp_get_errno()). Replacing assert with a test and abort call, when the exception is not managed it cause program to abort, but definitively this is not odd.

Symbian OS C++ framework do not support exception because of efficiency, and, mostly, because of memory cleanup problems, but using the outlined way for exception management and adding a cleanup stack (similar to the symbian os one), using ibstack where necessary, and every trick that came to be useful, this idiom result to be more versatile and as robust as symbian’s one.

The story does not end here. Actually there are some problem that arise when using setjmp/longjmp, i.e. how does interact with posix thread idiom (i.e. at least one exception for thread are needed). To integrate such thing in a complete (and complex enough) library could be not so easy as it could appear.

References

setjmp() and longjmp() functions are defined in POSIX and single unix standard, documentation is in its own man page and info format GNU Libc documentation in GNU systems
Symbian OS is documented in its own site, documentation is distributed as zip package and available on line, also distribuited by symbian vendors (Nokia, Siemens, Sony Ericsson, Motorola, etc.)


Posted

in

,

by