| # ClangIR Cleanup and Exception Handling Design |
| |
| ::: {.contents local=""} |
| ::: |
| |
| ## Overview |
| |
| This document describes the design for C++ cleanups and exception |
| handling representation and lowering in the CIR dialect. The initial CIR |
| generation will follow the general structure of the cleanup and |
| exception handling code in Clang's LLVM IR generation. In particular, |
| we will continue to use the `EHScopeStack` with pushing and popping of |
| `EHScopeStack::Cleanup` objects to drive the creation of cleanup scopes |
| within CIR. |
| |
| However, the LLVM IR generated by Clang is fundamentally unstructured |
| and therefore isn't well suited to the goals of CIR. Therefore, we are |
| proposing a high-level representation that follows MLIR's structured |
| control flow model. |
| |
| The `cir::LowerCFG` pass will lower this high-level representation to a |
| different form where control flow is block-based and explicit. This form |
| will more closely resemble the LLVM IR used when Clang is generating |
| LLVM IR directly. However, this form will still be ABI-agnostic. |
| |
| An additional pass will be introduced to lower the flattened form to an |
| ABI-specific representation. This ABI-specific form will have a direct |
| correspondence to the LLVM IR exception handling representation for a |
| given target. |
| |
| ## High-level CIR representation |
| |
| ### Normal and EH cleanups |
| |
| Scopes that require normal or EH cleanup will be represented using a new |
| operation, `cir.cleanup.scope`. |
| |
| ``` |
| cir.cleanup.scope { |
| // body region |
| } cleanup [normal|eh|all] { |
| // cleanup instructions |
| } |
| ``` |
| |
| Execution begins with the first operation in the body region and |
| continues according to normal control flow semantics until a terminating |
| operation (`cir.yield`, `cir.break`, `cir.return`, `cir.continue`) is |
| encountered or an exception is thrown. |
| |
| If the cleanup region is marked as `eh_only`, normal control flow exits |
| from the body region skip the cleanup region and continue to their |
| normal destination according to the semantics of the operation. If the |
| cleanup region is not marked as `eh_only`, normal control flow exits |
| from the body region must execute the cleanup region before control is |
| transferred to the destination implied by the operation. |
| |
| If a `cir.goto` operation occurs within a cleanup scope, the behavior |
| depends on the target of the operation. If the target is within the |
| same cleanup scope, control is transferred to the target block directly. |
| If the target is not within the cleanup scope, control is transferred to |
| the cleanup region according to the rules described above for normal |
| exits before branching to the destination of the goto operation. |
| |
| While we do not expect to encounter `cir.br` or `cir.brcond` operations |
| that exit a cleanup scope, if such a thing did happen, it would follow |
| the rules described above for `cir.goto` operations. |
| |
| The `cir.indirect_br` operation is not permitted within a cleanup scope. |
| |
| When an exception is thrown from within a cleanup scope and not caught |
| within the scope, the cleanup region must be executed before handling of |
| the exception continues. If the cleanup scope is nested within another |
| cleanup scope, the cleanup region of the inner scope is executed, |
| followed by the cleanup region of the outer scope, and handling |
| continues according to these rules. If the cleanup scope is nested |
| within a try operation, the cleanup region is executed before control is |
| transferred to the catch handlers. If an exception is thrown from within |
| a cleanup region that is not nested within either another cleanup region |
| or a try operation, the cleanup region is executed and then exception |
| unwinding continues as if a `cir.resume` operation had been executed. |
| |
| If a `cir.resume` operation occurs within a cleanup scope, for example, |
| if the scope contains a try operation with uncaught exception types, the |
| `cir.resume` operation will unwind to the cleanup region of the enclosing |
| cleanup scope. |
| |
| Note that this design eliminates the need for synthetic try operations, |
| such as were used to represent calls within a cleanup scope in the |
| ClangIR incubator project. |
| |
| #### Implementation notes |
| |
| The `cir.cleanup.scope` must be created when we call `pushCleanup`. We |
| will need to set the insertion point at that time. When each cleanup |
| block is popped, we will need to set the insertion point to immediately |
| following the cleanup scope operation. If `forceCleanups()` is called, |
| it will pop cleanup blocks, which is good. |
| |
| #### Example: Automatic storage object cleanup |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| SomeClass c; |
| c.doSomething(); |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc() { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup normal { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.return |
| } |
| ``` |
| |
| In this example, we create an instance of `SomeClass` which has a |
| constructor and a destructor. If an exception occurs within the |
| constructor call, it unwinds without any handling in this function. The |
| cleanup scope is not entered in that case. Once the object has been |
| constructed, we enter a cleanup scope which continues until the object |
| goes out of scope, in this case for the remainder of the function. |
| |
| If an exception is thrown from within the `doSomething()` function, we |
| execute the cleanup region, calling the `SomeClass` destructor before |
| continuing to unwind the exception. If the call to `doSomething()` |
| completes successfully, the object goes out of scope and we execute the |
| cleanup region, calling the destructor, before continuing to the return |
| operation. |
| |
| #### Example: Multiple automatic objects |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| SomeClass c; |
| SomeClass c2; |
| c.doSomething(); |
| SomeClass c3; |
| c3.doSomething(); |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc() { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| %1 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c2", init] |
| %2 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c3", init] |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClassC1Ev(%1) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.call @_ZN9SomeClassC1Ev(%2) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%2) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup normal { |
| cir.call @_ZN9SomeClassD1Ev(%2) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.yield |
| } cleanup normal { |
| cir.call @_ZN9SomeClassD1Ev(%1) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.yield |
| } cleanup normal { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.return |
| } |
| ``` |
| |
| In this example, we have three objects with automatic storage duration. |
| The destructor must be called for each object that has been constructed, |
| and the destructors must be called in reverse order of object creation. |
| We guarantee that by creating nested cleanup scopes as each object is |
| constructed. |
| |
| Normal execution control flows through the body region of each of the |
| nested cleanup scopes until the body of the innermost scope. Next, the |
| cleanup scopes are visited, calling the destructor once in each cleanup |
| scope, in reverse order of the object construction. |
| |
| #### Implementation notes |
| |
| Branch through cleanups will be handled during flattening. In the |
| structured CIR representation, an operation like `cir.break`, |
| `cir.return`, or `cir.continue` has well-defined behavior. We will need |
| to define the semantics such that they include visiting the cleanup |
| region before continuing to their currently defined destination. |
| |
| #### Example: Branch through cleanup |
| |
| **C++** |
| |
| ``` c++ |
| int someFunc() { |
| int i = 0; |
| while (true) { |
| SomeClass c; |
| if (i == 3) |
| continue; |
| if (i == 7) |
| break; |
| i = c.get(); |
| } |
| return i; |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc() -> !s32i { |
| %0 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"] |
| %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init] |
| %2 = cir.const #cir.int<0> : !s32i |
| cir.store align(4) %2, %1 : !s32i, !cir.ptr<!s32i> |
| cir.scope { |
| cir.while { |
| %5 = cir.const #true |
| cir.condition(%5) |
| } do { |
| cir.scope { |
| %5 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.scope { // This is a scope for the `if`, unrelated to cleanups |
| %7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| %8 = cir.const #cir.int<3> : !s32i |
| %9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool |
| cir.if %9 { |
| cir.continue // This implicitly branches through the cleanup region |
| } |
| } |
| cir.scope { // This is a scope for the `if`, unrelated to cleanups |
| %7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| %8 = cir.const #cir.int<7> : !s32i |
| %9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool |
| cir.if %9 { |
| cir.break // This implicitly branches through the cleanup region |
| } |
| } |
| %6 = cir.call @_ZN9SomeClass3getEv(%5) : (!cir.ptr<!rec_SomeClass>) -> !s32i |
| cir.store align(4) %6, %1 : !s32i, !cir.ptr<!s32i> |
| cir.yield |
| } cleanup normal { |
| cir.call @_ZN9SomeClassD1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| } |
| cir.yield |
| } |
| } |
| %3 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| cir.store %3, %0 : !s32i, !cir.ptr<!s32i> |
| %4 = cir.load %0 : !cir.ptr<!s32i>, !s32i |
| cir.return %4 : !s32i |
| } |
| ``` |
| |
| In this example we have a cleanup scope inside the body of a |
| `while-loop`, and multiple instructions that may exit the loop body with |
| different destinations. When the `cir.continue` operation is executed, |
| it will transfer control to the cleanup region, which calls the object |
| destructor before transferring control to the while condition region |
| according to the semantics of the `cir.continue` operation. |
| |
| When the `cir.break` operation is executed, it will transfer control to |
| the cleanup region, which calls the object destructor before |
| transferring control to the operation following the while loop according |
| to the semantics of the `cir.break` operation. |
| |
| If neither the `cir.continue` or `cir.break` operations are executed |
| during an iteration of the loop, when the end of the cleanup scope's |
| body region is reached, control will be transferred to the cleanup |
| region, which calls the object destructor before transferring control to |
| the next operation following the cleanup scope, in this case falling |
| through to the `cir.yield` operation to complete the loop iteration. |
| |
| This control flow is implicit in the semantics of the CIR operations at |
| this point. When this CIR is flattened, explicit branches and a switch |
| on destination slots will be created, matching the LLVM IR control flow |
| for cleanup block sharing. |
| |
| #### Example: EH-only cleanup |
| |
| **C++** |
| |
| ``` c++ |
| class Base { |
| public: |
| Base(); |
| ~Base(); |
| }; |
| |
| class Derived : public Base { |
| public: |
| Derived() : Base() { f(); } |
| ~Derived(); |
| }; |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @_ZN7DerivedC2Ev(%arg0: !cir.ptr<!rec_Derived>) { |
| %0 = cir.alloca !cir.ptr<!rec_Derived>, !cir.ptr<!cir.ptr<!rec_Derived>>, ["this", init] |
| cir.store %arg0, %0 : !cir.ptr<!rec_Derived>, !cir.ptr<!cir.ptr<!rec_Derived>> |
| %1 = cir.load %0 : !cir.ptr<!cir.ptr<!rec_Derived>>, !cir.ptr<!rec_Derived> |
| %2 = cir.base_class_addr %1 : !cir.ptr<!rec_Derived> nonnull [0] -> !cir.ptr<!rec_Base> |
| cir.call @_ZN4BaseC2Ev(%2) : (!cir.ptr<!rec_Base>) -> () |
| cir.cleanup.scope { |
| cir.call exception @_Z1fv() : () -> () |
| cir.yield |
| } cleanup eh { |
| %3 = cir.base_class_addr %1 : !cir.ptr<!rec_Derived> nonnull [0] -> !cir.ptr<!rec_Base> |
| cir.call @_ZN4BaseD2Ev(%3) : (!cir.ptr<!rec_Base>) -> () |
| cir.resume |
| } |
| cir.return |
| } |
| ``` |
| |
| In this example, the `Derived` constructor calls the `Base` constructor |
| and then calls a function which may throw an exception. If an exception |
| is thrown, we must call the `Base` destructor before continuing to |
| unwind the exception. However, if no exception is thrown, we do not call |
| the destructor. Therefore, this cleanup handler is marked as eh_only. |
| |
| ### Try Operations and Exception Handling |
| |
| Try-catch blocks will be represented, as they are in the ClangIR |
| incubator project, using a `cir.try` operation. |
| |
| ``` |
| cir.try { |
| cir.call exception @function() : () -> () |
| cir.yield |
| } catch [type #cir.global_view<@_ZTIPf> : !cir.ptr<!u8i>] { |
| ... |
| cir.yield |
| } unwind { |
| cir.resume |
| } |
| ``` |
| |
| The operation consists of a try region, which contains the operations to |
| be executed during normal execution, and one or more handler regions, |
| which represent catch handlers or the fallback unwind for uncaught |
| exceptions. |
| |
| #### Example: Simple try-catch |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| try { |
| f(); |
| } catch (std::exception &e) { |
| // Do nothing |
| } |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.scope { |
| cir.try { |
| cir.call exception @_Z1fv() : () -> () |
| cir.yield |
| } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { |
| cir.yield |
| } unwind { |
| cir.resume |
| } |
| } |
| cir.return |
| } |
| ``` |
| |
| If the call to `f()` throws an exception that matches the handled type |
| (`std::exception&`), control will be transferred to the catch handler |
| for that type, which simply yields, continuing execution immediately |
| after the try operation. |
| |
| If the call to `f()` throws any other type of exception, control will be |
| transferred to the unwind region, which simply continues unwinding the |
| exception at the next level, in this case, the handlers (if any) for the |
| function that called `someFunc()`. |
| |
| #### Example: Try-catch with catch all |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| try { |
| f(); |
| } catch (std::exception &e) { |
| // Do nothing |
| } catch (...) { |
| // Do nothing |
| } |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.scope { |
| cir.try { |
| cir.call exception @_Z1fv() : () -> () |
| cir.yield |
| } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { |
| cir.yield |
| } catch all { |
| cir.yield |
| } |
| } |
| cir.return |
| } |
| ``` |
| |
| In this case, if the call to `f()` throws an exception that matches the |
| handled type (`std::exception&`), everything works exactly as in the |
| previous example. Control will be transferred to the catch handler for |
| that type, which simply yields, continuing execution immediately after |
| the try operation. |
| |
| If the call to `f()` throws any other type of exception, control will be |
| transferred to the catch all region, which also yields, continuing |
| execution immediately after the try operation. |
| |
| #### Example: Try-catch with cleanup |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| try { |
| SomeClass c; |
| c.doSomething(); |
| } catch (...) { |
| // Do nothing |
| } |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.scope { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try { |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup all { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| } catch all { |
| cir.yield |
| } |
| } |
| cir.return |
| } |
| ``` |
| |
| In this case, an object that requires cleanup is instantiated inside the |
| try block scope. If the call to `doSomething()` throws an exception, the |
| cleanup region will be executed before control is transferred to the |
| catch handler. |
| |
| #### Example: Try-catch within a cleanup region |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| SomeClass c; |
| try { |
| c.doSomething(); |
| } catch (std::exception& e) { |
| // Do nothing |
| } |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.scope { |
| cir.try { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| } catch [type #cir.global_view<@_ZTISt9exception> : !cir.ptr<!u8i>] { |
| cir.yield |
| } unwind { |
| cir.resume |
| } |
| } |
| cir.yield |
| } cleanup all { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.return |
| } |
| ``` |
| |
| In this case, the object that requires cleanup is instantiated outside |
| the try block scope, and not all exception types have catch handlers. |
| |
| If the call to `doSomething()` throws an exception of type |
| `std::exception&`, control will be transferred to the catch handler, |
| which will simply continue execution at the point immediately following |
| the try operation, and the cleanup handler will be executed when the |
| cleanup scope is exited normally. |
| |
| If the call to `doSomething()` throws any other exception of type, |
| control will be transferred to the unwind region, which executes |
| `cir.resume` to continue unwinding the exception. However, the cleanup |
| region of the cleanup scope will be executed before exception unwinding |
| continues because we are exiting the scope via the `cir.resume` |
| operation. |
| |
| ### Partial Array Cleanup |
| |
| Partial array cleanup is a special case because the details of array |
| construction and deletion are already encapsulated within high-level CIR |
| operations. When an array of objects is constructed, the constructor for |
| each object is called sequentially. If one of the constructors throws an |
| exception, we must call the destructor for each object that was |
| previously constructed in reverse order of their construction. In the |
| high-level CIR representation, we have a single operation, |
| `cir.array.ctor` to represent the array construction. Because the |
| cleanup needed is entirely within the scope of this operation, we can |
| represent the cleanup by adding a cleanup region to this operation. |
| |
| ``` |
| cir.array.ctor(%0 : !cir.ptr<!cir.array<!rec_SomeClass x 16>>) { |
| ^bb0(%arg0: !cir.ptr<!rec_SomeClass>): |
| cir.call @_ZN9SomeClassC1Ev(%arg0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup { |
| ^bb0(%arg0: !cir.ptr<!rec_SomeClass>): |
| cir.call @_ZN9SomeClassD1Ev(%arg0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| ``` |
| |
| This representation shows how a single instance of the object is |
| initialized and cleaned up. When the operation is transformed to a |
| low-level form (during `cir::LoweringPrepare`), these two regions will |
| be expanded to a loop within a `cir.cleanup.scope` for the |
| initialization, and a loop within the cleanup scope's cleanup region to |
| perform the partial array cleanup, as follows |
| |
| ``` |
| cir.scope { |
| %1 = cir.const #cir.int<16> : !u64i |
| %2 = cir.cast array_to_ptrdecay %0 : !cir.ptr<!cir.array<!rec_SomeClass x 16>> -> !cir.ptr<!rec_SomeClass> |
| %3 = cir.ptr_stride %2, %1 : (!cir.ptr<!rec_SomeClass>, !u64i) -> !cir.ptr<!rec_SomeClass> |
| %4 = cir.alloca !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>>, ["__array_idx"] |
| cir.store %2, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>> |
| cir.cleanup.scope { |
| cir.do { |
| %5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass> |
| cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () |
| %6 = cir.const #cir.int<1> : !u64i |
| %7 = cir.ptr_stride %5, %6 : (!cir.ptr<!rec_SomeClass>, !u64i) -> !cir.ptr<!rec_SomeClass> |
| cir.store %7, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>> |
| cir.yield |
| } while { |
| %5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass> |
| %6 = cir.cmp(ne, %5, %3) : !cir.ptr<!rec_SomeClass>, !cir.bool |
| cir.condition(%6) |
| } |
| } cleanup eh { |
| cir.while { |
| %5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass> |
| %6 = cir.cmp(ne, %5, %2) : !cir.ptr<!rec_SomeClass>, !cir.bool |
| cir.condition(%6) |
| } cir.do { |
| %5 = cir.load %4 : !cir.ptr<!cir.ptr<!rec_SomeClass>>, !cir.ptr<!rec_SomeClass> |
| %6 = cir.const #cir.int<-1> : !s64i |
| %7 = cir.ptr_stride %5, %6 : (!cir.ptr<!rec_SomeClass>, !s64i) -> !cir.ptr<!rec_SomeClass> |
| cir.call @_ZN9SomeClassD1Ev(%7) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.store %7, %4 : !cir.ptr<!rec_SomeClass>, !cir.ptr<!cir.ptr<!rec_SomeClass>> |
| cir.yield |
| } |
| } |
| } |
| ``` |
| |
| Here, both the construction and cleanup loops use the same temporary |
| pointer variable to track their location. If an exception is thrown by |
| one of the constructor, the `__array_idx` variable will point to the |
| object that was being constructed when the exception was thrown. If the |
| exception was thrown during construction of the first object, |
| `__array_idx` will point to the start of the array, and so no destructor |
| will be called. If an exception is thrown during the constructor call |
| for any other object, `__array_idx` will not point to the start of the |
| array, and so the cleanup region will decrement the pointer, call the |
| destructor for the previous object, and so on until we reach the |
| beginning of the array. This corresponds to the way that partial array |
| destruction is handled in Clang's LLVM IR codegen. |
| |
| ## CFG Flattening |
| |
| Before CIR can be lowered to the LLVM dialect, the CFG must be |
| flattened. That is, functions must not contain nested regions, and all |
| blocks in the function must belong to the parent region. This state is |
| formed by the `cir::FlattenCFG` pass. This pass will need to transform |
| the high-level CIR representation described above to a flat form where |
| cleanups and exception handling are explicitly routed through blocks, |
| which are shared as needed. |
| |
| The CIR representation will remain ABI agnostic after the flattening |
| pass. The flattening pass will implement the semantics for branching |
| through cleanup regions using the same slot and dispatch mechanism used |
| in Clang's LLVM IR codegen. |
| |
| ### Exception Handling |
| |
| Flattening the CIR for exception handling, including any cleanups that |
| must be performed during exception unwinding, requires some specialized |
| CIR operations. The operations that were used in the ClangIR incubator |
| project were closely matched to the Itanium exception handling ABI. In |
| order to achieve a representation that also works well for other ABIs, |
| the following new operations are being proposed: `cir.eh.initiate`, |
| `cir.eh.dispatch`, `cir.begin_cleanup`, `cir.end_cleanup`, |
| `cir.begin_catch`, and `cir.end_catch`. |
| |
| Any time a cir.call operation that may throw and exception appears |
| within the try region of a `cir.try` operation or within the body region |
| of a `cir.cleanup.scope` with a cleanup region marked as an exception |
| cleanup, the call will be converted to a `cir.try_call` operation, with |
| normal and unwind destinations. The first operation in the unwind |
| destination block must be a `cir.eh.initiate` operation. |
| |
| `%eh_token = cir.eh.initiate [cleanup]` |
| |
| If this destination includes cleanup code, the cleanup keyword will be |
| present, and the cleanup code will be executed before the exception is |
| dispatched to any handlers. The `cir.eh.initiate` operation returns a |
| value of type `!cir.eh_token`. This is an opaque value that will be used |
| during ABI-lowering. At this phase, it conceptually represents the |
| exception that was thrown and is passed as the argument to the |
| `cir.begin_cleanup`, `cir.begin_catch`, and `cir.eh.dispatch` |
| operations. |
| |
| ``` |
| cir.eh.dispatch %eh_token : !cir.eh_token [ |
| catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb6 |
| catch_all : ^bb7 |
| ] |
| |
| cir.eh.dispatch %eh_token : !cir.eh_token [ |
| catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb6 |
| unwind : ^bb7 |
| ] |
| ``` |
| |
| The `cir.eh.dispatch` operation behaves similarly to the LLVM IR switch |
| instruction. It takes as an argument a token that was returned by a |
| previous `cir.eh.initiate` operation. It then has a list of key-value |
| pairs, where the key is either a type identifier, the keyword catch_all, |
| or the keyword unwind and the value is a block to which execution should |
| be transferred if the key is matched. Although the example above shows |
| both the catch_all and unwind keyword, in practice only one or the other |
| will be present, but the operation is required to have one of these |
| values. |
| |
| When we are unwinding an exception with cleanups, the `cir.eh.initiate` |
| operation will be marked with the cleanup attribute and will be followed |
| by a branch to the cleanup block, passing the EH token as an operand to |
| the block. The cleanup block will begin with a call to |
| `cir.begin_cleanup` which returns a cleanup token. |
| |
| ``` |
| ^bb4 (%eh_token : !cir.eh_token): |
| %cleanup_token = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token |
| ``` |
| |
| This is followed by the operations to perform the cleanup and then a |
| cir.end_cleanup operation. |
| |
| `cir.end_cleanup(%cleanup_token : !cir.cleanup_token)` |
| |
| Finally, the cleanup block either branches to a catch dispatch block or |
| executes a `cir.resume` operation to continue unwinding the exception. |
| |
| When an exception is caught, the catch block will receive the eh token |
| for the exception being caught as an argument, and the first operation |
| of the catch handling block must be a `cir.begin_catch` operation. |
| |
| ``` |
| ^bb6 (%token : !cir.eh_token): |
| %catch_token, %exn_ptr = cir.begin_catch %8 -> (!cir.catch_token, !cir.ptr<!s32i>) |
| ``` |
| |
| The `cir.begin_catch` operation returns two values: a new token that |
| uniquely identify this catch handler, and a pointer to the exception |
| object. All paths through the catch handler must converge on a single |
| `cir.end_catch` operation, which marks the end of the handler. |
| |
| `cir.end_catch %catch_token` |
| |
| The argument to the `cir.end_catch` operation is the token returned by |
| the `cir.begin_catch` operation. |
| |
| #### Example: Try-catch with cleanup |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| try { |
| SomeClass c; |
| c.doSomething(); |
| } catch (...) { |
| // Do nothing |
| } |
| } |
| ``` |
| |
| **High-level CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.scope { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try { |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup all { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| } catch all { |
| cir.yield |
| } |
| } |
| cir.return |
| } |
| ``` |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb2 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb8 |
| ^bb3 // EH catch (from entry block) |
| %1 = cir.eh.initiate : !cir.eh_token |
| cir.br ^bb6(%1 : !cir.eh_token) |
| ^bb4 // EH cleanup (from ^bb1) |
| %2 = cir.eh.initiate cleanup : !cir.eh_token |
| cir.br ^bb5(%2 : !cir.eh_token) |
| ^bb5(%eh_token : !cir.eh_token) |
| %3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.end_cleanup(%3 : !cir.cleanup_token) |
| cir.br ^bb6(%eh_token : !cir.eh_token) |
| ^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4) |
| cir.eh.dispatch %eh_token.1 : !cir.eh_token [ |
| catch_all : ^bb7 |
| ] |
| ^bb7(%eh_token.2 : !cir.eh_token) |
| %catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb8 |
| ^bb8 // Normal continue (from ^bb2 or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| In this example, the normal cleanup is performed in a different block |
| than the EH cleanup. This follows the pattern established by Clang's |
| LLVM IR codegen. Only the EH cleanup requires `cir.begin_cleanup` and |
| `cir.end_cleanup` operations. |
| |
| If the `SomeClass` constructor throws an exception, it unwinds to an EH |
| catch block (`^bb3`), which has excecutes a `cir.eh.initiate` operation |
| before branching to a shared catch dispatch block (`^bb6`). |
| |
| If the `doSomething()` function throws an exception, it unwinds to an EH |
| block `^bb4` that performs cleanup before branching to the shared catch |
| dispatch block (`^bb5`). |
| |
| #### Example: Cleanup with unhandled exception |
| |
| **C++** |
| |
| ``` c++ |
| void someFunc() { |
| SomeClass c; |
| c.doSomething(); |
| } |
| ``` |
| |
| **High-level CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.call @_ZN9SomeClass11doSomethingEv(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } cleanup all { |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| cir.return |
| } |
| ``` |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb4 |
| ^bb2 // EH cleanup (from entry block) |
| %1 = cir.eh.initiate cleanup : !cir.eh_token |
| cir.br ^bb3(%1 : !cir.eh_token) |
| ^bb3(%eh_token : !cir.eh_token) // Perform cleanup |
| %2 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.end_cleanup(%2 : !cir.cleanup_token) |
| cir.resume // Unwind to caller |
| ^bb4 // Normal continue (from ^bb1) |
| cir.return |
| } |
| ``` |
| |
| In this example, if `doSomething()` throws an exception, it unwinds to |
| the EH cleanup block (`^bb2`), which branches to `^bb3` to perform the |
| cleanup, but because we have no catch handler, we execute `cir.resume` |
| after the cleanup to unwind to the function that called `someFunc()`. |
| |
| #### Example: Shared cleanups |
| |
| **C++** |
| |
| ``` c++ |
| int someFunc() { |
| int i = 0; |
| while (true) { |
| SomeClass c; |
| if (i == 3) |
| continue; |
| if (i == 7) |
| break; |
| i = c.get(); |
| } |
| return i; |
| } |
| ``` |
| |
| **CIR** |
| |
| ``` |
| cir.func @someFunc() -> !s32i { |
| %0 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"] |
| %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init] |
| %2 = cir.const #cir.int<0> : !s32i |
| cir.store align(4) %2, %1 : !s32i, !cir.ptr<!s32i> |
| cir.scope { |
| cir.while { |
| %5 = cir.const #true |
| cir.condition(%5) |
| } do { |
| cir.scope { |
| %5 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.call @_ZN9SomeClassC1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanup.scope { |
| cir.scope { |
| %7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| %8 = cir.const #cir.int<3> : !s32i |
| %9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool |
| cir.if %9 { |
| cir.continue |
| } |
| } |
| cir.scope { |
| %7 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| %8 = cir.const #cir.int<7> : !s32i |
| %9 = cir.cmp(eq, %7, %8) : !s32i, !cir.bool |
| cir.if %9 { |
| cir.break |
| } |
| } |
| %6 = cir.call @_ZN9SomeClass3getEv(%5) : (!cir.ptr<!rec_SomeClass>) -> !s32i |
| cir.store align(4) %6, %1 : !s32i, !cir.ptr<!s32i> |
| cir.yield |
| } cleanup all { |
| cir.call @_ZN9SomeClassD1Ev(%5) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.yield |
| } |
| } |
| cir.yield |
| } |
| } |
| %3 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| cir.store %3, %0 : !s32i, !cir.ptr<!s32i> |
| %4 = cir.load %0 : !cir.ptr<!s32i>, !s32i |
| cir.return %4 : !s32i |
| } |
| ``` |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc() -> !s32i { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__cleanup_dest_slot "] |
| %2 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"] |
| %3 = cir.alloca !s32i, !cir.ptr<!s32i>, ["i", init] |
| %4 = cir.const #cir.int<0> : !s32i |
| cir.store align(4) %4, %3 : !s32i, !cir.ptr<!s32i> |
| cir.br ^bb1 |
| ^bb1: // 3 preds: ^bb0, ^bb9, ^bb11 |
| %5 = cir.const #true |
| cir.brcond %5 ^bb2, ^bb12 |
| ^bb2: // pred: ^bb1 |
| cir.call @_ZN9SomeClassC1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb3 |
| ^bb3: // pred: ^bb2 |
| %6 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i |
| %7 = cir.const #cir.int<3> : !s32i |
| %8 = cir.cmp(eq, %6, %7) : !s32i, !cir.bool |
| cir.brcond %8 ^bb4, ^bb5 |
| ^bb4: // pred: ^bb3 |
| // Set the destination slot and branch through cleanup |
| %9 = cir.const #cir.int<0> : !s32i |
| cir.store %9, %1 : !s32i, !cir.ptr<!s32i> |
| cir.br ^bb9 |
| ^bb5: // pred: ^bb3 |
| %10 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i |
| %11 = cir.const #cir.int<7> : !s32i |
| %12 = cir.cmp(eq, %10, %11) : !s32i, !cir.bool |
| cir.brcond %12 ^bb6, ^bb7 |
| ^bb6: // pred: ^bb5 |
| // Set the destination slot and branch through cleanup |
| %13 = cir.const #cir.int<1> : !s32i |
| cir.store %13, %1 : !s32i, !cir.ptr<!s32i> |
| cir.br ^bb9 |
| ^bb7: // pred: ^bb5 |
| %14 = cir.call @_ZN9SomeClass3getEv(%0) : (!cir.ptr<!rec_SomeClass>) -> !s32i |
| cir.store align(4) %14, %3 : !s32i, !cir.ptr<!s32i> |
| cir.br ^bb8 |
| ^bb8: // pred: ^bb7 |
| // Set the destination slot and branch through cleanup |
| %15 = cir.const #cir.int<2> : !s32i |
| cir.store %15, %1 : !s32i, !cir.ptr<!s32i> |
| cir.br ^bb9 |
| ^bb9: // pred |
| // Shared cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| %16 = cir.load align(4) %1 : !cir.ptr<!s32i>, !s32i |
| cir.switch.flat %16 : !s32i, ^bb10 [ |
| 0: ^bb1 // continue |
| 1: ^bb12 // break |
| 2: ^bb11 // end of loop |
| ] |
| ^bb10: // preds: ^bb9 |
| cir.unreachable |
| ^bb11: // pred: ^bb9 |
| cir.br ^bb1 |
| ^bb12: // pred: ^bb1 |
| %17 = cir.load align(4) %3 : !cir.ptr<!s32i>, !s32i |
| cir.store align(4) %17, %2 : !s32i, !cir.ptr<!s32i> |
| %18 = cir.load align(4) %2 : !cir.ptr<!s32i>, !s32i |
| cir.return %18 : !s32i |
| } |
| ``` |
| |
| In this example we have a cleanup scope inside the body of a while loop, |
| and multiple instructions that may exit the loop body with different |
| destinations. For simplicity, the example is shown without exception |
| handling. |
| |
| When any of the conditions that exit a loop iteration occur (continue, |
| break, or completion of an iteration), we set a cleanup destination slot |
| to a unique value and branch to a shared normal cleanup block. That |
| block performs the cleanup and then compares the cleanup destination |
| slot value to the set of expected constants and branches to the |
| corresponding destination. |
| |
| For example, when the continue instruction is reached, we set the |
| cleanup destination slot (`%1`) to zero, branch to the shared cleanup |
| block (`^bb9`), which calls the `SomeClass` destructor, then uses |
| `cir.switch.flat` to switch on the cleanup destination slot value and, |
| finding it to be zero, branches to the loop condition block (`^bb1`). |
| |
| If none of the expected values is matched, the `cir.switch.flat` |
| branches to a block with a `cir.unreachable` operation. This corresponds |
| to the behavior of Clang's LLVM IR codegen. |
| |
| ## ABI Lowering |
| |
| A new pass will be introduced to lower the flattened representation to |
| lower the ABI-agnostic flattened CIR representation to an ABI-specific |
| form. This will be a separate pass from the main CXXABI lowering pass, |
| which runs before CFG flattening. The ABI lowering pass will introduce |
| personality functions and ABI-specific exception handling operations. |
| |
| This new pass will make use of the `cir::CXXABI` interface class and |
| ABI-specific subclasses, but it will introduce a new set of interface |
| methods for use with the exception handling ABI. |
| |
| For each supported exception handling ABI, the operations and function |
| calls used will have a direct correspondence to the LLVM IR instructions |
| and runtime library functions used for that ABI. The LLVM IR exception |
| handling model is described in detail here: [LLVM Exception |
| Handling](https://llvm.org/docs/ExceptionHandling.html). |
| |
| A personality function attribute will be added to functions that require |
| it during the ABI lowering phase. |
| |
| ### Itanium ABI Lowering |
| |
| The Itanium exception handling ABI representation replaces the |
| `cir.eh.initiate` and `cir.eh.dispatch` operations with a |
| `cir.eh.landingpad` operation and a series of `cir.compare` and |
| `cir.brcond` operations to model the correct handling based on type IDs |
| for the catch handlers. The `cir.begin_cleanup` and `cir.end_cleanup` |
| operations are simply dropped. The `cir.begin_catch` operation becomes a |
| call to `__cxa_begin_catch`. The `cir.end_catch` operation becomes a |
| call to `__cxa_end_catch`. |
| |
| The only operation that is specific to Itanium exception handling is |
| `cir.eh.landingpad`. |
| |
| `%exn_ptr_0, %type_id = cir.eh.landingpad [@_ZTISt9exception] : !cir.ptr<!void>, !u32i` |
| |
| This operation corresponds directly to the LLVM IR landingpad |
| instruction. It may have a list of type IDs that the handler can catch |
| (or null for \"catch all\") or it may have the cleanup attribute if the |
| handler performs cleanup but does not catch any exceptions. |
| |
| #### Example: Try-catch with cleanup |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb2 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb8 |
| ^bb3 // EH catch (from entry block) |
| %1 = cir.eh.initiate : !cir.eh_token |
| cir.br ^bb6(%1 : !cir.eh_token) |
| ^bb4 // EH cleanup (from ^bb1) |
| %2 = cir.eh.initiate cleanup : !cir.eh_token |
| cir.br ^bb5(%2 : !cir.eh_token) |
| ^bb5(%eh_token : !cir.eh_token) |
| %3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.end_cleanup(%3 : !cir.cleanup_token) |
| cir.br ^bb6(%eh_token : !cir.eh_token) |
| ^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4) |
| cir.eh.dispatch %eh_token.1 : !cir.eh_token [ |
| catch_all : ^bb7 |
| ] |
| ^bb7(%eh_token.2 : !cir.eh_token) |
| %catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb8 |
| ^bb8 // Normal continue (from ^bb2 or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| **ABI-lowered CIR** |
| |
| ``` |
| cir.func @someFunc() #personality_fn = @__gxx_personality_v0 { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb2 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb8 |
| ^bb3 // EH catch (from entry block) |
| %exn, %type_id = cir.eh.landingpad [null] : (!cir.ptr<!void>, !u32i) |
| cir.br ^bb6(%exn, &type_id : !cir.ptr<!void>, !u32i) |
| ^bb4 // EH cleanup (from ^bb1) |
| %exn.1, %type_id.1 = cir.eh.landingpad cleanup [null] : (!cir.ptr<!void>, !u32i) |
| cir.br ^bb5(%exn, %type_id : !cir.ptr<!void>, !u32i) |
| ^bb5(%1: !cir.ptr<!void>, %2: !u32i) |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb6(%1, %2 : !cir.ptr<!void>, !u32i) |
| ^bb6(%3: !cir.ptr<!void>, %4: !u32i) // Catch dispatch (from ^bb3 or ^bb4) |
| cir.br ^bb7(%3, %4 : !cir.ptr<!void>, !u32i) |
| ^bb7(%5: !cir.ptr<!void>, %6: !u32i) // Catch all handler |
| %7 = cir.call @__cxa_begin_catch(%5 : !cir.ptr<!void>) |
| cir.call @__cxa_end_catch() |
| cir.br ^bb8 |
| ^bb8 // Normal continue (from ^bb2 or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| In this example, if an exception is thrown by the `SomeClass` |
| constructor, it unwinds to a landing pad block (`^bb3`), which branches |
| to the shared catch dispatch block (`^bb6`), which branches to the catch |
| all handler block (`^bb7`). The catch all handler calls |
| `__cxa_begin_catch` and `__cxa_end_catch` and then continues to the |
| normal continuation block (`^bb8`). |
| |
| #### Example: Try-catch with multiple catch handlers |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.try_call @f() ^bb1, ^bb2 |
| ^bb1 |
| cir.br ^bb7 |
| ^bb2 // EH catch (from entry block) |
| %1 = cir.eh.initiate : !cir.eh_token |
| cir.br ^bb3(%1 : !cir.eh_token) |
| ^bb3(%eh_token : !cir.eh_token) // Catch dispatch (from ^bb2) |
| cir.eh.dispatch %eh_token : !cir.eh_token [ |
| catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb4 |
| catch (#cir.global_view<@_ZTIf> : !u32i) : ^bb5 |
| catch_all : ^bb6 |
| ] |
| ^bb4(%eh_token.1 : !cir.eh_token) // Catch handler for int exception |
| %catch.token = cir.begin_catch(%eh_token.1 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb5(%eh_token.2 : !cir.eh_token) // Catch handler for float exception |
| %catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb6(%eh_token.3 : !cir.eh_token) // Catch all handler |
| %catch.token = cir.begin_catch(%eh_token.3 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb7 // Normal continue (from ^bb1, ^bb4, ^bb5, or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| **ABI-lowered CIR** |
| |
| ``` |
| cir.func @someFunc() #personality_fn = @__gxx_personality_v0 { |
| cir.try_call @f() ^bb1, ^bb2 |
| ^bb1 |
| cir.br ^bb8 |
| ^bb2 // EH catch (from entry block) |
| %exn, %type_id = cir.eh.landingpad [null] : (!cir.ptr<!void>, !u32i) |
| cir.br ^bb3(%exn, &type_id : !cir.ptr<!void>, !u32i) |
| ^bb3(%0: !cir.ptr<!void>, %1: !u32i) // Catch compare for int exception |
| %2 = cir.eh.typeid @_ZTIi : !u32i |
| %3 = cir.cmp(eq, %1, %2) : !u32i, !cir.bool |
| cir.brcond %3 ^bb4(%0 : !cir.ptr<!void>), ^bb5(%0, %1 : !cir.ptr<!void>, !u32i) |
| ^bb4(%4: !cir.ptr<!void>, %5: !u32i) // Catch all handler for int exception |
| %6 = cir.call @__cxa_begin_catch(%4 : !cir.ptr<!void>) |
| cir.call @__cxa_end_catch() |
| cir.br ^bb8 |
| ^bb5(%7: !cir.ptr<!void>, %8: !u32i) // Catch compare for float exception |
| %9 = cir.eh.typeid @_ZTIf : !u32i |
| %10 = cir.cmp(eq, %8, %9) : !u32i, !cir.bool |
| cir.brcond %10 ^bb7(%7 : !cir.ptr<!void>), ^bb8(%7 : !cir.ptr<!void>) |
| ^bb6(%11: !cir.ptr<!void>, %12: !u32i) // Catch all handler for float exception |
| %13 = cir.call @__cxa_begin_catch(%11 : !cir.ptr<!void>) |
| cir.call @__cxa_end_catch() |
| cir.br ^bb8 |
| ^bb7(%14: !cir.ptr<!void>) // Catch all handler |
| %15 = cir.call @__cxa_begin_catch(%14 : !cir.ptr<!void>) |
| cir.call @__cxa_end_catch() |
| cir.br ^bb8 |
| ^bb8 // Normal continue (from ^bb1, ^bb4, ^bb6, or ^bb7) |
| cir.return |
| } |
| ``` |
| |
| In this example, if an exception is thrown by the `f()` call, it unwinds |
| to a landing pad block (`^bb2`), which uses the `cir.eh.landingpad` |
| operation to capture the exception pointer and its type id, then branches |
| to `^bb3` to begin searching for a catch handler that handles the type id |
| of the exception. Each catch handler simply consumes the exception by |
| calling `__cxa_begin_catch` and `__cxa_end_catch` and then continues to |
| the normal continuation block (`^bb8`). |
| |
| ### Microsoft C++ ABI Lowering |
| |
| The Microsoft C++ exception handling ABI representation drops the |
| `cir.eh.initiate` operation and replaces the `cir.eh.dispatch` operation |
| with `cir.eh.catchswitch` operation. The `cir.begin_cleanup` and |
| `cir.end_cleanup` operations are replaced with `cir.cleanuppad` and |
| `cir.cleanupret` respectively, and the `cir.begin_catch` and |
| `cir.end_catch` operations are replaced with `cir.catchpad` and |
| `cir.catchret`. |
| |
| Each of these operations corresponds directly to a similarly named |
| instruction in LLVM IR and have the same semantics. The first operation |
| in the unwind destination of a `cir.try_call` must be either |
| `cir.eh.catchswitch` or `cir.cleanuppad`. |
| |
| `%4 = cir.eh.catchswitch within none [^bb2, ^bb3] unwind to caller` |
| |
| The `cir.eh.catchswitch` operation takes an operand which specifies the |
| parent token, which may either be none or the token returned by a |
| previous `cir.catchpad` operation. This is followed by a list of blocks |
| which contain catch handlers. Each block in this list must begin with a |
| `cir.catchpad` operation. Finally, the unwind destination is provided to |
| specify where excution continues if the exception is not caught by any |
| of the handlers, with unwind to caller indicating that the unwind is not |
| handled further in the current function. This operation returns a token |
| that is used as the operand for `cir.catchpad` operations associated |
| with this switch. |
| |
| `%5 = cir.cleanuppad within none []` |
| |
| The `cir.cleanuppad` operation takes an operand which specifies the |
| parent token, which may either be none or the token returned by a |
| previous `cir.catchpad` operation. This is followed by a arguments |
| required by the personality function. In the case of C++ exception |
| handlers, the personality function will be `__CxxFrameHandler3` and the |
| argument list will be empty. This operation returns a token that is used |
| as the operand for the associated `cir.cleanupret` operation. |
| |
| `cir.cleanupret from %5 unwind to ^bb7` |
| |
| The `cir.cleanupret` operation takes an operand which specifies the |
| `cir.cleanuppad` operation which is completed by this operation and a |
| block at which unwinding of the current exception continues (or unwind |
| to caller if there is no catch handling in the current function). |
| |
| `%8 = cir.catchpad within %4 [ptr @"??_R0H@8", i32 0, ptr %e]` |
| |
| The `cir.catchpad` operation takes an operand which specifies the parent |
| token, which must have been return by a previous `cir.catchswitch` |
| operation. This is followed by a list of arguments, beginning with the |
| typeid for the type of exception being caught (or null for catch all), |
| followed by a type info flag value, followed by a pointer to the |
| in-flight exception. This operation returns a token that is used as the |
| operand for the associated `cir.catchret` operation or as the parent for |
| any `cir.catchswitch` or `cir.cleanuppad` operations that are nested |
| within this catch handler. |
| |
| `cir.catchret from %8 to ^bb8` |
| |
| The `cir.catchret` operation takes an operand which specifies the |
| `cir.catchpad` operation which is completed by this operation and a |
| block at which excution should be resumed. |
| |
| #### Example: Try-catch with cleanup |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc() { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb2 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb8 |
| ^bb3 // EH catch (from entry block) |
| %1 = cir.eh.initiate : !cir.eh_token |
| cir.br ^bb6(%1 : !cir.eh_token) |
| ^bb4 // EH cleanup (from ^bb1) |
| %2 = cir.eh.initiate cleanup : !cir.eh_token |
| cir.br ^bb5(%2 : !cir.eh_token) |
| ^bb5(%eh_token : !cir.eh_token) |
| %3 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.end_cleanup(%3 : !cir.cleanup_token) |
| cir.br ^bb6(%eh_token : !cir.eh_token) |
| ^bb6(%eh_token.1 : !cir.eh_token) // Catch dispatch (from ^bb3 or ^bb4) |
| cir.eh.dispatch %eh_token.1 : !cir.eh_token [ |
| catch_all : ^bb7 |
| ] |
| ^bb7(%eh_token.2 : !cir.eh_token) |
| %catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb8 |
| ^bb8 // Normal continue (from ^bb2 or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| **ABI-lowered CIR** |
| |
| ``` |
| cir.func @someFunc() #personality_fn = @ __CxxFrameHandler3 { |
| %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] |
| cir.try_call @_ZN9SomeClassC1Ev(%0) ^bb1, ^bb4 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb1 |
| cir.try_call @_ZN9SomeClass11doSomethingEv(%0) ^bb2, ^bb3 : (!cir.ptr<!rec_SomeClass>) -> () |
| ^bb2 // Normal cleanup |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.br ^bb6 |
| ^bb3 // EH cleanup (from ^bb1) |
| %1 = cir.cleanuppad within none : !cir.cleanup_token |
| cir.call @_ZN9SomeClassD1Ev(%0) : (!cir.ptr<!rec_SomeClass>) -> () |
| cir.cleanupret from %1 unwind to ^bb4 |
| ^bb4 // Catch dispatch (from ^bb3 or ^bb4) |
| %2 = cir.catchswitch within none [^bb5] unwind to caller |
| ^bb5 |
| %catch.token = cir.catchpad within %2 [null : !cir.ptr<!void>] : !cir.catch_token |
| cir.catchret within %catch.token to ^bb6 |
| ^bb6 // Normal continue (from ^bb2 or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| #### Example: Try-catch with multiple catch handlers |
| |
| **Flattened CIR** |
| |
| ``` |
| cir.func @someFunc(){ |
| cir.try_call @f() ^bb1, ^bb2 |
| ^bb1 |
| cir.br ^bb7 |
| ^bb2 // EH catch (from entry block) |
| %1 = cir.eh.initiate : !cir.eh_token |
| cir.br ^bb3(%1 : !cir.eh_token) |
| ^bb3(%eh_token : !cir.eh_token) // Catch dispatch (from ^bb2) |
| cir.eh.dispatch %eh_token : !cir.eh_token [ |
| catch (#cir.global_view<@_ZTIi> : !u32i) : ^bb4 |
| catch (#cir.global_view<@_ZTIf> : !u32i) : ^bb5 |
| catch_all : ^bb6 |
| ] |
| ^bb4(%eh_token.1 : !cir.eh_token) // Catch handler for int exception |
| %catch.token = cir.begin_catch(%eh_token.1 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb5(%eh_token.2 : !cir.eh_token) // Catch handler for float exception |
| %catch.token = cir.begin_catch(%eh_token.2 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb6(%eh_token.3 : !cir.eh_token) // Catch all handler |
| %catch.token = cir.begin_catch(%eh_token.3 : !cir.eh_token) : !cir.catch_token |
| cir.end_catch(%catch.token : !cir.catch_token) |
| cir.br ^bb7 |
| ^bb7 // Normal continue (from ^bb1, ^bb4, ^bb5, or ^bb6) |
| cir.return |
| } |
| ``` |
| |
| **ABI-lowered CIR** |
| |
| ``` |
| cir.func @someFunc() #personality_fn = @__CxxFrameHandler3 { |
| cir.try_call @f() ^bb1, ^bb2 |
| ^bb1 |
| cir.br ^bb6 |
| ^bb2 // EH catch (from entry block) |
| %0 = cir.catchswitch within none [^bb3, ^bb4, ^bb5] unwind to caller |
| ^bb3(%0: !cir.ptr<!void>) // Catch handler for int exception |
| %1 = cir.catchpad within %0 [eh.typeid @"??_R0H@8", 0, %0 : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token |
| cir.catchret from %1 to ^bb6 |
| ^bb4(%2: !cir.ptr<!void>) // Catch compare for float exception |
| %2 = cir.catchpad within %0 [eh.typeid @"??_R0M@8", 0, %0 : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token |
| cir.catchret from %2 to ^bb6 |
| ^bb5(%3: !cir.ptr<!void>) // Catch all handler |
| %4 = cir.catchpad within %0 [null, 64, null : (!cir.ptr<!void>, !u32i, !cir.ptr<!void>)] : !cir.catch_token |
| cir.catchret from %4 to ^bb6 |
| ^bb6 // Normal continue (from ^bb1, ^bb3, ^bb4, or ^bb5) |
| cir.return |
| } |
| ``` |
| |
| In this example, if an exception is thrown by the `f()` call, it unwinds |
| to a catch dispatch block (`^bb2`), which uses the `cir.catchswitch` |
| operation to dispatch to a catch handler (`^bb3`, `^bb4`, or `^bb5`) |
| based on the type id of the exception. The actual comparisons in this |
| case will be handled by the personality function, using tables that are |
| generated from the `cir.catchpad` operations. Each catch handler simply |
| continues to the normal continuation block (`^bb6`) using the |
| `cir.catchret` operation. |