203 lines
7.6 KiB
Plaintext
203 lines
7.6 KiB
Plaintext
Meeting notes: Implementation idea: Exception Handling in C++/Java
|
|
|
|
The 5/18/01 meeting discussed ideas for implementing exceptions in LLVM.
|
|
We decided that the best solution requires a set of library calls provided by
|
|
the VM, as well as an extension to the LLVM function invocation syntax.
|
|
|
|
The LLVM function invocation instruction previously looks like this (ignoring
|
|
types):
|
|
|
|
call func(arg1, arg2, arg3)
|
|
|
|
The extension discussed today adds an optional "with" clause that
|
|
associates a label with the call site. The new syntax looks like this:
|
|
|
|
call func(arg1, arg2, arg3) with funcCleanup
|
|
|
|
This funcHandler always stays tightly associated with the call site (being
|
|
encoded directly into the call opcode itself), and should be used whenever
|
|
there is cleanup work that needs to be done for the current function if
|
|
an exception is thrown by func (or if we are in a try block).
|
|
|
|
To support this, the VM/Runtime provide the following simple library
|
|
functions (all syntax in this document is very abstract):
|
|
|
|
typedef struct { something } %frame;
|
|
The VM must export a "frame type", that is an opaque structure used to
|
|
implement different types of stack walking that may be used by various
|
|
language runtime libraries. We imagine that it would be typical to
|
|
represent a frame with a PC and frame pointer pair, although that is not
|
|
required.
|
|
|
|
%frame getStackCurrentFrame();
|
|
Get a frame object for the current function. Note that if the current
|
|
function was inlined into its caller, the "current" frame will belong to
|
|
the "caller".
|
|
|
|
bool isFirstFrame(%frame f);
|
|
Returns true if the specified frame is the top level (first activated) frame
|
|
for this thread. For the main thread, this corresponds to the main()
|
|
function, for a spawned thread, it corresponds to the thread function.
|
|
|
|
%frame getNextFrame(%frame f);
|
|
Return the previous frame on the stack. This function is undefined if f
|
|
satisfies the predicate isFirstFrame(f).
|
|
|
|
Label *getFrameLabel(%frame f);
|
|
If a label was associated with f (as discussed below), this function returns
|
|
it. Otherwise, it returns a null pointer.
|
|
|
|
doNonLocalBranch(Label *L);
|
|
At this point, it is not clear whether this should be a function or
|
|
intrinsic. It should probably be an intrinsic in LLVM, but we'll deal with
|
|
this issue later.
|
|
|
|
|
|
Here is a motivating example that illustrates how these facilities could be
|
|
used to implement the C++ exception model:
|
|
|
|
void TestFunction(...) {
|
|
A a; B b;
|
|
foo(); // Any function call may throw
|
|
bar();
|
|
C c;
|
|
|
|
try {
|
|
D d;
|
|
baz();
|
|
} catch (int) {
|
|
...int Stuff...
|
|
// execution continues after the try block: the exception is consumed
|
|
} catch (double) {
|
|
...double stuff...
|
|
throw; // Exception is propogated
|
|
}
|
|
}
|
|
|
|
This function would compile to approximately the following code (heavy
|
|
pseudo code follows):
|
|
|
|
Func:
|
|
%a = alloca A
|
|
A::A(%a) // These ctors & dtors could throw, but we ignore this
|
|
%b = alloca B // minor detail for this example
|
|
B::B(%b)
|
|
|
|
call foo() with fooCleanup // An exception in foo is propogated to fooCleanup
|
|
call bar() with barCleanup // An exception in bar is propogated to barCleanup
|
|
|
|
%c = alloca C
|
|
C::C(c)
|
|
%d = alloca D
|
|
D::D(d)
|
|
call baz() with bazCleanup // An exception in baz is propogated to bazCleanup
|
|
d->~D();
|
|
EndTry: // This label corresponds to the end of the try block
|
|
c->~C() // These could also throw, these are also ignored
|
|
b->~B()
|
|
a->~A()
|
|
return
|
|
|
|
Note that this is a very straight forward and literal translation: exactly
|
|
what we want for zero cost (when unused) exception handling. Especially on
|
|
platforms with many registers (ie, the IA64) setjmp/longjmp style exception
|
|
handling is *very* impractical. Also, the "with" clauses describe the
|
|
control flow paths explicitly so that analysis is not adversly effected.
|
|
|
|
The foo/barCleanup labels are implemented as:
|
|
|
|
TryCleanup: // Executed if an exception escapes the try block
|
|
c->~C()
|
|
barCleanup: // Executed if an exception escapes from bar()
|
|
// fall through
|
|
fooCleanup: // Executed if an exception escapes from foo()
|
|
b->~B()
|
|
a->~A()
|
|
Exception *E = getThreadLocalException()
|
|
call throw(E) // Implemented by the C++ runtime, described below
|
|
|
|
Which does the work one would expect. getThreadLocalException is a function
|
|
implemented by the C++ support library. It returns the current exception
|
|
object for the current thread. Note that we do not attempt to recycle the
|
|
shutdown code from before, because performance of the mainline code is
|
|
critically important. Also, obviously fooCleanup and barCleanup may be
|
|
merged and one of them eliminated. This just shows how the code generator
|
|
would most likely emit code.
|
|
|
|
The bazCleanup label is more interesting. Because the exception may be caught
|
|
by the try block, we must dispatch to its handler... but it does not exist
|
|
on the call stack (it does not have a VM Call->Label mapping installed), so
|
|
we must dispatch statically with a goto. The bazHandler thus appears as:
|
|
|
|
bazHandler:
|
|
d->~D(); // destruct D as it goes out of scope when entering catch clauses
|
|
goto TryHandler
|
|
|
|
In general, TryHandler is not the same as bazHandler, because multiple
|
|
function calls could be made from the try block. In this case, trivial
|
|
optimization could merge the two basic blocks. TryHandler is the code
|
|
that actually determines the type of exception, based on the Exception object
|
|
itself. For this discussion, assume that the exception object contains *at
|
|
least*:
|
|
|
|
1. A pointer to the RTTI info for the contained object
|
|
2. A pointer to the dtor for the contained object
|
|
3. The contained object itself
|
|
|
|
Note that it is necessary to maintain #1 & #2 in the exception object itself
|
|
because objects without virtual function tables may be thrown (as in this
|
|
example). Assuming this, TryHandler would look something like this:
|
|
|
|
TryHandler:
|
|
Exception *E = getThreadLocalException();
|
|
switch (E->RTTIType) {
|
|
case IntRTTIInfo:
|
|
...int Stuff... // The action to perform from the catch block
|
|
break;
|
|
case DoubleRTTIInfo:
|
|
...double Stuff... // The action to perform from the catch block
|
|
goto TryCleanup // This catch block rethrows the exception
|
|
break; // Redundant, eliminated by the optimizer
|
|
default:
|
|
goto TryCleanup // Exception not caught, rethrow
|
|
}
|
|
|
|
// Exception was consumed
|
|
if (E->dtor)
|
|
E->dtor(E->object) // Invoke the dtor on the object if it exists
|
|
goto EndTry // Continue mainline code...
|
|
|
|
And that is all there is to it.
|
|
|
|
The throw(E) function would then be implemented like this (which may be
|
|
inlined into the caller through standard optimization):
|
|
|
|
function throw(Exception *E) {
|
|
// Get the start of the stack trace...
|
|
%frame %f = call getStackCurrentFrame()
|
|
|
|
// Get the label information that corresponds to it
|
|
label * %L = call getFrameLabel(%f)
|
|
while (%L == 0 && !isFirstFrame(%f)) {
|
|
// Loop until a cleanup handler is found
|
|
%f = call getNextFrame(%f)
|
|
%L = call getFrameLabel(%f)
|
|
}
|
|
|
|
if (%L != 0) {
|
|
call setThreadLocalException(E) // Allow handlers access to this...
|
|
call doNonLocalBranch(%L)
|
|
}
|
|
// No handler found!
|
|
call BlowUp() // Ends up calling the terminate() method in use
|
|
}
|
|
|
|
That's a brief rundown of how C++ exception handling could be implemented in
|
|
llvm. Java would be very similar, except it only uses destructors to unlock
|
|
synchronized blocks, not to destroy data. Also, it uses two stack walks: a
|
|
nondestructive walk that builds a stack trace, then a destructive walk that
|
|
unwinds the stack as shown here.
|
|
|
|
It would be trivial to get exception interoperability between C++ and Java.
|
|
|