Subterranean IL: Compiling C# exception handlers
- by Simon Cooper
An exception handler in C# combines the IL catch and finally exception handling clauses into a single try statement:
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):
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:
[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 to End 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):
.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:
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):
.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 up: catch-all blocks, and how the runtime deals with non-Exception exceptions.