[LLVMdev] Alternative exception handling proposal

Duncan Sands baldrick at free.fr
Wed Dec 1 13:37:45 PST 2010


Here is an alternative proposal to Bill's.  It is closer to what we already
have (which can be seen as a good or a bad thing!), and is also closer to
what gcc does.  It is more incremental than Bill's and introduces fewer
new concepts.

Executive summary
-----------------

Remove the personality and list of catches out of eh.selector and stick them
directly on invoke instructions.

The invoke instruction
----------------------

The invoke instruction is modified by adding extra catch info to it:

   <result> = invoke <function>(<function args>)
     to label <normal label> unwind label <exception label> <catch info>

Here <catch info> comprises all the stuff we currently bundle into eh.selector,
namely the personality function, a list of catch type infos and filters, and
a flag indicating a cleanup (in eh.selector the flag is the number 0).  A
possible syntax:

<catch info> = [personality <ptr>] [cleanup] [catches <list of catches and filters>]

Here's an example where there is no cleanup and there are two handlers:

   invoke void @_Z3foov()
           to label %invcont unwind label %catch.handlers personality 
@__gxx_personality_v0 catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc

Note that unlike in Bill's proposal there isn't a label for each catch
object, just one global label (the existing unwind label).

The semantics of the invoke instruction are slightly modified: if an exception
unwinds and it doesn't match anything in the list of catches and filters,
and there is no cleanup, then control doesn't branch to the unwind label,
unwinding simply continues out of the function.

I marked the personality function as being optional since in fact if there
is only a cleanup and no catches or filters then the personality is not needed
(recent gcc implements this optimization).

Note that there is no longer any need to append a catch-all (as llvm-gcc
sometimes has to) or do any other mucking around to get proper cleanup
semantics, the list of catches just corresponds directly to those in the
original function.

What about getting rid of invoke?
---------------------------------

If we want to get rid of invoke and allow any instruction in a basic block
to throw, that's not a problem.  The unwind label and catch info is then
just attached to the basic block instead.  This proposal is orthogonal to
the issue of allowing arbitrary instructions to throw.

The eh.selector intrinsic
-------------------------

The eh.selector intrinsic changes to take no arguments whatsoever, just like
eh.exception:
   declare i32 @llvm.eh.selector() nounwind
In the same way as eh.exception gives you the value of the exception register
in a landing pad, eh.selector gives you the value of the selector register.
All the extra gumph currently in eh.selector is moved into invoke instructions
instead.

The meaning of the value of the eh.selector doesn't change, it is the same
as now: the id of the catch (or filter) that the exception matched, or zero
if it matched a cleanup.

It's actually possible to remove eh.selector altogether - see the "variants"
section below.

The rewind instruction
----------------------

For extra goodness, I propose introducing a new instruction "rewind" that takes
an exception pointer and selector value as arguments:
   rewind <ptr>, <i32>
The codegenerators would lower this to a call to _Unwind_Resume (the second
selector argument would not be used - it only exists to simplify inlining,
see below - the first, exception, argument would be passed as the parameter of
_UnwindResume).  The goal here is to simplify inlining and avoid explicitly
dragging in library functions like _Unwind_Resume.  It is possible to do without
this new instruction, but I think it represents an improvement so I include it
here.

An example
----------

Here is a technique for auto-generating examples of the new scheme from
existing LLVM IR (in fact it should be possible to auto-upgrade existing
bitcode using this scheme; I've left out some details which are only needed
for rather crazy IR; I'm not claiming that auto-upgrade is always possible,
just very often possible):

(1) For every invoke instruction, look in its unwind block for the eh.selector
call.  Remove the eh.selector arguments, and stick them in the invoke's
catch info instead.
(2) Replace calls to _Unwind_Resume (or _Unwind_Resume_Or_Rethrow which is what
we use right now) with a "rewind" instruction.

That's all folks!

Here's how the new scheme would look for Bill's example (I used the above recipe
to generate this from the LLVM IR produced by dragonegg):

#include <cstdio>

extern void foo();

struct A { ~A(); };
struct B { ~B(); };
struct C { ~C(); };

void bar() {
   try {
     foo();
     A a;
     foo();
     B b;
     foo();
     C c;
     foo();
   } catch (int i) {
     printf("caught int: %d\n", i);
   } catch (const char *c) {
     printf("caught string: %s\n", c);
   } catch (...) {
     printf("catchall\n");
   }
}

