Subterranean IL: Compiling C# exception handlers

Posted by Simon Cooper on Simple Talk See other posts from Simple Talk or by Simon Cooper
Published on Tue, 25 Jan 2011 11:51:00 GMT Indexed on 2011/01/28 23:34 UTC
Read the original article Hit count: 361

Filed under:

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?

  1. Program execution enters top of try block, and exception is thrown within it
  2. CLR searches for an exception handler, finds catch
  3. Because control flow is leaving .try, finally block is run
  4. The catch block is run
  5. 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.

© Simple Talk or respective owner