ABI modification for exception propagation

Sebastian Redl sebastian.redl at getdesigned.at
Sun May 25 22:25:29 UTC 2008


Hi,

I experimentally implemented N2197 "Exception Propagation" in GCC and 
came to the conclusion that the current exception handling ABI 
specification is insufficient. It needs to be changed in order to make 
exception propagation possible.

I wrote up a proposal for the changes. My current implementation doesn't 
quite follow this proposal, since I came up with some of the ideas while 
writing the document, but I will change it.

The proposal is copied below.

Sebastian Redl

--------------------------------

With the integration of N2197 "Exception Propagation" into the C++0x working
paper, the C++ ABI described at 
http://www.codesourcery.com/cxx-abi/abi-eh.html
has unfortunately become insufficient. It describes a model where exceptions
are bound to a single thread and cannot be copied.

In order to allow implementation of exception propagation, I propose the
following changes to the ABI. These changes have been successfully 
implemented
in the G++ compiler.

The Level I ABI remains unchanged.


Level II: C++ ABI

The current C++ ABI is structured like this: the exception data lies in 
a single
block of memory, starting with C++-specific information (the __cxa_exception
object), then the general unwind header (the unwindHeader field of the
__cxa_exception object), and finally the actual C++ exception object. 
Neither
the __cxa_exception object nor the unwind header can be used concurrently in
more than one thread. Therefore, the exception data must be copied in 
order to
implement N2197.
Since the exception data contains the exception object, copying the data 
implies
copying the exception object, unless the object is separated from the 
data. Both
approaches are possible. In this proposal, I've chosen the separation 
approach,
because it requires fewer changes to the compiler itself. In the old 
model, a
copy constructor was not necessary. Were the new model to require 
copying of the
exception object, a copy constructor would have to be stored in the 
data. In the
case of G++, this would mean that a copy constructor would have to be 
looked up
and (because of inconsistent signatures allowed for copy constructors) 
possibly
even generated.

This proposal therefore suggests splitting the data transported for a C++
exception into two parts. The first part is the current __cxa_exception 
object,
slightly changed. There is one such object for every exception actually 
thrown.
The second part is the actual exception object, plus some type information
(the exceptionType and exceptionDestructor fields of the current 
__cxa_exception
structure). This block is reference-counted and shared between all 
exceptions
that are really the same exception object, as well as all exception_ptrs
referring to the exception.

Because of the incompatible changes with earlier versions, the proposal 
suggests
changing the language-specific exception identifier to C+09. 
Implementations may
treat legacy C++ exceptions as foreign, or they may branch on the 
exception type
in order to support both.

2.2.1 C++ Exception Objects

The __cxa_exception is renamed and looks like this in the new scheme:

    struct __cxa_unwind_header {
        void *             exceptionObject;

        unexpected_handler unexpectedHandler;
        terminate_handler  terminateHandler;
        __cxa_exception *  nextException;
        int                handlerCount;

        int                handlerSwitchValue;
        const char *       actionRecord;
        const char *       languageSpecificData;
        void *             catchTemp;
        void *             adjustedPtr;

        _Unwind_Exception  unwindHeader;
    };

The name is changed because the object no longer represents the entire
exception, but really a language-specific extension of the _Unwind_Exception
structure.

The new field exceptionObject points to the actual exception object.
The exception object is prepended the type information and reference count.

    struct __cxa_exception_info {
        std::size_t        referenceCount;
        std::type_info *   exceptionType;
        void (*exceptionDestructor) (void *):
    };

The memory layout of a complete exception looks like this:

+--------------------------------+-------------------+
| __cxa_unwind_header            | _Unwind_Exception |
+--------------------------------+-------------------+
 |
 +---------------------+
                       |
                      \|/
+----------------------+-----------------------------+
| __cxa_exception_info | actual exception object     |
+----------------------+-----------------------------+

Note that __cxa_unwind_header::exceptionObject points at the start of the
exception object, not the additional information. This, plus the fact that
exceptionObject is the first field of __cxa_unwind_header, allows the 
compiler
to treat a pointer to a __cxa_unwind_header as a double pointer to the 
exception
object.

The referenceCount field of __cxa_exception_info counts the number of both
__cxa_unwind_header and std::exception_ptr objects referring to the 
exception.
Working with this count must happen in a thread-safe manner.

The meaning of all other fields remains unchanged.

The convention that a __cxa_exception pointer points to behind the header is
abandoned. Since the exception object is no longer there, the header 
object can
be freely extended in this direction, making the convention no longer 
useful.

The convention is instead adopted for __cxa_exception_info pointers.


2.4.1 Overview of Throw Processing

