#include <sm/exc.h> typedef struct sm_exc_type SM_EXC_TYPE_T; typedef struct sm_exc SM_EXC_T; typedef union sm_val SM_VAL_T; /* ** Exception types */ extern const char SmExcTypeMagic[]; struct sm_exc_type { const char *sm_magic; const char *etype_category; const char *etype_argformat; void (*etype_print)(SM_EXC_T *exc, SM_FILE_T *stream); const char *etype_printcontext; }; extern const SM_EXC_TYPE_T SmEtypeOs; extern const SM_EXC_TYPE_T SmEtypeErr; void sm_etype_printf( SM_EXC_T *exc, SM_FILE_T *stream); /* ** Exception objects */ extern const char SmExcMagic[]; union sm_val { int v_int; long v_long; char *v_str; SM_EXC_T *v_exc; }; struct sm_exc { const char *sm_magic; size_t exc_refcount; const SM_EXC_TYPE_T *exc_type; SM_VAL_T *exc_argv; }; SM_EXC_T * sm_exc_new_x( const SM_EXC_TYPE_T *type, ...); SM_EXC_T * sm_exc_addref( SM_EXC_T *exc); void sm_exc_free( SM_EXC_T *exc); bool sm_exc_match( SM_EXC_T *exc, const char *pattern); void sm_exc_print( SM_EXC_T *exc, SM_FILE_T *stream); void sm_exc_write( SM_EXC_T *exc, SM_FILE_T *stream); void sm_exc_raise_x( SM_EXC_T *exc); void sm_exc_raisenew_x( const SM_EXC_TYPE_T *type, ...); /* ** Ensure that cleanup code is executed, ** and/or handle an exception. */ SM_TRY Block of code that may raise an exception. SM_FINALLY Cleanup code that may raise an exception. This clause is guaranteed to be executed even if an exception is raised by the SM_TRY clause or by an earlier SM_FINALLY clause. You may have 0 or more SM_FINALLY clauses. SM_EXCEPT(exc, pattern) Exception handling code, triggered by an exception whose category matches 'pattern'. You may have 0 or more SM_EXCEPT clauses. SM_END_TRY
Functions in libsm report errors and other unusual conditions by raising an exception, rather than by returning an error code or setting a global variable such as errno. If a libsm function is capable of raising an exception, its name ends in "_x". (We do not raise an exception when a bug is detected in the program; instead, we terminate the program using sm_abort. See the assertion package for details.)
When you are using the libsm exception handling package, you are using a new programming paradigm. You will need to abandon some of the programming idioms you are accustomed to, and switch to new idioms. Here is an overview of some of these idioms.
Here is an example of how to construct an exception object and raise an exception. In this example, we convert a Unix system error into an exception.
Because the idiom sm_exc_raise_x(sm_exc_new_x(...)) is so common, it can be abbreviated as sm_exc_raisenew_x(...).fd = open(path, O_RDONLY); if (fd == -1) sm_exc_raise_x(sm_exc_new_x(&SmEtypeOs, errno, "open", "%s", path));
use this:errx(1, "%s:%d: syntax error", filename, lineno);
The latter code raises an exception, unwinding the call stack and executing cleanup code. If the exception is not handled, then the exception is printed to stderr and the program exits. The end result is substantially the same as a call to errx.sm_exc_raisenew_x(&SmEtypeErr, "%s:%d: syntax error", filename, lineno);
For example, suppose that you have written the following code:
If any of the functions called within "... some code ..." have names ending in _x, then it is possible that an exception will be raised, and if that happens, then "rpool" will not be freed. And that's a bug. To fix this bug, change your code so it looks like this:rpool = sm_rpool_new_x(&SmRpoolRoot, 0); ... some code ... sm_rpool_free_x(rpool);
rpool = sm_rpool_new_x(&SmRpoolRoot, 0); SM_TRY ... some code that can raise an exception ... SM_FINALLY sm_rpool_free_x(rpool); SM_END_TRY
Exceptions are reference counted. The SM_END_TRY macro contains a call to sm_exc_free, so you don't normally need to worry about freeing an exception after handling it. In the rare case that you want an exception to outlive an exception handler, then you increment its reference count by calling sm_exc_addref.SM_TRY /* code that can raise an exception */ ... SM_EXCEPT(exc, "*") /* catch all exceptions */ sm_exc_print(exc, stderr); SM_END_TRY
SM_TRY /* code that might raise end-of-file, or some other exception */ ... SM_EXCEPT(exc, "E:sm.eof") /* what to do if end-of-file is encountered */ ... SM_EXCEPT(exc, "*") /* what to do if some other exception is raised */ ... SM_END_TRY
In libsm, an exceptional condition is described by an object of type SM_EXC_T. An exception object is created by specifying an exception type and a list of exception arguments.
The exception arguments are an array of zero or more values. The values may be a mixture of ints, longs, strings, and exceptions. In the SM_EXC_T structure, the argument vector is represented by SM_VAL_T *exc_argv, where SM_VAL_T is a union of the possible argument types. The number and types of exception arguments is determined by the exception type.
An exception type is a statically initialized const object of type SM_EXC_TYPE_T, which has the following members:
The class is used to assign the exception type to one of a number of broad categories of exceptions on which an exception handler might want to discriminate. I suspect that what we want is a hierarchical taxonomy, but I don't have a full design for this yet. For now, I am recommending the following classes:
The name uniquely identifies the exception type. I recommend a string of the form library.package.detail.
Statically initialized exception values cannot contain any run-time parameters, so the normal case is to dynamically allocate a new exception object whenever you raise an exception. Before you can create an exception, you need an exception type. Libsm defines the following standard exception types.
If errno is ENOENT and filename is "/etc/mail/snedmail.cf", then the exception raised by the above code will be printed asfd = open(filename, O_RDONLY); if (fd == -1) sm_exc_raisenew_x(&SmEtypeOs, errno, "open", "%s", filename);
/etc/mail/snedmail.cf: open failed: No such file or directory
sm_exc_raisenew_x(&SmEtypeErr, "name lookup failed: %s", name);
Every new exception type needs a print function. The standard print function sm_etype_printf is all you need in the majority of cases. It prints the etype_printcontext string of the exception type, substituting occurrences of %0 through %9 with the corresponding exception argument. If exception argument 3 is an int or long, then %3 will print the argument in decimal, and %o3 or %x3 will print it in octal or hex.
In the following example, I will assume that your library package implements regular expressions, and can raise 5 different exceptions. When compiling a regular expression, 3 different syntax errors can be reported:
The obvious approach is to define 4 separate exception types. Here they are:
/* print a regular expression syntax error */ void rx_esyntax_print(SM_EXC_T *exc, SM_FILE_T *stream) { sm_io_fprintf(stream, "rx syntax error at character %d: %s", exc->exc_argv[0].v_int, exc->exc_type->etype_printcontext); } SM_EXC_TYPE_T RxSyntaxParen = { SmExcTypeMagic, "E:mylib.rx.syntax.paren", "i", rx_esyntax_print, "unbalanced parenthesis" }; SM_EXC_TYPE_T RxSyntaxBracket = { SmExcTypeMagic, "E:mylib.rx.syntax.bracket", "i", rx_esyntax_print, "unbalanced bracket" }; SM_EXC_TYPE_T RxSyntaxMissingArg = { SmExcTypeMagic, "E:mylib.rx.syntax.missingarg", "i", rx_esyntax_print, "missing argument for repetition operator" }; SM_EXC_TYPE_T RxRunCorrupt = { SmExcTypeMagic, "E:mylib.rx.run.corrupt", "", sm_etype_printf, "rx runtime error: compiled regular expression is corrupt" };
With the above definitions, you can raise a syntax error reporting an unbalanced parenthesis at string offset i using:
If i==42 then this exception will be printed as:sm_exc_raisenew_x(&RxSyntaxParen, i);
An exception handler can provide special handling for regular expression syntax errors using this code:rx syntax error at character 42: unbalanced parenthesis
SM_TRY ... code that might raise an exception ... SM_EXCEPT(exc, "E:mylib.rx.syntax.*") int i = exc->exc_argv[0].v_int; ... handle a regular expression syntax error ... SM_END_TRY
External requirements may force you to define an integer code for each error reported by your package. Or you may be wrapping an existing package that works this way. In this case, it might make sense to define a single exception type, patterned after SmEtypeOs, and include the integer code as an exception argument.
Your package might intercept an exception E generated by a lower level package, and then reclassify it as a different expression E'. For example, a package for reading a configuration file might reclassify one of the regular expression syntax errors from the previous example as a configuration file syntax error. When you do this, the new exception E' should include the original exception E as an exception parameter, and the print function for exception E' should print the high level description of the exception (eg, "syntax error in configuration file %s at line %d\n"), then print the subexception that is stored as an exception parameter.
A list of zero or more exception arguments follows the exception type; these are copied into the new exception object. The number and types of these arguments is determined by type->etype_argformat.
Note that there is no rpool argument to sm_exc_new_x. Exceptions are allocated directly from the heap. This is because exceptions are normally raised at low levels of abstraction and handled at high levels. Because the low level code typically has no idea of how or at what level the exception will be handled, it also has no idea of which resource pool, if any, should own the exception.
First, the SM_TRY clause is executed, then each SM_FINALLY clause is executed in sequence. If one or more of these clauses was terminated by an exception, then the first such exception is remembered, and the other exceptions are lost. If no exception was raised, then we are done. Otherwise, each of the SM_EXCEPT clauses is examined in sequence. and the first SM_EXCEPT clause whose pattern argument matches the exception (see sm_exc_match) is executed. If none of the SM_EXCEPT clauses matched the exception, or if there are no SM_EXCEPT clauses, then the remembered exception is re-raised.SM_TRY A block of code that may raise an exception. SM_FINALLY Cleanup code that may raise an exception. This code is guaranteed to be executed whether or not an exception was raised by a previous clause. You may have 0 or more SM_FINALLY clauses. SM_EXCEPT(e, pat) Exception handling code, which is triggered by an exception whose category matches the glob pattern 'pat'. The exception value is bound to the local variable 'e'. You may have 0 or more SM_EXCEPT clauses. SM_END_TRY
SM_TRY .. SM_END_TRY clauses may be nested arbitrarily.
It is illegal to jump out of a SM_TRY or SM_FINALLY clause using goto, break, continue, return or longjmp. If you do this, you will corrupt the internal exception handling stack. You can't use break or continue in an SM_EXCEPT clause; these are reserved for use by the implementation. It is legal to jump out of an SM_EXCEPT clause using goto or return; however, in this case, you must take responsibility for freeing the exception object.
The SM_TRY and SM_FINALLY macros contain calls to setjmp, and consequently, they suffer from the limitations imposed on setjmp by the C standard. Suppose you declare an auto variable i outside of a SM_TRY ... SM_END_TRY statement, initializing it to 0. Then you modify i inside of a SM_TRY or SM_FINALLY clause, setting it to 1. If you reference i in a different SM_FINALLY clause, or in an SM_EXCEPT clause, then it is implementation dependent whether i will be 0 or 1, unless you have declared i to be volatile.
int volatile i = 0; SM_TRY i = 1; ... SM_FINALLY /* the following reference to i only works if i is declared volatile */ use(i); ... SM_EXCEPT(exc, "*") /* the following reference to i only works if i is declared volatile */ use(i); ... SM_END_TRY