14. Exception Handling#
The high-level representation of exception handling in the IL closely follows
the C++ language specification. The data structures involved are defined in
il_def.h, and the routines to manage declarations and other support are in
decls.c, statements.c, and expr.c.
A global variable, exceptions_enabled, controls whether exception
processing is done. An error is issued if the flag is FALSE and the source
program attempts to use a syntactic feature that pertains to exception
handling. The default value of the exceptions_enabled flag is configurable
(see DEFAULT_EXCEPTIONS_ENABLED in lang_feat.h). The value may be
toggled by a command-line option (-x), except that its value may be TRUE
only in C++ mode and only when cfront-compatibility mode has not been selected.
Any actual implementation of exception handling will require a tradeoff between
portability and efficiency. The high-level representation of exception
handling (that is, the representation of the “unlowered” IL) is not explicit
about a number of details that an actual implementation will have to deal with,
notably finding the right handler, copying the throw object to the right
handler parameter, and calling the right set of destructors at the right time.
Therefore, though a portable implementation of exception handling is provided
(see lower_eh.c and routines provided for run-time support), back ends may
choose to replace it with an implementation adapted to the specific support
available on the target platform. IL lowering can instead be configured to do
partial lowering or no lowering of exception handling features.
This chapter describes the high-level support for exceptions and then summarizes the main features of the portable implementation.
14.1. Exception Specifications#
An exception specification that appears on a function declaration is
represented by an entry of type an_exception_specification. The
function refers to it by means of the exception_specification field of
its routine type supplement. When the function is declared with no
exception specification (meaning that any exception might be thrown), the
exception_specification pointer will be NULL. A dynamic throw
specification entry itself has a pointer to a linked list of entries of
type a_throw_spec_type_entry, each of which indicates a type of
exception that will be thrown from the given routine; that pointer is NULL
to indicate that no exceptions will be thrown. A noexcept specifier
points to a constant representing its operand or NULL if there is no such
operand.
scan_exception_specification, which is called from function_declarator,
scans an exception specification and allocates a throw specification entry
and its associated list of types, if any. Subsequently (i.e., after the
routine entry has been identified) the exception specification is either added
to the routine type supplement or checked against a prior declaration (see
check_exception_specification).
The used_in_exception flag is set for any type that appears in an exception
specification (as well as for any type that is the type of a throw
expression or appears in the exception declaration of a handler). Some
implementations of exception handling may use this flag in generating special
code for run-time type identification. In addition, if such a type is (or
“contains”) a class or enumeration type, that class or enumeration type must be
externally linked. See set_used_in_exception_or_rtti_flag (in
types.c).
14.2. try Blocks#
A try block is represented in the IL by a statement of kind
stmk_try_block, which points to a compound statement block and a linked
list of handlers. The latter are entries of type a_handler; each points to
a (possibly unnamed) parameter (the local variable to which the throw
object will be copied when the handler is invoked) and to an stmk_block
statement, the body of the handler; for a default handler (i.e., the
catch(...) case) the parameter pointer in the handler entry is NULL.
A try block is scanned by try_block_statement (in statements.c).
First compound_statement is called to scan the part of the try block
that precedes the first handler declaration. Then handler_declaration (in
decls.c) is called for each catch clause.
handler_declaration scans the exception declaration (calling
decl_specifiers and, if a named parameter is present, declarator).
Except for the default handler case, it creates a variable entry to
represent the parameter, whether it is explicitly declared or not. It also
does various checks to assure that the parameter’s type is valid (e.g., that it
is not an incomplete type) and calls set_used_in_exception_or_rtti_flag;
then it calls type_masks_handler_param_type (in types.c), to assure
that it is not masked by a previous handler declaration in the same try
block.
Finally, compound_statement is called to scan the handler’s body. Special
checking is done to assure that no label defined within the handler is
referenced by a goto from outside the handler.
in C++/CLI mode, there may be a “finally” block at the end, either in
addition to or in place of the catch clauses. It provides code that is
always executed when the try block is exited, whether normally, via a
goto or return, or via a thrown exception.
14.3. throw Expressions#
A throw expression is scanned by scan_throw_operator (in expr.c).
An enk_throw expression node is produced, which points to a dynamic
initialization entry that describes the object to be thrown (or rather, how to
copy it to the space for the object allocated by the run-time).
set_used_in_exception_or_rtti_flag is called for the type. A “rethrow”,
where no throw object is specified (i.e., throw;), is represented by an
enk_throw expression with a NULL pointer.
14.4. Portable Implementation#
One exception handling implementation provided in this release is a portable implementation that provides complete EH support while requiring few changes to existing back ends. Because this is a portable implementation there is a significant impact on the performance of the generated code when exception handling is enabled.
14.4.1. A walk-through of an EH example#
What follows is a rather contrived example that provides an overview of how the compiled code and the run-time system cooperate in throwing and handling exceptions. It is intended as an introduction, with a more detailed discussion of some of its features in subsequent sections.
Here is the example (with the numbers to the right corresponding to the text that follows):
#include <stdio.h>
struct A { ~A() { } };
struct B : public A { };
struct C { };
void f(int i) throw(A) { // 2, 3, 4
A a1; // 5
if (i==0) throw a1; // 6
B b1;
if (i==1) throw b1;
C c1;
if (i==2) throw c1;
}
int main() {
for (int i = 0; i < 3; ++i) {
printf("i=%0d\n", i);
try { // 1
f(i);
}
catch (A a) { printf("Caught an A\n"); } // 7, 8
catch (...) { printf("Caught something else\n"); }
} /* for */ // 9
}
Each time the
forloop inmainis executed and control enters thetryblock, prologue code is executed, which includes pushing an entry onto the “EH stack” to record that atryblock has been entered. (The EH stack is the principal data structure used to track the dynamic context in which exception handling events occur.)Each time
fis called, prologue code for the function pushes another entry onto the EH stack to record that a function with exception specifications has been entered. This entry provides the run-time system with a list of the types that may be thrown fromf.Moreover, since
fcontains objects (namely, automatic variables with destructors) that may need to be cleaned up if an exception is thrown during its execution, an additional entry is pushed onto the EH stack for object cleanup. [1] The array it points to is updated each time an object that is eligible for clean-up is constructed.The first clean-up region of
fis entered, and global variable__eh_curr_regionis updated accordingly. This routine has three such regions – the region in which no objects require clean-up, the region in whicha1requires clean-up, and the region in which botha1andb1require clean-up. [2]When
a1is constructed, the second clean-up region is entered. The address ofa1is entered in the “object address table”.The processing for the original source expression “
throw a1” involves three steps:__throw_setupis called to record information abouta1(most importantly, its type) and to return a pointer to a piece of storage into which it may be copied. [3]- The compiled code makes a copy of
a1to pass to__throw. Note that if classAhad a copy constructor, the generated code would include a call of__exception_startedprior to making the copy ofa1. __throwis called. First it walks the EH stack to identify the action that is required – in this case, it locates a handler that can catch anAobject. It then makes a second pass through the EH stack to do the appropriate clean-up, which in this case involves calling the destructor fora1and popping a couple of entries off the EH stack. The actions performed by__throwmay entail a good deal more complexity, as described in greater detail later.
After the clean-up is complete, control transfers back to the
tryblock inmain, and specifically to the first handler (since it is the one that catches anAobject), and then “Caught an A” is printed out.The epilogue code of the handler involves a call to
__free_thrown_object, to deallocate the storage into whicha1was copied.As the
tryblock is exited, the EH stack entry for the currenttryblock is popped, restoring the state to what it was originally, before the try block was entered.
On the second iteration of the for loop in main, the same sequence of
events takes place, except for two things. First, in function fb1 is
also constructed, so that the third clean-up region is entered and the address
of b1 is recorded in the object address table (along with that of a1);
that means that two objects are eligible for clean-up when an exception occurs.
Second, __throw must take into account that A is an accessible base
class of B in locating the handler that can catch b1.
Finally, on the third iteration of the loop, __throw detects a violation of
the exception specification for function f – c1 is the thrown object
this time, but C is not on the list of types that the function is allowed
to throw. As a result, unexpected is called, which results in a call to
terminate.
14.4.2. Run-Time Support#
14.4.2.1. The EH Stack#
A principal data structure used in coordinating the compiled code and the
run-time is the “EH stack”. It maintains information about the dynamic context
in which exception handling events occur. The EH stack is a linked list of
structures, and a new entry is pushed onto the stack each time a try block
is entered, each time a function with an exception specification is entered,
and each time a function is entered for which some kind of object clean-up must
be done if an exception is thrown.
14.4.2.2. Code generated for a try block#
When a try block is entered, a try block entry is pushed onto the EH
stack. The try block entry contains
- a
setjmpbuffer that will be used for transfer of control back to thetryblock when an exception is thrown; - a list of the
catchclauses associated with thetryblock; - other status information used to record the current state of the
tryblock.
The state information in the try block is initialized so that the run-time
knows that the try section of the block (as opposed to one of the handlers)
is currently being executed. When generating code for exception cleanup it is
sometimes necessary for the front end to generate try blocks that do not
appear in the original program. These are known as “internal” try blocks
and are distinguished from normal try blocks by the fact that the pointer
to the list of catch clauses is null.
14.4.2.3. Prologue code for a function with an exception specification#
When a function with an exception specification is entered, a throw
specification entry is pushed onto the EH stack. This entry provides a list of
the types that may be thrown (directly or indirectly) by the function. It is
removed from the EH stack when the function returns.
14.4.2.4. Prologue code for a function that may need clean-up if an exception is thrown#
When a function constructs objects that may need to be cleaned up in the event
of an exception, its prologue code pushes a function clean-up entry onto the EH
stack. Note that a given function may push both a throw specification
entry and a function clean-up entry.
A function clean-up entry will be pushed on the stack if the function creates
any automatic objects (including temporaries) of classes with destructors; one
is also generated when an object may be dynamically allocated using operator
new and the front end is configured to generate the operator new call
outside the constructor.
The function clean-up entry contains
- a pointer to an array of region entries that are used to describe the clean-up regions within the function
- a pointer to an array table that provides additional information for certain region entries
- a pointer to an object address table used to determine the address of objects referenced by the region and array tables
A region is associated with a specific sequence of clean-up actions that would be required should an exception occur. A new region is typically started whenever a destructable object comes into or goes out of scope.
The global variable __eh_curr_region records the region number currently
being executed. It is saved each time a new function entry or try block is
pushed on the stack and restored when the entry is popped.
The region entries contain the information needed to identify the object to be cleaned up and the kind of clean-up action required. The run-time must be able to determine the address of each object for which some kind of clean-up must be done. Ideally this would be done by putting stack offsets in the region entries. This can’t be done in the portable implementation, and so instead the region entries contain an index into the object address table. The object address table is allocated on the stack because its contents vary with each invocation of the function. As each region is entered, the appropriate entry in the object address table is filled in with the address of the object to be cleaned up. The address of the object address table is recorded in the function entry on the EH stack.
The region entries have been designed to be as compact as possible. Some
unusual circumstances require additional information not normally available in
the region entry. The additional information is needed when the region entry
describes an array or when the region entry describes a new allocation of a
class object whose operator delete function is of the two-operand variety.
In these cases the region entry contains an index into an array supplement and
the array supplement pointer is stored in the function entry on the EH stack.
14.4.2.5. Throwing an exception (generated code)#
When an exception is thrown, the run-time routine __throw_setup is called
to provide information to the run-time system about the object being thrown and
to allocate space into which the thrown object may be copied. A pointer to
this space is returned to the caller (i.e., the compiled code), which is
responsible for actually making the copy of the object. If copying the object
requires calling a copy constructor, __exception_started is called after
the evaluation of the object to be thrown, but before the copy constructor is
called. Once the object has been copied the run-time routine __throw is
called to complete the throw processing. If the thrown object requires
destruction, __throw_setup_dtor is called instead of __throw_setup. If
the thrown object is a multi-level pointer, __throw_setup_ptr is called
instead of __throw_setup.
14.4.2.6. Throwing an exception (__throw_setup)#
When __throw_setup is called, an entry is pushed onto the throw stack
(which is distinct from the EH stack). The throw stack contains information
about each of the thrown objects and is needed because throws may be nested
(i.e., a throw may occur while in a handler reached as the result of an earlier
throw). __throw_setup records the information provided by the caller in
the throw stack entry. This includes information about the type of the thrown
object.
The run-time system requires information about the types of objects that are
thrown and caught and also for types used in exception specifications. The
term “typeinfo” is used to refer to this type description information.
[4] It is not necessarily possible to generate only one typeinfo record
for a given type. [5] Consequently it must be possible to determine that
two typeinfo records refer to the same type. This is done by having each
typeinfo point to a “unique ID object”. The unique ID objects are
generated as tentative definitions to avoid potential multiple definition
problems. Two typeinfo records refer to the same type if they point to the
same unique ID object. The typeinfo information includes a list of the
base classes of the type. The base class list includes all direct base classes
as well as all virtual base classes, whether direct or indirect.
When checking exception specifications and looking for a catch clause that
can catch the thrown object, it is necessary to determine whether a given type
is a base class (and an accessible one) of the thrown type. This is done by
traversing the typeinfo base class list. The base class list includes
information about accessibility and ambiguity, which allows the runtime to
ignore non-public and ambiguous base classes.
In front end versions preceding 2.29, the runtime routine __throw_alloc was
called instead of __throw_setup. It had a similar function and a similar
interface, with one additional parameter, which (when non-NULL) was a string
indicating the accessibility for each base class, computed by the front end at
the point of the throw. This changed because the C++ language changed: As of
March 1995, the rules for throw were changed to allow catching only public
base classes, which eliminates the need for determining any special
accessibility at the point of throw.
14.4.2.7. Marking the initiation of the exception (__exception_started)#
An exception is considered “uncaught” after the expression in the throw
statement has been evaluated. If a copy constructor must be called to copy the
thrown object into the temporary copy used by the EH runtime, the copy
constructor must be called while the exception is considered uncaught. In
other words, if an exception is thrown during the evaluation of the expression
in the throw, that exception is handled normally. If, however, an exception is
thrown from a copy constructor called to copy an object to the temporary used
by the EH runtime (and control is transferred out of the copy constructor
because of the exception) terminate() must be called instead.
__exception_started is called to mark the point at which an exception is
considered uncaught. When the copy of the thrown object can be made without
calling a copy constructor __exception_started is not called, and the
exception is considered uncaught at the point at which __throw is called.
14.4.2.8. Throwing an exception (__throw)#
After the compiled code copies the thrown object into the space designated by
__throw_setup, it issues a call to __throw to complete the processing.
__throw makes two passes through the try stack. The first pass
determines what should occur as a result of the throw – transferring to a
specific handler, calling unexpected because an exception specification was
violated, or calling terminate (for any of several reasons). Once the end
result of the throw has been determined, a second pass is made through the EH
stack entries to perform any clean-up actions that are required.
During the first pass through the EH stack
check_exception_type_specifications is called for each try block entry
and each throw specification entry. Only try blocks that are executing
the try portion of the block (as opposed to a handler) are eligible to be
the destination of a throw. try blocks that are currently in handlers
are simply ignored. The search is terminated when a matching catch clause
is found or a violated exception specification is detected. When it is a
matching catch clause that is found, a pointer to the thrown object is
passed to the routine so that it may be adjusted for any derived-to-base-class
conversions that may be needed. If the matching catch clause is associated
with an internal try block, the search of the EH stack continues to
determine if a normal try block can be found. The search for a normal
try block that with a matching handler is used to determine whether
terminate() should be called.
According to the standard it is implementation-defined whether the stack is
unwound before terminate() is called. The runtime can be configured to
select either of the possible behaviors by setting the
UNWIND_STACK_BEFORE_CALLING_TERMINATE configuration flag. If this flag is
not set, terminate() would be called now if no matching handler was found.
In the second pass only the function clean-up entries are processed. The
function cleanup is called for each such entry. cleanup processes the
region entries associated with the function starting with the region number
specified by the caller. Each region entry contains a “flags” field that
controls how the region information is interpreted.
- If the “conditional flag” bit is set, the region entry describes an object that is conditionally constructed and must only be destroyed if a variable, whose address is determined from the next element in the region array, indicates the object was actually constructed.
- If the “new allocation” bit is set, the region entry describes an object allocated by
newthat must be deleted during clean-up. In this case the function pointer in the region entry that normally points to the destructor instead points to the delete routine to be called. - If the “VLA” bit is set, the region entry describes a variable length array, whose size is determined from the next element in the region array.
One of the fields in the region entry is the region number of the next entry to be processed. The end of the region list is designated by a reserved value in the next entry field.
If an exception occurs while constructing or destructing an array, the clean-up
operation must destroy the remaining portion of the partially constructed (or
destructed) array. To accomplish this there is a special kind of EH stack
entry that is only used by vec_new and vec_delete. When this entry is
encountered during the clean-up pass, __cleanup_vec_new_or_delete is called
to the required clean-up.
After all of the function clean-up entries have been processed, __throw
determines whether there are any clean-up actions required by the try block
associated with the handler to which control is to be transferred. If so,
cleanup is called to do the required clean-up operations.
Only after all the clean-up operations have been completed is the action performed that was determined by the first pass through the EH stack:
- If an exception specification was violated,
unexpectedis called. - If no handler was found,
terminate()is called (when theUNWIND_STACK_BEFORE_CALLING_TERMINATEconfiguration flag is set). - Otherwise, control is transferred to the handler. Global variable
__catch_clause_numberis set to the sequence number of thecatchclause to which control is to be transferred, and another global variable,__caught_object_address, is set to the address of the object to be used to initialize the handler parameter. The state information in thetryblock entry is updated to indicate that a handler is now being executed. Finally,longjmpis used to branch back to the appropriatetryblock.
14.4.2.9. In the handler#
The try block code uses __catch_clause_number to select the appropriate
handler, and then the handler parameter is initialized using
__caught_object_address: If the handler parameter is a reference, it is
initialized by setting a pointer to the global variable; otherwise, the object
pointed to by __caught_object_address is used to initialize the handler
parameter. __exception_caught is called after the handler parameter has
been initialized. This marks the end of the period during which an exception
is considered uncaught. __exception_caught is not called by the catch
clause associated with an internal try block. Instead, an internal try block
exits by calling __internal_rethrow, which calls __exception_caught
before performing the rethrow.
14.4.2.10. A rethrow from the handler#
A rethrow (a throw with no operand) may be executed anywhere within the
dynamic context of a handler. This has the effect of doing a nested throw of
the object on top of the throw stack. Note that it rethrows the thrown object
and not the caught object (the two can be different if a derived-to-base
conversion occurred or if the object was caught using ellipsis).
The handling of rethrow is almost identical to the handling of a normal throw. The only difference is that the thrown object is not copied. The throw stack entry for the rethrow points to the copy of the thrown object made when the original throw was done.
14.4.2.11. At the end of a handler#
At the end of each handler __free_throw_object is called to pop entries off
of the throw stack, call the destructor for the thrown object, and to free the
space used for copies of the thrown objects associated with the throw stack
entries being cleared.
14.4.2.12. Run-Time storage management#
The exception handling run-time dynamically allocates information used for book
keeping (e.g., throw stack entries) and for making copies of thrown objects.
The memory is allocated and freed using a stack discipline. The run-time
includes a static buffer that is used for this dynamically allocated memory.
If the static buffer is exhausted, additional memory is allocated using
malloc. If the run-time is unable to allocate the memory needed to handle
an exception, terminate is called.
14.5. Non-Portable Implementation#
The non-portable implementation would be more properly described as “turning off parts of the portable implementation so that a back end can do something better.” EDG does not provide a complete non-portable implementation (in IL lowering and the runtime), but our customers should be able to write one using the information provided by the front end.
The portable implementation is also known as the full-lowering implementation,
because it lowers everything related to exception handling to C, leaving
nothing related to exception handling in the IL. It is enabled by setting
DO_FULL_PORTABLE_EH_LOWERING to TRUE.
The next step down from that is the partial-lowering implementation, which is
enabled by setting GENERATE_EH_TABLES to TRUE. In that mode, the data
tables of the portable scheme are still generated, but the executable code is
rendered as exception-handling operations, so that a back end can choose a
different approach (e.g., not using setjmp, not maintaining a separate EH
stack).
The final step down is the no-lowering implementation, which is enabled by
having both DO_FULL_PORTABLE_EH_LOWERING and GENERATE_EH_TABLES FALSE.
In that mode, the data tables are not generated either, but the object lifetime
information is preserved so that the back end can use it to generate its own
version of the cleanup tables.
See the IL Lowering chapter for details on the constructs preserved and lowered under each alternative.