target datalayout = 
"e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"

%struct.A = type <{ i8 }>
%struct.__fundamental_type_info_pseudo = type { %struct.__type_info_pseudo }
%struct.__pointer_type_info_pseudo = type { %struct.__type_info_pseudo, i32, 
%struct.type_info* }
%struct.__type_info_pseudo = type { i8*, i8* }
%struct.type_info = type opaque

@_ZTIi = external constant %struct.__fundamental_type_info_pseudo
@_ZTIPKc = external constant %struct.__pointer_type_info_pseudo
@.str = private constant [16 x i8] c"caught int: %d\0A\00", align 1
@.str1 = private constant [19 x i8] c"caught string: %s\0A\00", align 1
@.str2 = private constant [9 x i8] c"catchall\00", align 1
@llvm.eh.catch.all.value = linkonce constant i8* null, section "llvm.metadata"
@llvm.used = appending global [1 x i8*] [i8* bitcast (i8** 
@llvm.eh.catch.all.value to i8*)], section "llvm.metadata"

define void @_Z3barv() {
entry:
   %memtmp = alloca %struct.A, align 8
   %memtmp1 = alloca %struct.A, align 8
   %memtmp2 = alloca %struct.A, align 8
   invoke void @_Z3foov()
           to label %"3" unwind label %lpad personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"3":                                              ; preds = %entry
   invoke void @_Z3foov()
           to label %"4" unwind label %lpad25 personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"4":                                              ; preds = %"3"
   invoke void @_Z3foov()
           to label %"5" unwind label %lpad26 personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"5":                                              ; preds = %"4"
   invoke void @_Z3foov()
           to label %"6" unwind label %"10" personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo*, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"6":                                              ; preds = %"5"
   invoke void @_ZN1CD1Ev(%struct.A* %memtmp)
           to label %"7" unwind label %lpad26 personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"7":                                              ; preds = %"6"
   invoke void @_ZN1BD1Ev(%struct.A* %memtmp1)
           to label %"8" unwind label %lpad25 personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"8":                                              ; preds = %"7"
   invoke void @_ZN1AD1Ev(%struct.A* %memtmp2)
           to label %return unwind label %lpad personality @__gxx_personality_v0 
catches %struct.__fundamental_type_info_pseudo* @_ZTIi, 
%struct.__pointer_type_info_pseudo* @_ZTIPKc, i8* null

"10":                                             ; preds = %"5"
   %exc_ptr31 = call i8* @llvm.eh.exception()
   %filter32 = call i32 @llvm.eh.selector()
   invoke void @_ZN1CD1Ev(%struct.A* %memtmp)
           to label %"11" unwind label %fail personality @__gxx_personality_v0 
catches i32 1 ; <- this is an empty filter, i.e. one that catches everything

lpad26:                                           ; preds = %"6", %"4"
   %exc_ptr29 = call i8* @llvm.eh.exception()
   %filter30 = call i32 @llvm.eh.selector()
   br label %"11"

"11":                                             ; preds = %"10", %lpad26
   %filt_tmp8.0 = phi i32 [ %filter30, %lpad26 ], [ %filter32, %"10" ]
   %exc_tmp5.0 = phi i8* [ %exc_ptr29, %lpad26 ], [ %exc_ptr31, %"10" ]
   invoke void @_ZN1BD1Ev(%struct.A* %memtmp1)
           to label %"12" unwind label %fail41 personality @__gxx_personality_v0 
catches i32 1 ; <- this is an empty filter, i.e. one that catches everything

lpad25:                                           ; preds = %"7", %"3"
   %exc_ptr27 = call i8* @llvm.eh.exception()
   %filter28 = call i32 @llvm.eh.selector()
   br label %"12"

"12":                                             ; preds = %"11", %lpad25
   %filt_tmp12.0 = phi i32 [ %filter28, %lpad25 ], [ %filt_tmp8.0, %"11" ]
   %exc_tmp10.0 = phi i8* [ %exc_ptr27, %lpad25 ], [ %exc_tmp5.0, %"11" ]
   invoke void @_ZN1AD1Ev(%struct.A* %memtmp2)
           to label %"16" unwind label %fail44 personality @__gxx_personality_v0 
catches i32 1 ; <- this is an empty filter, i.e. one that catches everything

lpad:                                             ; preds = %"8", %entry
   %exc_ptr = call i8* @llvm.eh.exception()
   %filter = call i32 @llvm.eh.selector()
   br label %"16"

"16":                                             ; preds = %"12", %lpad
   %exc_tmp14.0 = phi i8* [ %exc_ptr, %lpad ], [ %exc_tmp10.0, %"12" ]
   %filt_tmp16.0 = phi i32 [ %filter, %lpad ], [ %filt_tmp12.0, %"12" ]
   %typeid = call i32 @llvm.eh.typeid.for(i8* bitcast 
(%struct.__fundamental_type_info_pseudo* @_ZTIi to i8*))
   %0 = icmp eq i32 %filt_tmp16.0, %typeid
   br i1 %0, label %"18", label %1

; <label>:1                                       ; preds = %"16"
   %typeid24 = call i32 @llvm.eh.typeid.for(i8* bitcast 
(%struct.__pointer_type_info_pseudo* @_ZTIPKc to i8*))
   %2 = icmp eq i32 %filt_tmp16.0, %typeid24
   %3 = call i8* @__cxa_begin_catch(i8* %exc_tmp14.0) nounwind
   br i1 %2, label %"20", label %"22"

"18":                                             ; preds = %"16"
   %4 = call i8* @__cxa_begin_catch(i8* %exc_tmp14.0) nounwind
   %5 = bitcast i8* %4 to i32*
   %6 = load i32* %5, align 4
   %7 = call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([16 x 
i8]* @.str, i64 0, i64 0), i32 %6)
   call void @__cxa_end_catch() nounwind
   ret void

