Eric said:
My own impression of the frameworks I've seen (and on
occasion used) is that they can easily short-circuit code
that releases resources or otherwise cleans up. Code that
calls malloc(), calls a lower-level function inner(), and
then calls free() may never get to the free() if inner()
throws an exception.
This problem also exists, and is often ignored, in languages that
natively support exceptions; if you don't use RAII or finally-blocks in
C++ or Java, you can create resource leaks in these languages just as
easily.
A nice way to deal with this in C is to use a cleanup stack, consisting
of nodes like
struct cleanupnode {
struct cleanupnode *next;
void (*handler)(void *arg, int exception);
void *arg;
};
When an exception is thrown, you just have to unwind this stack and call
all the handlers. To catch exceptions, the bottom element of the stack
would contain a handler that longjumps back to the try-catch-block;
otherwise the program terminates:
void throw(int exception, struct cleanupnode *cleanupstack)
{
while (cleanupstack) {
cleanupstack->handler(cleanupstack->arg, exception);
cleanupstack = cleanupstack->next;
}
fprintf(stderr, "Unhandled exception: %d", exception);
exit(EXIT_FAILURE);
}
The stack nodes can be stored as local variables in nested scopes, for
example, you can keep a pointer called 'cleanupstack' to the top of the
stack and then use macros like
#define CLEANUP_PUSH(handler, arg) { \
struct cleanupnode cleanupnode = {cleanupstack,(handler),(arg)}; \
struct cleanupnode *cleanupstack = &cleanupnode;
#define CLEANUP_POP(execute) \
if (execute) cleanupnode.handler(cleanupnode.arg, 0); }
Then you can just call CLEANUP_PUSH using a suitable cleanup handler
after allocating a resource, call throw() if an error occurs, and
CLEANUP_POP to remove the handler from the stack in the end (possibly
calling the handler at the same time). You can even pass the
cleanupstack pointer to other functions. For example:
void function(struct cleanupnode *cleanupstack)
{
char *x = malloc(42);
if (!x) throw(some_exception_number, cleanupstack);
CLEANUP_PUSH(malloc_cleanup, x)
something_t *something = create_something(333, cleanupstack);
CLEANUP_PUSH(cleanup_something, something)
another_function(x, something, cleanupstack);
CLEANUP_POP(1) /* free something */
CLEANUP_POP(1) /* free x */
}
Of course you will have to write all these cleanup handlers, but they
are just simple one-liners.
Using a cleanup stack like that can be a better alternative to the usual
if-error-goto-fail error handling, which becomes complicated and
error-prone if you have a lot of resources to free and call a lot of
functions that could fail.