An exception handler in C# combines the IL catch
and finally
exception handling clauses into a single try
statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
try { Console.WriteLine("Try block") // ... } catch (IOException) { Console.WriteLine("IOException catch") // ... } catch (Exception e) { Console.WriteLine("Exception catch") // ... } finally { Console.WriteLine("Finally block") // ... } |
How does this get compiled into IL?
Initial implementation
If you remember from my earlier post, finally
clauses must be specified with their own .try
clause. So, for the initial implementation, we take the try/catch/finally, and simply split it up into two .try
clauses (I have to use label syntax for this):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
StartTry: ldstr "Try block" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End EndTry: StartIOECatch: ldstr "IOException catch" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End EndIOECatch: StartECatch: ldstr "Exception catch" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End EndECatch: StartFinally: ldstr "Finally block" call void [mscorlib]System.Console::WriteLine(string) // ... endfinally EndFinally: End: // ... .try StartTry to EndTry catch [mscorlib]System.IO.IOException handler StartIOECatch to EndIOECatch catch [mscorlib]System.Exception handler StartECatch to EndECatch .try StartTry to EndTry finally handler StartFinally to EndFinally |
However, the resulting program isn’t verifiable, and doesn’t run:
1 |
[IL]: Error: Shared try has finally or fault handler. |
Nested try blocks
What’s with the verification error? Well, it’s a condition of IL verification that all exception handling regions (try
, catch
, filter
, finally
, fault
) of a single .try
clause have to be completely contained within any outer exception region, and they can’t overlap with any other exception handling clause. In other words, IL exception handling clauses must to be representable in the scoped syntax, and in this example, we’re overlapping catch
and finally
clauses.
Not only is this example not verifiable, it isn’t semantically correct. The finally
handler is specified round the .try
. What happens if you were able to run this code, and an exception was thrown?
- Program execution enters top of
try
block, and exception is thrown within it - CLR searches for an exception handler, finds
catch
- Because control flow is leaving
.try
,finally
block is run - The
catch
block is run leave.s End
inside the catch handler branches toEnd
label.
We’re actually running the finally
before the catch
!
What we do about it
What we actually need to do is put the catch
clauses inside the finally
clause, as this will ensure the finally
gets executed at the correct time (this time using scoped syntax):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
.try { .try { ldstr "Try block" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End } catch [mscorlib]System.IO.IOException { ldstr "IOException catch" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End } catch [mscorlib]System.Exception { ldstr "Exception catch" call void [mscorlib]System.Console::WriteLine(string) // ... leave.s End } } finally { ldstr "Finally block" call void [mscorlib]System.Console::WriteLine(string) // ... endfinally } End: ret |
Returning from methods
There is a further semantic mismatch that the C# compiler has to deal with; in C#, you are allowed to return
from within an exception handling block:
1 2 3 4 5 6 7 8 9 10 |
public int HandleMethod() { try { // ... return 0; } catch (Exception) { // ... return -1; } } |
However, you can’t ret
inside an exception handling block in IL. So the C# compiler does a leave.s
to a ret
outside the exception handling area, loading/storing any return value to a local variable along the way (as leave.s
clears the stack):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
.method public instance int32 HandleMethod() { .locals init ( int32 retVal ) .try { // ... ldc.i4.0 stloc.0 leave.s End } catch [mscorlib]System.Exception { // ... ldc.i4.m1 stloc.0 leave.s End } End: ldloc.0 ret } |
Conclusion
As you can see, the C# compiler has quite a few hoops to jump through to translate C# code into semantically-correct IL, and hides the numerous conditions on IL exception handling blocks from the C# programmer.
Next time: onto fault
handlers.
Load comments