"20":                                             ; preds = %1
   %8 = call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([19 x 
i8]* @.str1, i64 0, i64 0), i8* %3)
   call void @__cxa_end_catch() nounwind
   ret void

"22":                                             ; preds = %1
   %9 = call i32 @puts(i8* getelementptr inbounds ([9 x i8]* @.str2, i64 0, i64 0))
   call void @__cxa_end_catch()
   ret void

return:                                           ; preds = %"8"
   ret void

fail:                                             ; preds = %"10"
   %exc_ptr39 = call i8* @llvm.eh.exception()
   %filter40 = call i32 @llvm.eh.selector()
   call void @_ZSt9terminatev() noreturn nounwind
   unreachable

fail41:                                           ; preds = %"11"
   %exc_ptr42 = call i8* @llvm.eh.exception()
   %filter43 = call i32 @llvm.eh.selector()
   call void @_ZSt9terminatev() noreturn nounwind
   unreachable

fail44:                                           ; preds = %"12"
   %exc_ptr45 = call i8* @llvm.eh.exception()
   %filter46 = call i32 @llvm.eh.selector()
   call void @_ZSt9terminatev() noreturn nounwind
   unreachable
}

declare void @_Z3foov()

declare void @_ZN1CD1Ev(%struct.A*)

declare void @_ZN1BD1Ev(%struct.A*)

declare void @_ZN1AD1Ev(%struct.A*)

declare void @__cxa_end_catch()

declare i32 @llvm.eh.typeid.for(i8*) nounwind

declare i8* @__cxa_begin_catch(i8*) nounwind

declare i32 @printf(i8* noalias nocapture, ...) nounwind

declare i32 @puts(i8* nocapture) nounwind

declare i8* @llvm.eh.exception() nounwind readonly

declare i32 @llvm.eh.selector() nounwind

declare i32 @__gxx_personality_v0(i32, i64, i8*, i8*)

declare void @_ZSt9terminatev() noreturn nounwind


How is it codegened
-------------------

Code generation is like now, only simpler.  The DwarfEHPrepare pass, which
currently has to do crazy things about catch-all's will still exist but much
simpler: it just has to ensure that the value of eh.selector makes sense no
matter where it is declared, like it does already for eh.exception, in fact
the same code could be used for both.

Currently when the code generators see an invoke, they rummage around in
the landing pad looking for an eh.selector call so they can extract the
catch info (and if it doesn't find one, it tries to look in sucessor blocks
because loop passes like to move eh.selector there...).  Now they don't have
to rummage because the needed information is directly attached to the invoke.

The eh.set.selector and eh.set.exception intrinsics
---------------------------------------------------

