Simulate C++ Exception handling with setjmp/longjmp?

B

Bo Yang

Hi,
I have searched with Google, but only find some simple example of
this. Could anybody tell me is there any library or complete code used
for simulating all the Exception of OO language? Thanks!

Regards!
Bo
 
E

Eric Sosman

Bo said:
Hi,
I have searched with Google, but only find some simple example of
this. Could anybody tell me is there any library or complete code used
for simulating all the Exception of OO language? Thanks!

Many people have done it, many times. The broad
outline is always the same (`throw' is a longjmp(), `catch'
is a setjmp()), but the details tend to vary (e.g., is there
a `finally'?).

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. You get the same problem with fopen()
and fclose(), or with code that starts modifying a data
structure, calls inner(), and then finishes the modifications.
Lacking linguistic support, the burden of anticipating and
defending against this sort of thing falls squarely on the
programmer. And the sneakiest part is that the inner() that
throws no exceptions in Version 1.0 may start to throw them
in version 1.4, silently invalidating the work of all those
callers who reasoned "This is safe because inner() never
throws (up)."

Exceptions can be a Good Thing, and can significantly
ease the programming task. But they need full integration
with the language(s) used; if they're just pasted onto as
an afterthough they give the impression of being Good but in
fact can be Very Bad. Use them sparingly, and with caution.
 
B

Bertrand Mollinier Toublet

Eric said:
Many people have done it, many times. The broad
outline is always the same (`throw' is a longjmp(), `catch'
is a setjmp()), but the details tend to vary (e.g., is there
a `finally'?).

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. You get the same problem with fopen()
and fclose(), or with code that starts modifying a data
structure, calls inner(), and then finishes the modifications.
Lacking linguistic support, the burden of anticipating and
defending against this sort of thing falls squarely on the
programmer. And the sneakiest part is that the inner() that
throws no exceptions in Version 1.0 may start to throw them
in version 1.4, silently invalidating the work of all those
callers who reasoned "This is safe because inner() never
throws (up)."

Exceptions can be a Good Thing, and can significantly
ease the programming task. But they need full integration
with the language(s) used; if they're just pasted onto as
an afterthough they give the impression of being Good but in
fact can be Very Bad. Use them sparingly, and with caution.
Along the same line of reasoning, note (OP) that you can discipline
yourself into writing code that behaves similarly to what exceptions
would do by:
a) having all your functions return an error code (this becomes
unwieldy for "small" functions that are meant to return values of basic
types)
b) testing the error code after each invocation and zapping to
end-of-function and cleanup in case of error (one of the uses of goto I
allow myself)
c) doing this at all function levels


Illustrated:

enum statuses_ {
kSuccess = 0,
kError1 = 1,
kError2 = 2,
};
typedef enum statuses_ status;

status foo(int x) {
status rc = kSuccess;

/* do something with x */
/* also allocate some resources */

if (not_satisfying(x)) {
rc = kError1;
goto exit;
}

/* do something more with x */

if (still_not_satisfying(x)) {
rc = kError2;
goto exit;
}

/* do one last thing */
exit:
/* free resources */
return rc;
}

status bar(int y) {
status rc = kSuccess;

rc = compute_x_from_y(y, &x);
if (kSuccess != rc) {
log("failed to compute x from y");
goto exit;
}

rc = foo(x);
if (kSuccess != rc) {
log("failed to foo-ize x");
goto exit;
}

/* do some more stuff */
exit:
/* free resources */
return rc;
}

int main(void) {
int rc = 0;
int y = 0;

rc = bar(y);
if (kSuccess != rc) {
log("failed to bar-ize y");
goto exit;
}

/* etc. */
/* etc. */
/* etc. */
exit:
/* free resources as needed */
return rc;
}


As you can see, with such an organization, a failure deep below bubbles
up to the surface, interrupting further processing. In that sense, it
behaves pretty much like an exception would. As an added bonus, if your
log() function makes clever use of __FILE__ and __LINE__ (and, where
available __FUNCTION__), the consistent use of log will give you a nice
"stack trace" following a failure.

As illustrated by compute_x_from_y(), a function that computes an int
from an int takes the not so practical form of

status function(int in_val, int *out_val);

rather than the more flexible

int function(int in_val);
 
K

Keith Thompson

Bertrand Mollinier Toublet
Along the same line of reasoning, note (OP) that you can discipline
yourself into writing code that behaves similarly to what exceptions
would do by:
a) having all your functions return an error code (this becomes
unwieldy for "small" functions that are meant to return values of basic
types)
b) testing the error code after each invocation and zapping to
end-of-function and cleanup in case of error (one of the uses of goto I
allow myself)
c) doing this at all function levels


Illustrated: [snip]
int main(void) {
int rc = 0;
int y = 0;

rc = bar(y);
if (kSuccess != rc) {
log("failed to bar-ize y");
goto exit;
}

/* etc. */
/* etc. */
/* etc. */
exit:
/* free resources as needed */
return rc;
}
[...]

No comment on the technique, but I wouldn't use "exit" as the label
name, since there's a standard function by that name. (Perhaps the
point is that if you use this technique, you don't need the exit()
function, but if so it's an odd way to make that point.)
 
T

Tony

Bertrand Mollinier Toublet said:
Along the same line of reasoning, note (OP) that you can discipline
yourself into writing code that behaves similarly to what exceptions
would do by:
a) having all your functions return an error code (this becomes
unwieldy for "small" functions that are meant to return values of basic
types)
b) testing the error code after each invocation and zapping to
end-of-function and cleanup in case of error (one of the uses of goto I
allow myself)
c) doing this at all function levels


Illustrated:

enum statuses_ {
kSuccess = 0,
kError1 = 1,
kError2 = 2,
};
typedef enum statuses_ status;

status foo(int x) {
status rc = kSuccess;

/* do something with x */
/* also allocate some resources */

if (not_satisfying(x)) {
rc = kError1;
goto exit;
}

/* do something more with x */

if (still_not_satisfying(x)) {
rc = kError2;
goto exit;
}

/* do one last thing */
exit:
/* free resources */
return rc;
}

status bar(int y) {
status rc = kSuccess;

rc = compute_x_from_y(y, &x);
if (kSuccess != rc) {
log("failed to compute x from y");
goto exit;
}

rc = foo(x);
if (kSuccess != rc) {
log("failed to foo-ize x");
goto exit;
}

/* do some more stuff */
exit:
/* free resources */
return rc;
}

int main(void) {
int rc = 0;
int y = 0;

rc = bar(y);
if (kSuccess != rc) {
log("failed to bar-ize y");
goto exit;
}

/* etc. */
/* etc. */
/* etc. */
exit:
/* free resources as needed */
return rc;
}


As you can see, with such an organization, a failure deep below bubbles
up to the surface, interrupting further processing. In that sense, it
behaves pretty much like an exception would. As an added bonus, if your
log() function makes clever use of __FILE__ and __LINE__ (and, where
available __FUNCTION__), the consistent use of log will give you a nice
"stack trace" following a failure.

As illustrated by compute_x_from_y(), a function that computes an int
from an int takes the not so practical form of

status function(int in_val, int *out_val);

rather than the more flexible

int function(int in_val);

Yes, that's one potential compromise, but one that may be acceptable for the
elegant simplicity in avoidance of a setjmp/longjmp-based "exception"
implementation. In C++ though, there is another issue: constructors and
overloaded operators. They require C++'s multi-level propogation (exception)
machinery. While in C++, the above code can be made to eliminate the
possibility of the return code not being checked (another issue), in C that
is not possible as far as I know.

Tony
 
B

Bertrand Mollinier Toublet

Keith said:
No comment on the technique, but I wouldn't use "exit" as the label
name, since there's a standard function by that name. (Perhaps the
point is that if you use this technique, you don't need the exit()
function, but if so it's an odd way to make that point.)
Ah, good point.
Looks like a sed 's/exit:/some_other_identified:/g' is in order on my
code base :-/

At the same time, I have the following piece of DS9K non-compliant code
in there too:

unsigned char *my_memcpy(
unsigned char *dst,
const unsigned char *src,
size_t len) {
size_t i;

if (src > dst) { /* ouch! so much for portability... */
for (i = 0; i < len; i++) dst = src;
}
else {
for (i = len; i > 0; i--) dst[i - 1] = src[i - 1];
}

return dst;
}

Huk huk huk. I'd wager that as long as this is there, I can also call my
exit label "exit".

Please, don't ask why $DAYJOB has me rewrite my own memcpy...
 
F

Flash Gordon

Bertrand Mollinier Toublet wrote:

At the same time, I have the following piece of DS9K non-compliant code
in there too:

unsigned char *my_memcpy(
unsigned char *dst,
const unsigned char *src,
size_t len) {
size_t i;

if (src > dst) { /* ouch! so much for portability... */
for (i = 0; i < len; i++) dst = src;
}
else {
for (i = len; i > 0; i--) dst[i - 1] = src[i - 1];
}

return dst;
}

Huk huk huk. I'd wager that as long as this is there, I can also call my
exit label "exit".

Please, don't ask why $DAYJOB has me rewrite my own memcpy...


Perhaps because they don't know that memmove exists? Which is what it
looks at first glance to me you are implementing.
 
D

Daniel Pirch

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.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,432
Messages
2,571,682
Members
48,796
Latest member
Greg L.

Latest Threads

Top