__cxa_allocate_exception cannot return a pointer to the exception 
object, since
this would mean that the unwind header is unreachable. It is therefore 
changed
to return a pointer to the beginning of the __cxa_unwind_header. Since 
the first
field of that structure is a pointer to the exception object, the 
generated code
need merely add a dereference operation. The produced code should look like
this:

    // Allocate -- never throws:
    temp1 = __cxa_allocate_exception(sizeof(X));

    // Construct the exception object:
    #if COPY_ELISION
        [evaluate X into *temp1]
    #else
        [evaluate X into temp2]
        copy-constructor(*temp1, temp2)
        // Landing Pad L1 if this throws
    #endif

    // Pass the exception object to unwind library:
    __cxa_throw(temp1, type_info<X>, destructor<X>); // Never returns

    // Landing pad for copy constructor:
    L1: __cxa_free_exception(temp1) // never throws


2.4.2 Allocating the Exception Object

Memory for the exception is still allocated by __cxa_allocate_exception.
However, the return value now points to the start of the __cxa_unwind_header
instead of past the end.
__cxa_allocate_exception allocates memory for both the 
__cxa_unwind_header and
the __cxa_exception_info with the associated exception. The 
__cxa_unwind_header
refers to the __cxa_exception_info, whose reference count is 1.

However, there is now also the need to allocate a __cxa_unwind_header for an
existing __cxa_exception_info. A new function is specified for this:

    __cxa_unwind_header*
    __cxa_allocate_unwind_header(void *obj) throw();

obj points behind a __cxa_exception_info; the allocated 
__cxa_unwind_header's
exceptionObject contains this value unchanged.
The __cxa_exception_info's reference count is increased by 1.

Finally, to facilitate an implementation of copy_exception that is more
efficient than throwing and catching an exception, a third function is 
supplied:

    void* __cxa_allocate_exception_object(size_t thrown_size) throw();

This function allocates a __cxa_exception_info and thrown_size bytes of
additional space. All fields of the object are set to zero/null.
It returns a pointer behind the __cxa_exception_info, to the start of the
additional space.

The three allocation functions have their counterparts in only two 
deallocation
functions. One is __cxa_free_exception, which has slightly changed 
semantics.

__cxa_free_exception takes a pointer to the start of a __cxa_unwind_header,
instead of the end. It deallocates the object and decreases the 
reference count
of the pointed-to __cxa_exception_info by 1. If the reference count 
drops to zero, it calls __cxa_free_exception_object.

    void __cxa_free_exception_object(void *obj) throw();

__cxa_free_exception_object takes a pointer to the end of a
__cxa_exception_info. It calls the destructor for the exception object and
frees the memory unconditionally.


2.4.3 Throwing the Exception Object

__cxa_throw remains nearly unchanged, except for two details: the first
parameter points to the start of the __cxa_unwind_header instead of the 
end, and
tinfo and dest are stored in the __cxa_exception_info.


2.5.3 Exception Handlers

The semantics of __cxa_begin_catch and __cxa_end_catch remain unchanged. 
Note
that __cxa_end_catch does not destroy the exception object unless its 
reference
count drops to zero.

With std::current_exception() available, it would be possible to add an
ABI-specific extension to exception_ptr with equivalent functionality as
__cxa_current_exception_type(). For example:

    exception_ptr ep = current_exception();
    std::type_info *ti = ep.__cxa_exception_type();


2.5.4 Rethrowing Exceptions

The semantics of __cxa_rethrow are unchanged. std::rethrow_exception is
described separately.


2.5.5 Finishing and Destroying the Exception

An exception object is considered unreferenced:
* When all exceptions using the object are finished AND
* When there are no more exception_ptr instances referring to the object.

The actual exception object must not be destroyed before it is unreferenced,
whereas the unwind header will be freed when the exception is finished.

When the exception is finished, __cxa_free_exception shall be called, 
but the
destructor is not explicitly called. This is left to
__cxa_free_exception_object.


2.7 Exception Propagation

Since exception_ptr provides a mechanism to influence the lifetime of
exceptions, I consider it part of this machinery and will describe it in 
short
here.

exception_ptr is a smart pointer. Its only data member is a pointer past the
__cxa_exception_info it refers to. Upon construction, exception_ptr 
increments
the reference count. Upon destruction, it decrements it, and calls
__cxa_free_exception_object if the count drops to zero. exception_ptr 
does not
manage a __cxa_unwind_header.

std::current_exception() shall construct an exception_ptr referring to the
exceptionObject field of the __cxa_unwind_header that is the head of the 
current
thread's exception chain. If there is no such exception, or if that 
exception
is foreign, current_exception returns the null pointer. Note that, if the
implementation switches on the exception class to handle legacy code, it 
still
cannot return an exception_ptr for such exceptions.
Because the function does not allocate memory, it will never return a 
pointer
to a std::bad_alloc object.

std::rethrow_exception() shall call __cxa_allocate_exception_wrapper() 
and pass
the pointer of the exception_ptr. It then behaves like __cxa_throw: it 
stores
the unexpected and terminate handlers, sets the exception_class field,
increments the uncaught_exception flag and calls _Unwind_RaiseException.




More information about the cxx-abi-dev mailing list