Chris Lattner | 0095054 | 2001-06-06 20:29:01 +0000 | [diff] [blame] | 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 neccesary 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 | |