Implementing Exception Propagation (N2179)

Sebastian Redl sebastian.redl@getdesigned.at
Mon May 19 14:48:00 GMT 2008


Hi,

I've decided to implement the exception propagation proposal (N2179) for 
GCC. Since this is my first modification of GCC, I'd like to get some 
feedback on my ideas before getting down into the code. (Many of you 
will probably know the stuff I'm posting already, but it's a good way of 
organizing my thoughts, so I'll post it anyway. Besides, there is no 
good reference on the web that explains this stuff.)

I decided to implement this proposal after seeing an implementation for 
Visual C++ posted to the Boost list. My attempts all tried to get by 
without actually modifying GCC or libsupc++, but I realized that this is 
impossible.

To recap the situation:

N2179 defines four additions to the standard library:

namespace std {
  typedef unspecified exception_ptr;
  exception_ptr current_exception();
  void rethrow_exception( exception_ptr p );
  template <class E> exception_ptr copy_exception(E e);
}

exception_ptr is a type that refers to an arbitrary exception (i.e. an 
arbitrary object). It can be copied freely, especially between threads. 
(Transporting an exception from one thread to another is the original 
motivation for the proposal.)

current_exception() returns an exception_ptr that refers to the 
exception currently handled, or to a copy of it.

rethrow_exception(p) essentially does throw *p. The standard text says, 
"throws the exception to which p refers", but I believe throwing a copy 
is not an observable difference. This is important, because otherwise 
the proposal cannot be reasonably implemented in GCC.

copy_exception(e) does
try {
  throw e;
} catch(...) {
  return current_exception();
}
but possibly more efficient.



The situation in GCC is as follows.

Exceptions are thrown through libunwind. The unwind library defines a 
number of functions that are used for language-independent stack 
unwinding. This allows foreign exceptions to unwind through C++ code and 
vice versa. It also allows uncatchable unwinding, e.g. for POSIX thread 
cancellation, while still cleaning up properly.

The unwind library carries a single state object along. This object 
consists of a common "header" (it's actually a footer in libsupc++'s 
implementation) and a language-specific part. The language-specific part 
in C++ consists of the exception object and some management data.

The statement
throw foo();
is translated as follows:

First, space is allocated for the exception state by 
__cxa_allocate_exception().
Then, the thrown object is copied (or constructed) into the exception state.
Finally, __cxa_throw() is called. It is passed three parameters:
__cxa_throw(exception_state_ptr, &typeid(exception_class),
    has_trivial_destructor(exception_class) ? 0 : 
&exception_class::~exception_class));

__cxa_throw properly initializes the exception state, including storing 
the type info and the destructor in there, and calls upon the unwind 
library to do its thing.

Two elements of the exception state are really of interest here: 
handlerCount and nextException.

nextException is part of a singly linked list rooted in the 
thread-specific __cxa_eh_globals structure, threading through all 
exceptions that are currently being handled.

try {
  throw ex();
} catch(ex &e1) {
  // globals.caughtExceptions == &e1
  // e1.nextException == 0
  try {
    throw ex();
  } catch(ex &e2) {
    // globals.caughtExceptions == &e2
    // e2.nextException == &e1
    // e1.nextException == 0
  }
}

A rethrow statement always rethrows the exception referred to by 
globals.caughtException. When an exception is no longer handled, it is 
unlinked and the next exception becomes the current one. Because catch 
statements are strictly nested and only the most recent exception can be 
rethrown, this works nicely.

How does the system know when an exception is no longer handled? That's 
where handlerCount comes in.

try {
  throw ex();
} catch(ex &e) {
  // e.handlerCount == 1 - it's being handled
  try {
    throw;
  } catch(ex &e2) {
    // &e == &e2
    // e.handlerCount == 2 - there's two catch blocks handling it
  }
  // handlerCount is decremented to 1, but it's still being handled
}
// handlerCount reaches 0. The exception is unlinked and destroyed.

However, there's a special case. When an exception is rethrown, its 
handlerCount is negated. If the handlerCount reaches 0 coming from the 
negative side, it is only unlinked, not destroyed.

