Thursday, January 19, 2017

Exceptions Handling - Mechanics

I've posted (quite some time ago) a description of the try/catch blocks provided by clibutl. I guess it's now the time to look under the hood and see at the implementation.
Below is the code, we'll go through it in the following sections.
typedef struct utl_jb_s {
  jmp_buf           jmp;
  struct utl_jb_s  *prv;
  int               err;
  int32_t           flg;
} utl_jb_t;

extern utl_jb_t *utl_jmp_list; // Defined in utl_hdr.c

#define try  for ( utl_jb_t utl_jb = {.flg=0, .prv=utl_jmp_list}; \
                  !utl_jb.flg && (utl_jmp_list=&utl_jb); \
                   utl_jmp_list=utl_jb.prv, utl_jb.flg=1) \
              if ((utl_jb.err = setjmp(utl_jb.jmp)) == 0)
                 
#define catch(x) else if (   (utl_jb.err == (x)) \
                          && (utl_jmp_list=utl_jb.prv, utl_jb.flg=1)) 

#define catchall else for ( utl_jmp_list=utl_jb.prv; \
                           !utl_jb.flg; \
                            utl_jb.flg=1) 

#define throw(x) do { \
                   int ex_ = x; \
                   if (ex_ > 0 && utl_jmp_list) \
                     longjmp(utl_jmp_list->jmp,ex_); \
                 } while (0)

#define thrown()  utl_jb.err
  
#define rethrow() throw(thrown())

setjmp()/longjmp()


The magic that allow a C program to (almost) freely jump from a function to another is provided by the two functions setjmp() and longjmp() provided in the setjmp.h standard header.
When setjmp() is called it stores the current state of the function and returns 0. When longjmp() is executed, it restores the previously saved state and the execution proceeds as if the setjmp() had returned a non-zero value.
In the minimal example below:
#include <stdio.h>
#include <setjmp.h>

static jmp_buf jbuf;

void do_something(void) {
  printf("Almost there ... ");
  longjmp(jbuf,1);  // jumps back
  printf("done\n"); // not executed!
}

int main() {
  if (setjmp(jbuf) == 0)
    do_something();
  else // only if longjmp() is called
    printf("not done!\n");
  return 0;
}
Executing the program above will result in printing:
    Almost there ... not done!
This is what happens:
  • The call to setjmp(jbuf) returns 0 and the do_something() function is invoked
  • The string "Almost there ... " is printed.
  • The longjump(jbuf,1) is executed, jumping back to the if where setjmp() was called but, this time, returning 1
  • The string "not done!" is printed.
Using setjmp() and longjmp() is subject to limitations and caveats and may cause subtle bugs if not managed properly.

The jmp_buf list


Since try/catch blocks can be nested (directly or indirectly via function call) we need as many jmp_buf buffers as nested levels. Those buffers are kept in a linked list whose head is stored in the global variable utl_jmp_list:
typedef struct utl_jb_s {
  jmp_buf           jmp;
  struct utl_jb_s  *prv;
  int               err;
  int32_t           flg;
} utl_jb_t;

extern utl_jb_t *utl_jmp_list;
Actually, as you have surely noticed, this is a list of utl_jb_t structures, each one containing a jmp_buf element plus what is needed to handle the exceptions.
During the execution, if there are nested try blocks (or if there is a try block in a function called from within another try block) the list is as shown in the following image:


Within inner try block, the head of the list is the structure marked with C.

The try macro


Let's see how the try macro works:
#define try  for ( utl_jb_t utl_jb = {.flg=0, .prv=utl_jmp_list}; \
                  !utl_jb.flg && (utl_jmp_list=&utl_jb); \
                   utl_jmp_list=utl_jb.prv, utl_jb.flg=1) \
              if ((utl_jb.err = setjmp(utl_jb.jmp))== 0)
Using for to create statement-like macros is a trick similar to the well known do { } while(0). It is appropriate when you have to perform something at the end of the pseudo-statement.
The for initialization part:
utl_jb_t utl_jb = {.flg=0, .prv=utl_jmp_list};
creates a new local variable of type utl_jb_t and initializes it so that:
  • The current list is appended as its tail: .prv=utl_jmp_list
  • The flg field, which controls the flow of the for loop, is set to 0
Then we check if we have to enter the loop or not:
!utl_jb.flg && (utl_jmp_list=&utl_jb); 
The first time we execute the loop, the two expressions in AND are true:
  • !utl_jb.flg is true since we just initialized utl_jb.flg to 0;
  • (utl_jmp_list=&utl_jb) is true because the address of utl_jb is surely not NULL.
So, we will enter the body of the loop! Loot at how the newly created variable structure utl_jb is made the head of the list by the assignment utl_jmp_list = &utl_jb.

