This is the mail archive of the libstdc++@gcc.gnu.org mailing list for the libstdc++ project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

Implementing Exception Propagation (N2179)


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


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]