try {
  throw ex();
} catch(ex &e) {
  // handlerCount == 1
  throw; // handlerCount == -1
} // leave catch, the exception is no longer handled
// handlerCount is "decremented" (put closer to zero) to 0
// because it reached 0 from the negative side, it is unlinked, but not 
destroyed
// because the exception propagates and is still needed




OK, so far what we have. Now for what we want.

My initial assumption was that, because the exception state is on the 
heap, it should be possible to directly reuse it. In other words, 
current_exception() would return a pointer to the exception state, and 
rethrow_exception() would rethrow exactly that state. This assumption 
was wrong. There are way too many weird things you can do. For example:

try {
  throw ex();
} catch(...) {
  exception_ptr p1 = current_exception();
  try {
    throw ex();
  } catch(...) {
    exception_ptr p2 = current_exception();
    try {
      rethrow_exception(p1);
    } catch(...) {
      // globals.caughtExceptions list should be p1 -> p2 -> p1, but 
that's not possible
    }
  }
}

Or another example:

exception_ptr g_p;
condition caught;

void t2()
{
  try {
    throw foo();
  } catch(...) {
    g_p = current_exception();
    caught.signal();
    throw;
  }
}

void t1()
{
  thread th(&t2);
  caught.wait();
  rethrow_exception(g_p);
}

Given bad enough timing, the same _Unwind_header would be used by both 
threads' unwinding, which is surely not valid.


Another issue that I discovered was the difficulty of keeping the 
exception alive. The current mechanism knows only two reasons why an 
exception object should be alive: it is being thrown, or it is being 
handled. "It is being referred to by an exception_ptr" is not 
considered. (Duh!)
The most trivial example of this is this (assuming a very stupid 
exception_ptr):

exception_ptr p;
try {
  throw foo();
} catch(...) {
  p = current_exception();
} // exception destructed here
// p is dangling here

My first implementation thought it could just add to the handlerCount. 
However, if you do this, the exception is not unlinked, leading to very 
bad behaviour. Most apparent, a rethrow statement outside of a handler 
would work, and would throw the exception referred to by an 
exception_ptr - until the exception_ptr went out of scope during 
unwinding and took the exception object with it.
The handlerCount was not a place where I could put my reference count.

My second implementation, lacking a place to put a reference count, 
simply negated handlerCount to prevent the end of the catch handler from 
destroying the exception, and returned a shared_ptr to the exception, 
with a custom deleter. This deleter would see if the handlerCount was 
still non-zero and set it positive in this case, or if it was zero, 
delete the exception.

This approach had several problems. The most obvious one was this:

exception_ptr p1, p2;
try {
  throw foo();
} catch(...) {
  p1 = current_exception();
  p2 = current_exception();
  // p1 and p2 know nothing of each other, they have separate reference 
counts
}
// handlerCount is 0
// now leaving scope of p1 and p2
// p1 destructs, destroys exception object
// p2 dangles, tries to destroy exception object again

A more subtle problem was this:

exception_ptr p;
try {
  throw foo();
} catch(...) {
  p = current_exception();
  // handlerCount == -1
  try {
    rethrow_exception(p);
  } catch(...) { // handlerCount == 2 (yes, positive)
  }
  // handlerCount == 1
}
// handlerCount == 0, exception_ptr dangles.

It was clear that I needed an embedded count. I finally found the place 
where to keep it: the cleanup function. There is a pointer to a cleanup 
function in the common unwind header. This is so that the exception 
mechanism can destroy foreign exceptions just as native exceptions.

I replaced the pointer to the libsupc++ cleanup function with a pointer 
to my own function. This function was actually an object. It started 
with some bytes that were executable machine code. This code set the 
handlerCount of the exception object to 0 (something the catch leave 
handler neglects to do if it wants to destroy the object) and then 
returned. It did not destroy the exception object. This solved the 
subtle problem above, because I no longer needed to modify the 
handlerCount at all.

After the program code I placed my own reference count, and the pointer 
to the real cleanup function, so I could restore it when all pointers 
went out of scope.

Now that I had an "internal" reference count, I could use a 
boost::intrusive_ptr as the exception_ptr type.


