OSSP ex - Exception Handling
OSSP ex 1.0.5 (02-Oct-2005)
ex_t variable;
ex_try BLOCK1 [ex_cleanup BLOCK2]
ex_catch (variable) BLOCK3
ex_throw(class, object, value);
ex_rethrow;
ex_defer BLOCK
ex_shield BLOCK
if (ex_catching) ...
if (ex_deferred) ...
if (ex_shielding) ...
OSSP ex is a small ISO-C++ style exception handling library for
use in the ISO-C language. It allows you to use the paradigm of
throwing and catching exceptions in order to reduce the amount of error
handling code without making your program less robust.
This is achieved by directly transferring exceptional return codes
(and the program control flow) from the location where the exception is
raised (throw point) to the location where it is handled (catch point) --
usually from a deeply nested sub-routine to a parent routine. All
intermediate routines no longer have to make sure that the exceptional
return codes from sub-routines are correctly passed back to the parent.
EXCEPTIONS
An OSSP ex exception is a triple
<class,object,value> where class
identifies the class of the exception thrower, object identifies the
particular class instance of the exception thrower, and value is the
exceptional return code value the thrower wants to communicate. All three
parts are of type "void *" internally, but
every value which can be lossless "casted" to this type is usable.
Exceptions are created on-the-fly by the ex_throw command.
APPLICATION PROGRAMMER INTERFACE (API)
The OSSP ex API consists of the following elements:
- ex_t variable;
- This is the declaration of an exception variable. It is usually never
initialized manually. Instead it is initialized by an ex_catch
clause and just used read-only inside its block. Such a variable of type
ex_t consists of six attributes:
- void *ex_class
- This is the class argument of the ex_throw call which
created the exception. This can globally and uniquely identify the class
to which ex_value belongs to. Usually this is a pointer to a static
object (variable, structure or function) which identifies the class of the
thrower and allows the catcher to correctly handle ex_value. It is
usually just an additional (optional) information to ex_value.
- void *ex_object
- This is the object argument of the ex_throw call which
created the exception. This can globally and uniquely identify the class
instance ex_value belongs to (in case multiple instances exists at
all). Usually this a pointer to a dynamic object (structure) which
identifiers the particular instance of the thrower. It is usually just an
additional (optional) information to ex_value.
- void *ex_value
- This is the value argument of the ex_throw call which
created the exception. This is the exceptional return code value which has
to uniquely identify the type of exception. Usually this is the value
which is returned if no exceptions would be
thrown. In the simple case this is just a numerical return code. In the
complex case this can be a pointer to an arbitrary complex data structure
describing the exception.
- char *ex_file
- This is the file name of the ISO-C source where the ex_throw
call was performed. It is automatically provided as an additional
information about the throw point and is intended mainly for tracing and
debugging purposes.
- int ex_line
- This is the line number inside the ISO-C source file name where the
ex_throw call was performed. It is automatically provided as an
additional information about the throw point and is intended mainly for
tracing and debugging purposes.
- char *ex_func
- This is the function name (if determinable, else
"#NA#") inside the ISO-C source
file name where the ex_throw call was performed. It is
automatically provided as an additional information about the throw point
and is intended mainly for tracing and debugging purposes.
- ex_try BLOCK1 [ex_cleanup BLOCK2]
ex_catch (variable) BLOCK3
- This is the primary syntactical construct provided by OSSP ex. It
is modeled after the ISO-C++
try-catch clause which in
turn is very similar to an ISO-C
if-else clause. It
consists of an ex_try block BLOCK1 which forms the dynamic
scope for exception handling (i.e. exceptions directly thrown there or
thrown from its sub-routines are caught), an optional ex_cleanup
block BLOCK2 for performing cleanup operations and an
ex_catch block BLOCK3 where the caught exceptions are
handled.
The control flow in case no exception is thrown is simply
BLOCK1, optionally followed by BLOCK2; BLOCK3 is
skipped. The control flow in case an exception is thrown is:
BLOCK1 (up to the statement where the exception is thrown only),
optionally followed by BLOCK2, followed by BLOCK3.
The ex_try, ex_cleanup and ex_catch
cannot be used separately, they work only in combination because they
form a language clause as a whole. In contrast to ISO-C++ there
is only one ex_catch block and not multiple ones (all OSSP
ex exceptions are of the same ISO-C type ex_t). If an
exception is caught, it is stored in variable for inspection
inside the ex_catch block. Although having to be declared
outside, the variable value is only valid within the
ex_catch block. But the variable can be re-used in subsequent
ex_catch clauses, of course.
The ex_try block is a regular ISO-C language
statement block, but it is not allowed to jump into it via
"goto" or
longjmp(3) or out of it via
"break",
"return",
"goto" or
longjmp(3) because there is some hidden setup
and cleanup that needs to be done by OSSP ex regardless of
whether an exception is caught. Jumping into an ex_try clause
would avoid doing the setup, and jumping out of it would avoid doing the
cleanup. In both cases the result is a broken exception handling
facility. Nevertheless you are allowed to nest ex_try
clauses.
The ex_cleanup and ex_catch blocks are regular
ISO-C language statement blocks without any restrictions. You are
even allowed to throw (and in the ex_catch block to re-throw) an
exception.
There is just one subtle portability detail you have to
remember about ex_try blocks: all accessible ISO-C objects
have the (expected) values as of the time ex_throw was called,
except that the values of objects of automatic storage invocation
duration that do not have the
"volatile" storage class and
have been changed between the ex_try invocation and
ex_throw are indeterminate. This is because both you usually do
not know which commands in the ex_try were already successful
before the exception was thrown (logically speaking) and because the
underlying ISO-C setjmp(3) facility applies those
restrictions (technically speaking).
- ex_throw(class, object, value);
- This builds an exception from the supplied arguments and throws it. If an
ex_try/ex_catch clause formed the dynamic scope of the
ex_throw call, this exception is copied into the variable of
its ex_catch clause and the program control flow is continued in
the (optional ex_cleanup and then in the) ex_catch block. If
no ex_try/ex_catch clause exists in the dynamic scope of the
ex_throw call, the program calls abort(3).
The ex_throw can be performed everywhere, including inside
ex_try, ex_cleanup and ex_catch blocks.
- ex_rethrow;
- This is only valid within an ex_catch block and re-throws the
current exception (in variable). It is similar to the call
ex_throw(variable.ex_class, variable.ex_object,
variable.ex_value) except for the difference that the
ex_file, ex_line and
ex_func elements of the caught exception are
passed through as it would have been never caught.
- ex_defer BLOCK
- This directive executes BLOCK while deferring the throwing of
exceptions, i.e., inside the dynamic scope of ex_defer all
ex_throw operations are remembered but deferred and on leaving the
BLOCK the first occurred exception is thrown. The second and
subsequent exceptions are ignored.
The ex_defer block BLOCK is a regular
ISO-C language statement block, but it is not allowed to jump
into it via "goto" or
longjmp(3) or out of it via
"break",
"return",
"goto" or
longjmp(3) because this would cause the deferral
level to become out of sync. Jumping into an ex_defer clause
would avoid increasing the exception deferral level, and jumping out of
it would avoid decreasing it. In both cases the result is an incorrect
exception deferral level. Nevertheless you are allowed to nest
ex_defer clauses.
- ex_shield BLOCK
- This directive executes BLOCK while shielding it against the
throwing of exceptions, i.e., inside the dynamic scope of ex_shield
all ex_throw operations are just silently ignored.
The ex_shield block is a regular ISO-C language
statement block, but it is not allowed to jump into it via
"goto" or
longjmp(3) or out of it via
"break",
"return",
"goto" or
longjmp(3) because this would cause the
shielding level to become out of sync. Jumping into an ex_shield
clause would avoid increasing the exception shielding level, and jumping
out of it would avoid decreasing it. In both cases the result is an
incorrect exception shielding level. Nevertheless you are allowed to
nest ex_shield clauses.
- ex_catching
- This is a boolean flag which can be checked inside the dynamic scope of an
ex_try clause to test whether the current scope is exception
catching (see ex_try/ex_catch clause).
- ex_deferred
- This is a boolean flag which can be checked inside the dynamic scope of an
ex_defer clause to test whether the current scope is exception
deferring (see ex_defer clause).
- ex_shielding
- This is a boolean flag which can be checked inside the dynamic scope of an
ex_shield clause to test whether the current scope is exception
shielding (see ex_shield clause).
OSSP ex uses a very light-weight but still flexible exception facility
implementation. The following adjustments can be made before including the
ex.h header:
Machine Context
In order to move the program control flow from the exception throw
point (ex_throw) to the catch point (ex_catch), OSSP ex
uses four macros:
- __ex_mctx_struct
- This holds the contents of the machine context structure. A pointer to
such a machine context is passed to the following macros as
mctx.
- __ex_mctx_save(__ex_mctx_struct *mctx)
- This is called by the prolog of ex_try to save the current machine
context in mctx. This function has to return true (not
0) after saving. If the machine context is
restored (by __ex_mctx_restore) it has to return false
(0). In other words, this function has to return
twice and indicate the particular situation with the provided return
code.
- __ex_mctx_restored(__ex_mctx_struct *mctx)
- This is called by the epilog of ex_try to perform additional
operations at the new (restored) machine context after an exception was
caught. Usually this is a no-operation macro.
- __ex_mctx_restore(__ex_mctx_struct *mctx)
- This is called by ex_throw at the old machine context in order to
restore the machine context of the ex_try/ex_catch clause
which will catch the exception.
The default implementation (define
__EX_MCTX_SJLJ__ or as long as
__EX_MCTX_CUSTOM__ is not defined) uses the
ISO-C jmp_buf(3) facility:
#define __ex_mctx_struct jmp_buf jb;
#define __ex_mctx_save(mctx) (setjmp((mctx)->jb) == 0)
#define __ex_mctx_restored(mctx) /* noop */
#define __ex_mctx_restore(mctx) (void)longjmp((mctx)->jb, 1)
Alternatively, you can define
__EX_MCTX_SSJLJ__ to use POSIX.1
sigjmp_buf(3) or __EX_MCTX_MCSC__ to use
POSIX.1 ucontext(3). For using a custom implementation define
__EX_MCTX_CUSTOM__ and provide own definitions for
the four __ex_mctx_xxxx macros.
Exception Context
In order to maintain the exception catching stack and for passing
the exception between the throw and the catch point, OSSP ex uses a
global exception context, returned on-the-fly by the callback
"ex_ctx_t
*(*__ex_ctx)(void)".
By default, __ex_ctx (which is __ex_ctx_default as
provided by libex) returns a pointer to a static
ex_ctx_t context. For use in multi-threading
environments, this should be overwritten with a callback function returning
a per-thread context structure (see section MULTITHREADING
ENVIRONMENTS below).
To initialize an exception context structure there are two macros
defined: "EX_CTX_INITIALIZER" for static initialization and
"void
EX_CTX_INITIALIZE(ex_ctx_t *)"
for dynamic initialization.
Termination Handler
In case there is an exception thrown which is not caught by any
ex_try/ex_catch clauses, OSSP ex calls the callback
"void
(*__ex_terminate)(ex_t
*)". It receives a pointer to the exception object which was
thrown.
By default, __ex_terminate (which is
__ex_terminate_default as provided by libex) prints a message
of the form "**EX: UNCAUGHT
EXCEPTION: class=0xXXXXXXXX object=0xXXXXXXXX value=0xXXXXXXX
[xxxx:NNN:xxxx]" to stderr and then
calls abort(3) in order to terminate the application. For use in
multi-threading environments, this should be overwritten with a callback
function which terminates only the current thread. Even better, a real
application always should have a top-level ex_try/ex_catch
clause in its "main()" in order to more
gracefully terminate the application.
Namespace Mapping
The OSSP ex implementation consistently uses the
"ex_",
"__ex_" and
"__EX_" prefixes for namespace protection.
But at least the "ex_" prefix for the API
macros ex_try, ex_cleanup, ex_catch, ex_throw,
ex_rethrow and ex_shield sometimes have an unpleasant optical
appearance. Especially because OSSP ex is modeled after the exception
facility of ISO-C++ where there is no such prefix on the language
directives, of course.
For this, OSSP ex optionally provides the ability to
provide additional namespace mappings for those API elements. By default
(define __EX_NS_CXX__ or as long as
__EX_NS_CUSTOM__ and
__cplusplus is not defined) you can additionally use
the ISO-C++ style names catch, cleanup, throw,
rethrow and shield. As an alternative you can define
__EX_NS_UCCXX__ to get the same but with a more
namespace safe upper case first letter.
Exception handling is a very elegant and efficient way of dealing with
exceptional situation. Nevertheless it requires additional discipline in
programming and there are a few pitfalls one must be aware of. Look the
following code which shows some pitfalls and contains many errors (assuming a
mallocex() function which throws an exception if malloc(3)
fails):
/* BAD EXAMPLE */
ex_try {
char *cp1, *cp2, cp3;
cp1 = mallocex(SMALLAMOUNT);
globalcontext->first = cp1;
cp2 = mallocex(TOOBIG);
cp3 = mallocex(SMALLAMOUNT);
strcpy(cp1, "foo");
strcpy(cp2, "bar");
}
ex_cleanup {
if (cp3 != NULL) free(cp3);
if (cp2 != NULL) free(cp2);
if (cp1 != NULL) free(cp1);
}
ex_catch(ex) {
printf("cp3=%s", cp3);
ex_rethrow;
}
This example raises a few issues:
- 01: variable scope
- Variables which are used in the ex_cleanup or ex_catch
clauses must be declared before the ex_try clause, otherwise they
only exist inside the ex_try block. In the example above,
cp1, cp2 and
cp3 are automatic variables and only exist in the
block of the ex_try clause, the code in the ex_cleanup and
ex_catch clauses does not know anything about them.
- 02: variable initialization
- Variables which are used in the ex_cleanup or ex_catch
clauses must be initialized before the point of the first possible
ex_throw is reached. In the example above, ex_cleanup would
have trouble using cp3 if mallocex() throws
a exception when allocating a TOOBIG buffer.
- 03: volatile variables
- Variables which are used in the ex_cleanup or ex_catch
clauses must be declared with the storage class
"volatile", otherwise they might contain
outdated information if ex_throw throws an exception. If using a
"free if unset" approach like the example does in the
ex_cleanup clause, the variables must be initialized (see
02) and remain valid upon use.
- 04: clean before catch
- The ex_cleanup clause is not only written down before the
ex_catch clause, it is also evaluated before the ex_catch
clause. So, resources being cleaned up must no longer be used in the
ex_catch block. The example above would have trouble referencing
the character strings in the printf(3) statement because these have
been freed before.
- 05: variable uninitialization
- If resources are passed away and out of the scope of the
ex_try/ex_cleanup/ex_catch construct and the
variables were initialized for using a "free if unset" approach
then they must be uninitialized after being passed away. The example above
would free(3) cp1 in the ex_cleanup
clause if mallocex() throws an exception if allocating a
TOOBIG buffer. The
globalcontext->first pointer hence becomes
invalid.
The following is fixed version of the code (annotated with the
pitfall items for reference):
/* GOOD EXAMPLE */
{ /*01*/
char * volatile /*03*/ cp1 = NULL /*02*/;
char * volatile /*03*/ cp2 = NULL /*02*/;
char * volatile /*03*/ cp3 = NULL /*02*/;
try {
cp1 = mallocex(SMALLAMOUNT);
globalcontext->first = cp1;
cp1 = NULL /*05 give away*/;
cp2 = mallocex(TOOBIG);
cp3 = mallocex(SMALLAMOUNT);
strcpy(cp1, "foo");
strcpy(cp2, "bar");
}
clean { /*04*/
printf("cp3=%s", cp3 == NULL /*02*/ ? "" : cp3);
if (cp3 != NULL)
free(cp3);
if (cp2 != NULL)
free(cp2);
/*05 cp1 was given away */
}
catch(ex) {
/*05 global context untouched */
rethrow;
}
}
Alternatively, this could also be used:
/* ALTERNATIVE GOOD EXAMPLE */
{ /*01*/
char * volatile /*03*/ cp1 = NULL /*02*/;
char * volatile /*03*/ cp2 = NULL /*02*/;
char * volatile /*03*/ cp3 = NULL /*02*/;
try {
cp1 = mallocex(SMALLAMOUNT);
globalcontext->first = cp1;
/*05 keep responsibility*/
cp2 = mallocex(TOOBIG);
cp3 = mallocex(SMALLAMOUNT);
strcpy(cp1, "foo");
strcpy(cp2, "bar");
}
clean { /*04*/
printf("cp3=%s", cp3 == NULL /*02*/ ? "" : cp3);
if (cp3 != NULL)
free(cp3);
if (cp2 != NULL)
free(cp2);
if (cp1 != NULL)
free(cp1);
}
catch(ex) {
globalcontext->first = NULL;
rethrow;
}
}
OSSP ex is designed to work both in single-threading and multi-threading
environments. The default is to support single-threading only. But it is easy
to configure OSSP ex to work correctly in a multi-threading environment
like POSIX pthreads or GNU pth.
There are only two issues: which machine context to use and where
to store the exception context to make sure exception throwing happens only
within a thread and does not conflict with the regular thread dispatching
mechanism.
GNU pth
Using OSSP ex together with GNU pth is
straight-forward, because GNU pth 2.0 (and higher) already has
support for OSSP ex built-in. All which is needed is that GNU
pth is configured with the GNU Autoconf option
--with-ex. Then each GNU pth user-space
thread has its own OSSP ex exception context automatically. The
default of using ISO-C jmp_buf(3) does not conflict with the
thread dispatching mechanisms used by GNU pth.
POSIX pthreads
Using OSSP ex inside an arbitrary POSIX pthreads
standard compliant environment is also straight-forward, although it
requires extra coding. What you basically have to do is to make sure that
the __ex_ctx becomes a per-thread context and that
__ex_terminate terminates only the current thread. To get an
impression, a small utility library for this follows:
- pthread_ex.h
-
#ifndef __PTHREAD_EX_H__
#define __PTHREAD_EX_H__
#include <pthread.h>
int pthread_init_ex (void);
int pthread_create_ex (pthread_t *, const pthread_attr_t *,
void *(*)(void *), void *);
#ifndef PTHREAD_EX_INTERNAL
#define pthread_init pthread_init_ex
#define pthread_create pthread_create_ex
#endif
#endif /* __PTHREAD_EX_H__ */
- pthread_ex.c
-
#include <stdlib.h>
#include <pthread.h>
#define PTHREAD_EX_INTERNAL
#include "pthread_ex.h"
#include "ex.h"
/* context storage key */
static pthread_key_t pthread_ex_ctx_key;
/* context destructor */
static void pthread_ex_ctx_destroy(void *data)
{
if (data != NULL)
free(data);
return;
}
/* callback: context fetching */
static ex_ctx_t *pthread_ex_ctx(void)
{
return (ex_ctx_t *)
pthread_getspecific(pthread_ex_ctx_key);
}
/* callback: termination */
static void pthread_ex_terminate(ex_t *e)
{
pthread_exit(e->ex_value);
}
/* pthread init */
int pthread_init_ex(void)
{
int rc;
/* additionally create thread data key
and override OSSP ex callbacks */
pthread_key_create(&pthread_ex_ctx_key,
pthread_ex_ctx_destroy);
__ex_ctx = pthread_ex_ctx;
__ex_terminate = pthread_ex_terminate;
return rc;
}
/* internal thread entry wrapper information */
typedef struct {
void *(*entry)(void *);
void *arg;
} pthread_create_ex_t;
/* internal thread entry wrapper */
static void *pthread_create_wrapper(void *arg)
{
pthread_create_ex_t *wrapper;
ex_ctx_t *ex_ctx;
/* create per-thread exception context */
wrapper = (pthread_create_ex_t *)arg;
ex_ctx = (ex_ctx_t *)malloc(sizeof(ex_ctx_t));
EX_CTX_INITIALIZE(ex_ctx);
pthread_setspecific(pthread_ex_ctx_key, ex_ctx);
/* perform original operation */
return wrapper->entry(wrapper->arg);
}
/* pthread_create() wrapper */
int pthread_create_ex(pthread_t *thread,
const pthread_attr_t *attr,
void *(*entry)(void *), void *arg)
{
pthread_create_ex_t wrapper;
/* spawn thread but execute start
function through wrapper */
wrapper.entry = entry;
wrapper.arg = arg;
return pthread_create(thread, attr,
pthread_create_wrapper, &wrapper);
}
Now all which is required is that you include pthread_ex.h
after the standard pthread.h header and to call pthread_init
once at startup of your program.
As a real-life example we will look how you can add optional OSSP
ex based exception handling support to a library foo. The
original library looks like this:
- foo.h
-
typedef enum {
FOO_OK,
FOO_ERR_ARG,
FOO_ERR_XXX,
FOO_ERR_SYS,
FOO_ERR_IMP,
...
} foo_rc_t;
struct foo_st;
typedef struct foo_st foo_t;
foo_rc_t foo_create (foo_t **foo);
foo_rc_t foo_perform (foo_t *foo);
foo_rc_t foo_destroy (foo_t *foo);
- foo.c
-
#include "foo.h"
struct foo_st {
...
}
foo_rc_t foo_create(foo_t **foo)
{
if ((*foo = (foo_t)malloc(sizeof(foo))) == NULL)
return FOO_ERR_SYS;
(*foo)->... = ...
return FOO_OK;
}
foo_rc_t foo_perform(foo_t *foo)
{
if (foo == NULL)
return FOO_ERR_ARG;
if (...)
return FOO_ERR_XXX;
return FOO_OK;
}
foo_rc_t foo_destroy(foo_t *foo)
{
if (foo == NULL)
return FOO_ERR_ARG;
free(foo);
return FOO_OK;
}
Then the typical usage of this library is:
#include "foo.h"
...
foo_t foo;
foo_rc_t rc;
...
if ((rc = foo_create(&foo)) != FOO_OK)
die(rc);
if ((rc = foo_perform(foo)) != FOO_OK)
die(rc);
if ((rc = foo_destroy(foo)) != FOO_OK)
die(rc);
But what you really want, is to use exception handling to get rid
of the intermixed error handling code:
#include "foo.h"
#include "ex.h"
...
foo_t foo;
ex_t ex;
...
ex_try {
foo_create(&foo);
foo_perform(foo);
foo_destroy(foo);
}
ex_catch (ex) {
die((foo_rc_t)ex->ex_value);
}
You can achieve this very easily by changing the library as
following:
- foo.h
-
...
extern const char foo_id[];
...
- foo.c
-
#include "foo.h"
const char foo_id[] = "foo 1.0";
#ifdef WITH_EX
#include "ex.h"
#define FOO_RC(rv) \
( (rv) != FOO_OK && (ex_catching && !ex_shielding) \
? (ex_throw(foo_id, NULL, (rv)), (rv)) : (rv) )
#else
#define FOO_RC(rv) (rv)
#endif
struct foo_st {
...
}
foo_rc_t foo_create(foo_t **foo)
{
if ((*foo = (foo_t)malloc(sizeof(foo))) == NULL)
return FOO_RC(FOO_ERR_SYS);
(*foo)->... = ...
return FOO_OK;
}
foo_rc_t foo_perform(foo_t *foo)
{
if (foo == NULL)
return FOO_RC(FOO_ERR_ARG);
if (...)
return FOO_RC(FOO_ERR_XXX);
return FOO_OK;
}
foo_rc_t foo_destroy(foo_t *foo)
{
if (foo == NULL)
return FOO_RC(FOO_ERR_ARG);
free(foo);
return FOO_OK;
}
This way the library by default is still exactly the same. If you
now compile it with -DWITH_EX you activate exception
handling support. This means that all API functions throw exceptions where
ex_value is the foo_rc_t
instead of returning this value.
ISO-C++ try, catch,
throw.
Java try,
catch, finally,
throw.
ISO-C jmp_buf(3), setjmp(3),
longjmp(3).
POSIX.1 sigjmp_buf(3), sigsetjmp(3),
siglongjump(3).
POSIX.1 ucontext(3), setcontext(3),
getcontext(3).
OSSP ex was invented in January 2002 by Ralf S. Engelschall
<rse@engelschall.com> for use inside the OSSP project. Its
creation was prompted by the requirement to reduce the error handling inside
OSSP lmtp2nntp.
The core try/catch clause was inspired by
ISO-C++ and the implementation was partly derived from cexcept
2.0.0, a similar library written 2000 by Adam M. Costello
<amc@cs.berkeley.edu> and Cosmin Truta
<cosmin@cs.toronto.edu>.
The cleanup clause was inspired by the Java
finally clause. The shield feature was
inspired by an "errno" shielding facility
used in the GNU pth implementation. The defer feature was
invented to simplify an application's cleanup handling if multiple
independent resources are allocated and have to be freed on error.
Ralf S. Engelschall
rse@engelschall.com
www.engelschall.com