This is the mail archive of the
libstdc++@gcc.gnu.org
mailing list for the libstdc++ project.
Implementing Exception Propagation (N2179)
- From: Sebastian Redl <sebastian dot redl at getdesigned dot at>
- To: libstdc++ at gcc dot gnu dot org
- Date: Sat, 17 May 2008 22:54:40 +0200
- Subject: 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