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.
 | |
| 
 |