These new intrinsics can be used to set the value of the selector and
exception "registers", i.e. after a call eh.selector after a call to
eh.set.selector(%val) will return %val.  Implementing this can be done
easily in DwarfEHPrepare (in fact I considered adding these some time
ago to slightly simplify llvm-gcc lowering of the corresponding gcc
construct).

Inlining
--------

Many a plausible seeming exception handling scheme has fallen by the way-side
because it interacts poorly with inlining.

Here is how inlining would work with this scheme.  It's pretty close to how
it works right now.  Suppose you have

   invoke void @foo()
           to label %invcont unwind label %lpad <foo catch info>

and you want to inline foo.  Suppose foo contains an invoke:

   invoke void @bar()
           to label %invcont2 unwind label %lpad2 <bar catch info>

Then after inlining you have an invoke of bar in which foo's catch info
has been appended to bar's:

   invoke void @bar()
           to label %invcont2 unwind label %lpad2 <joined catch info>

What does appending <foo catch info> to <bar catch info> mean?  If the
personality functions are different then you are in trouble and need to
disallow the inlining!  The cleanup flag is the "or" of the foo and bar
cleanup flags.  The catches are the bar catches followed by the foo
catches.

Now suppose foo contains a call:

   call void @baz()

Then after inlining you have an invoke of baz with a copy of foo's
catch info:

   invoke void @baz()
           to label %continue unwind label %lpad <foo catch info>

In short inlining is exactly as before, except that you have to append foo's
catch info to everything you inline.

Now suppose foo has an instance of the rewind instruction:

   rewind i8* %exception, i32 %selector

Then after inlining this becomes:

   eh.set.exception(%exception)
   eh.set.selector(%selector)
   br label %lpad

The calls to eh.set.exception and eh.set.selector ensure that in %lpad the
calls to eh.exception and eh.selector return the right values.

Will everything work?
---------------------

I am confident that it will work fine, for a very simple reason: this is exactly
what gcc does!  Of course it is in disguise, a wolf in sheep's clothing some
might say :)  In fact moving closer to gcc like this is probably the best way
to be sure that exception handling works properly, since gcc is what everyone
tests against whether we like it or not (for example libstdc++ exploits some
details of how gcc implements exception handling that are not specified by the
standard, i.e. are implementation defined, and this has caused trouble for LLVM
in the past).

Ease of implementation
----------------------

Since this scheme is about as close as you can get to the existing scheme,
while still solving all problems the existing scheme has, the implementation
cost is actually quite small - mostly parsing the new invoke instruction and
storing the catch info inside the InvokeInst.  A bit of work on the inliner,
a bit of work on the code generators.

What does it solve?
-------------------

It solves the problem of eh.selector calls being moved far away from landing
pads by optimizers (or being placed far away from landing pads by front end
authors who don't know that they need to be careful).  It solves the problem
that LLVM inlining doesn't interact well with cleanups which is the reason why
llvm-gcc sticks catch-alls in funny places and has to stand on its head to get
things working.  This was essentially due to (1) invoke semantics (invoke
always unwinds to the landing pad), and (2) inlining an _Unwind_Resume through
an invoke resulting in catch info being placed on the _Unwind_Resume and far
away from the call that actually throws the exception.  People who've worked
in the guts of LLVM exception handling know what I'm talking about :)  All of
this badness just goes away with this scheme.

Variants
--------

With this scheme eh.exception and eh.selector are used pretty much the same
everywhere: every time you call eh.exception you likely call eh.selector and
vice versa.  We could get rid of eh.selector altogether by having eh.exception
return a pair of values (or a pointer to a stack slot in which codegen
sticks the two register values).  This requires a little bit of mucking around
with eh.typeid.for but nothing complicated.

The catch info could be made generic by using a metadata rather than a
list of catches etc.  So invoke would in essence have a metadata argument
in much the same way as intrinsics can have a metadata argument.  Then
different kinds of metadata could be used for dwarf eh, SEH etc.

Bad things
----------

I hate the way dwarf typeinfos, catches and filters are being baked into the
IR.  Maybe metadata (see above) helps with this.

What I didn't cover
-------------------

I should have provided more examples and explanations, but I'd rather go to bed
instead :)

Ciao,

Duncan.



More information about the llvm-dev mailing list