DragonFly On-Line Manual Pages

Search: Section:  


ex(3)                         Exception Handling                         ex(3)

NAME

OSSP ex - Exception Handling

VERSION

OSSP ex 1.0.5 (02-Oct-2005)

SYNOPSIS

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

DESCRIPTION

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

IMPLEMENTATION CONTROL

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.

PROGRAMMING PITFALLS

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

MULTITHREADING ENVIRONMENTS

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.

EXAMPLES

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.

SEE ALSO

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

HISTORY

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.

AUTHORS

Ralf S. Engelschall rse@engelschall.com www.engelschall.com 02-Oct-2005 OSSP ex 1.0.5 ex(3)

Search: Section: