Home | History | Annotate | Download | only in HistoricalNotes
      1 Meeting notes: Implementation idea: Exception Handling in C++/Java
      2 
      3 The 5/18/01 meeting discussed ideas for implementing exceptions in LLVM.
      4 We decided that the best solution requires a set of library calls provided by
      5 the VM, as well as an extension to the LLVM function invocation syntax.
      6 
      7 The LLVM function invocation instruction previously looks like this (ignoring
      8 types):
      9 
     10   call func(arg1, arg2, arg3)
     11 
     12 The extension discussed today adds an optional "with" clause that 
     13 associates a label with the call site.  The new syntax looks like this:
     14 
     15   call func(arg1, arg2, arg3) with funcCleanup
     16 
     17 This funcHandler always stays tightly associated with the call site (being
     18 encoded directly into the call opcode itself), and should be used whenever
     19 there is cleanup work that needs to be done for the current function if 
     20 an exception is thrown by func (or if we are in a try block).
     21 
     22 To support this, the VM/Runtime provide the following simple library 
     23 functions (all syntax in this document is very abstract):
     24 
     25 typedef struct { something } %frame;
     26   The VM must export a "frame type", that is an opaque structure used to 
     27   implement different types of stack walking that may be used by various
     28   language runtime libraries. We imagine that it would be typical to 
     29   represent a frame with a PC and frame pointer pair, although that is not 
     30   required.
     31 
     32 %frame getStackCurrentFrame();
     33   Get a frame object for the current function.  Note that if the current
     34   function was inlined into its caller, the "current" frame will belong to
     35   the "caller".
     36 
     37 bool isFirstFrame(%frame f);
     38   Returns true if the specified frame is the top level (first activated) frame
     39   for this thread.  For the main thread, this corresponds to the main() 
     40   function, for a spawned thread, it corresponds to the thread function.
     41 
     42 %frame getNextFrame(%frame f);
     43   Return the previous frame on the stack.  This function is undefined if f
     44   satisfies the predicate isFirstFrame(f).
     45 
     46 Label *getFrameLabel(%frame f);
     47   If a label was associated with f (as discussed below), this function returns
     48   it.  Otherwise, it returns a null pointer.
     49 
     50 doNonLocalBranch(Label *L);
     51   At this point, it is not clear whether this should be a function or 
     52   intrinsic.  It should probably be an intrinsic in LLVM, but we'll deal with
     53   this issue later.
     54 
     55 
     56 Here is a motivating example that illustrates how these facilities could be
     57 used to implement the C++ exception model:
     58 
     59 void TestFunction(...) {
     60   A a; B b;
     61   foo();        // Any function call may throw
     62   bar();
     63   C c;
     64 
     65   try {
     66     D d;
     67     baz();
     68   } catch (int) {
     69     ...int Stuff...
     70     // execution continues after the try block: the exception is consumed
     71   } catch (double) {
     72     ...double stuff...
     73    throw;            // Exception is propogated
     74   }
     75 }
     76 
     77 This function would compile to approximately the following code (heavy 
     78 pseudo code follows):
     79 
     80 Func:
     81   %a = alloca A
     82   A::A(%a)        // These ctors & dtors could throw, but we ignore this 
     83   %b = alloca B   // minor detail for this example
     84   B::B(%b)
     85 
     86   call foo() with fooCleanup // An exception in foo is propogated to fooCleanup
     87   call bar() with barCleanup // An exception in bar is propogated to barCleanup
     88 
     89   %c = alloca C
     90   C::C(c)
     91   %d = alloca D
     92   D::D(d)
     93   call baz() with bazCleanup // An exception in baz is propogated to bazCleanup
     94   d->~D();
     95 EndTry:                   // This label corresponds to the end of the try block
     96   c->~C()       // These could also throw, these are also ignored
     97   b->~B()
     98   a->~A()
     99   return
    100 
    101 Note that this is a very straight forward and literal translation: exactly
    102 what we want for zero cost (when unused) exception handling.  Especially on
    103 platforms with many registers (ie, the IA64) setjmp/longjmp style exception
    104 handling is *very* impractical.  Also, the "with" clauses describe the 
    105 control flow paths explicitly so that analysis is not adversly effected.
    106 
    107 The foo/barCleanup labels are implemented as:
    108 
    109 TryCleanup:          // Executed if an exception escapes the try block  
    110   c->~C()
    111 barCleanup:          // Executed if an exception escapes from bar()
    112   // fall through
    113 fooCleanup:          // Executed if an exception escapes from foo()
    114   b->~B()
    115   a->~A()
    116   Exception *E = getThreadLocalException()
    117   call throw(E)      // Implemented by the C++ runtime, described below
    118 
    119 Which does the work one would expect.  getThreadLocalException is a function
    120 implemented by the C++ support library.  It returns the current exception 
    121 object for the current thread.  Note that we do not attempt to recycle the 
    122 shutdown code from before, because performance of the mainline code is 
    123 critically important.  Also, obviously fooCleanup and barCleanup may be 
    124 merged and one of them eliminated.  This just shows how the code generator 
    125 would most likely emit code.
    126 
    127 The bazCleanup label is more interesting.  Because the exception may be caught
    128 by the try block, we must dispatch to its handler... but it does not exist
    129 on the call stack (it does not have a VM Call->Label mapping installed), so 
    130 we must dispatch statically with a goto.  The bazHandler thus appears as:
    131 
    132 bazHandler:
    133   d->~D();    // destruct D as it goes out of scope when entering catch clauses
    134   goto TryHandler
    135 
    136 In general, TryHandler is not the same as bazHandler, because multiple 
    137 function calls could be made from the try block.  In this case, trivial 
    138 optimization could merge the two basic blocks.  TryHandler is the code 
    139 that actually determines the type of exception, based on the Exception object
    140 itself.  For this discussion, assume that the exception object contains *at
    141 least*:
    142 
    143 1. A pointer to the RTTI info for the contained object
    144 2. A pointer to the dtor for the contained object
    145 3. The contained object itself
    146 
    147 Note that it is necessary to maintain #1 & #2 in the exception object itself
    148 because objects without virtual function tables may be thrown (as in this 
    149 example).  Assuming this, TryHandler would look something like this:
    150 
    151 TryHandler: 
    152   Exception *E = getThreadLocalException();
    153   switch (E->RTTIType) {
    154   case IntRTTIInfo:
    155     ...int Stuff...       // The action to perform from the catch block
    156     break;
    157   case DoubleRTTIInfo:
    158     ...double Stuff...    // The action to perform from the catch block
    159     goto TryCleanup       // This catch block rethrows the exception
    160     break;                // Redundant, eliminated by the optimizer
    161   default:
    162     goto TryCleanup       // Exception not caught, rethrow
    163   }
    164 
    165   // Exception was consumed
    166   if (E->dtor)
    167     E->dtor(E->object)    // Invoke the dtor on the object if it exists
    168   goto EndTry             // Continue mainline code...
    169 
    170 And that is all there is to it.
    171 
    172 The throw(E) function would then be implemented like this (which may be 
    173 inlined into the caller through standard optimization):
    174 
    175 function throw(Exception *E) {
    176   // Get the start of the stack trace...
    177   %frame %f = call getStackCurrentFrame()
    178 
    179   // Get the label information that corresponds to it
    180   label * %L = call getFrameLabel(%f)
    181   while (%L == 0 && !isFirstFrame(%f)) {
    182     // Loop until a cleanup handler is found
    183     %f = call getNextFrame(%f)
    184     %L = call getFrameLabel(%f)
    185   }
    186 
    187   if (%L != 0) {
    188     call setThreadLocalException(E)   // Allow handlers access to this...
    189     call doNonLocalBranch(%L)
    190   }
    191   // No handler found!
    192   call BlowUp()         // Ends up calling the terminate() method in use
    193 }
    194 
    195 That's a brief rundown of how C++ exception handling could be implemented in
    196 llvm.  Java would be very similar, except it only uses destructors to unlock
    197 synchronized blocks, not to destroy data.  Also, it uses two stack walks: a
    198 nondestructive walk that builds a stack trace, then a destructive walk that
    199 unwinds the stack as shown here. 
    200 
    201 It would be trivial to get exception interoperability between C++ and Java.
    202 
    203