At the end of the try/catch blocks, the third part of the for statement will be executed:
     utl_jmp_list=utl_jb.prv, utl_jb.flg=1
 removing the local utl_jb structure from the list and setting utl_jb.flg to 1 to ensure that we will not re-enter the for loop. .

When we'll re-execute the test to see if we have to re-enter the for loop:
    !utl_jb.flg && (utl_jmp_list=&utl_jb); 
we'll have utl_jb.flg set to 1, the entire expression is false and we will not re-enter the for loop body. Thanks to the short-circuit behaviour of && the other expression won't be evaluated (which would cause the jump buffer list to be messed up!).


The try/catch blocks


The setjmp() function needs to be in the scope of a conditional statement so to distinguish the normal path (i.e. when the setjmp() is executed) from the exceptional path (i.e. when the longjmp() is executed). In our case the body of the for is a single if statement which guards the the try body:
    if ((utl_jb.err = setjmp(utl_jb.jmp))== 0)
In essence, a try block is an if statement and a catch() block is its else part:
#define catch(x) else if (   (utl_jb.err == (x)) \
                          && (utl_jmp_list = utl_jb.prv, utl_jb.flg = 1))
A catch is an if as well so to be able to accept the next catch.

Looking at the macro expansions should make things clearer:
  try {               |   if ((utl_jb.err = setjmp(utl_jb.jmp))== 0) {
    do_something();   |     do_something();
  }                   |   }
  catch(4) {         ==>  else if ((utl_jb.err == (4)) &&
                      |            (utl_jmp_list = utl_jb.prv, utl_jb.flg = 1)) { 
    // exception 4    |     // exception 4
  }                   |   }
I've omitted the for part for clarity. You can see that each catch is just a branch of a set of nested if. The conditition (utl_jmp_list = utl_jb.prv, utl_jb.flg = 1) is an always true expression that removes the current jump buffer from the head of the list (the assignement to to utl_jb.flg is there only to ensure that the entire expressions evaluates to true).
Removing utl_jb from the list ensures that if we throw() an exception within a catch block, it will be handled by the higher-level try block (and not the same one the catch block belongs to).

Throwing exceptions


The throw() macro will call longjmp() to jump back:
#define throw(x) do { \
                   int ex_ = x; \
                   if (ex_ > 0 && utl_jmp_list) longjmp(utl_jmp_list->jmp,ex_); \
                 } while (0)
The variable ex_ is used to ensure that the macro argument won't be evaluated multiple times. Exceptions are positive integers (so that the setjmp() function won't be fooled).
Testing utl_jmp_list ensures that we are indeed within a try block (or in a function called from within a try block). Note how throw() take the head of the jump buffers list.

If, within a catch block, one needs to know which exception has been thrown he can use the thrown() function which is actully just a macro to access the value of utl_jb.err. This is most probably only useful within a catchall block that handles any exception that has not been caught by a previous catch block.

The rethrow() function is simply defined in terms of throw() and thrown():
#define rethrow() throw(thrown())

Catch all


The last block should be a catchall block for two reasons:
  • It's good practice to ensure that no exception is left unhandled. Even if, at first, one could cover every possible expection in its specific catch(), there is the risk that at a later time, a new exception is introduced and no specific handler for it is defined.
    Having a catchall block at the end, possibly with an error message if the event is really unexpected, ensures that no such problem will arise.
  • The try/catch macros, unfortunately, are not 100% safe. Consider the following code:
    if (some_test()) 
        try 
            do_something();
        catch(EXCEPTION)
            handle_it();
    else
        printf("Not even tried!\n");
    
    It's clear that the intention is to have it printing "Not even tried!" if some_test() fails.
    Too bad that the hidden if in the catch macro will claim the else statement as its own!
An obvious way to solve this is to enclose the try/catch block within braces:
if (some_test()) {
    try 
        do_something();
    catch(EXCEPTION)
        handle_it();
}
else
    printf("Not even tried!\n");
 However, had the catchall block being present, the problem would disappear entirely:
if (some_test()) 
    try 
        do_something();
    catch(EXCEPTION)
        handle_it();
    catchall
        handle_others();
else
    printf("Not even tried!\n");
The code above correctly prints "Not even tried!" if some_test() fails.

To achieve that, the catchall macro is defined as:
#define catchall else for ( utl_jmp_list=utl_jb.prv; \
                           !utl_jb.flg; \
                            utl_jb.flg=1) 
The else statement will close the set of nested if. The for statement removes the jump list head (as for the other catch blocks) and uses utl_jb.flg to ensure it is executed exactly once.

*  *  *  *
Adding exception handling to C is surprisingly easy and it has proven itself an enjoyable and instructive task. I hope you can find it useful as well.

0 comments :

Post a Comment