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:
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.