However, this did not solve all problems. For one, I had to leak the 
exception object under some circumstances, because I could not 
distinguish between destruction because the pointed-to exception was 
being rethrown and some different exception being thrown. For safety's 
sake, I could not destroy the object then.

And for another, I still had the problem originally outlined. There is 
no way to implement this proposal without creating copies of the 
exception object. And that led me here.



To implement the proposal, I believe that these changes are necessary:

To libsupc++:

1) Extend struct __cxa_exception with two fields:
1a) A pointer to the exception object's copy constructor, so that a copy 
can be created.
1b) A reference count, counting the number of exception_ptrs pointing to 
the object, plus one if the exception is being thrown. This makes the 
negative handlerCount thing unnecessary.
1c) Optionally, add a mutex to the struct, because the reference 
counting must atomically compare two reference counts at the same time. 
Alternatively, the mutex could be global for all exceptions, since the 
time spent there ought to be minimal.

2) Implement reference counting. All reference counting must be thread-safe.
2a) __cxa_throw and __cxa_rethrow must increment the reference count. 
__cxa_rethrow no longer negates handlerCount.
__cxa_throw takes a fourth parameter: void (*copy_constructor)(void*, 
const void*). It stores this in the field of 1a.
2b) __cxa_begin_catch must decrement the reference count. Special 
handling for a negative handlerCount is removed.
2c) Implement exception_ptr as an equivalent of boost::intrusive_ptr, 
using the reference count.
2d) If exception_ptr's destruction causes the reference count to reach 
zero and handlerCount == 0 too, destroy the exception.
2e) __cxa_end_catch must remove the special handling of a negative 
handlerCount. handlerCount is simply decremented. If it reaches zero, 
the exception is removed from the caughtExceptions linked list. If the 
reference count is also zero, destroy the exception.

3) Implement the three functions of N2179:
3a) current_exception() is trivial. It constructs and returns an 
exception_ptr referring to *globals->caughtExceptions. If that pointer 
is null, it returns the null pointer. (It also returns the null pointer 
if the current exception is foreign.)
3b) rethrow_exception() calls __cxa_allocate_exception() to create space 
for a new exception object. It copies the relevant state and copies the 
exception object using the stored copy constructor. It then calls 
_Unwind_RaiseException.
If, and only if, the passed exception_ptr is the only reference to the 
exception (handlerCount == 0 and the reference count == 1), 
rethrow_exception() may simply throw the existing exception object.
3c) While the simple implementation of copy_exception works, it's 
inefficient. It would be more efficient to call 
__cxa_allocate_exception() and initialize the resulting object, then 
return an exeption_ptr referring to it. However, this requires compiler 
support, as there is no library way of obtaining the destructor and copy 
constructor of a type.

To the C++ frontend itself:

4) Pass the copy constructor to __cxa_throw.

This is somewhat tricky. The destructor is simple. The simple signature 
void (void*) always fits.

In the simple case, this would be a relatively easy change to except.c, 
to build_throw(). It's not that simple, though.

A constructor taking more than one argument can be a copy constructor, 
as long as all but the first argument have defaults. This is a copy 
constructor:
foo(const foo &, int = 0);
Therefore, it may be necessary to construct a stub that fits the 
signature and calls the copy constructor with default arguments as 
required, then pass that stub to __cxa_throw.



These changes are ABI-breaking. Catching an exception thrown by code 
compiled with an earlier GCC would result in invalid accesses by 
__cxa_begin_catch and __cxa_end_catch at least; also, __cxa_rethrow 
wouldn't work. Throwing an exception *into* such code might work, but 
the code might delete the exception object even though there's 
exception_ptrs referring to it.

As such, the exception class would have to be changed. This would make 
interoperation with old code at least partially possible - the 
exceptions would be relegated to the status of foreign exceptions, could 
be caught with catch(...) only, and could not be nested. Possible 
classes are "GNU2C++\0" or "2NUCC++\0".


Comments, suggestions, critique, and ideas on the frontend modifications 
are highly welcome.

Sebastian Redl



More information about the Libstdc++ mailing list