Introduction to MSIL – Part 5 – Exception Handling
In this part of the Introduction to MSIL series I will introduce the constructs that the CLI provides for exception handling.
A try block is used to protect a range of instructions. If an exception is thrown by one of the instructions in the protected block, or by any method called directly or indirectly within the protected block, control is transferred to an appropriate exception handler. A try block is declared with the .try directive.
.try
{
/* protected code */
leave.s _CONTINUE
}
<exception handler>
_CONTINUE:
The last instruction in the try block is the leave.s instruction which transfers control to the _CONTINUE label. The leave.s instruction ensures that the evaluation stack is emptied and that the appropriate finally blocks are executed. It follows that if control leaves the protected block by some other means, that an exception was thrown. The exception handler must immediately follow the try block.
The CLI offers four different kinds of handlers that you can employ to construct the error handling semantics that you require for your programming language or application. Let’s examine each one individually.
Catch
The catch exception handler, or catch block, has to be the most well-known form of exception handling, since it is provided directly by C++ as well as C#. A catch block is declared with the catch keyword. The catch clause includes the type of exception that the handler is willing to catch as well as the block of code to transfer control to, given a matching exception object. Catch blocks can also be chained together after a single try block for convenience. The first catch clause with a matching exception type will be chosen as the handler. Consider this example.
.try
{
ldstr "I'm not a number"
// ldnull
// ldstr "123"
call int32 [mscorlib]System.Int32::Parse(string)
leave.s _CONTINUE
}
catch [mscorlib]System.ArgumentNullException
{
callvirt instance string [mscorlib]System.Exception::get_Message()
call void [mscorlib]System.Console::WriteLine(string)
leave.s _CONTINUE
}
catch [mscorlib]System.FormatException
{
callvirt instance string [mscorlib]System.Exception::get_Message()
call void [mscorlib]System.Console::WriteLine(string)
leave.s _CONTINUE
}
Here we ask the In32::Parse method to parse "I'm not a number", which predictably throws a FormatException. The first catch handler is never given an opportunity to execute. The FormatException handler dutifully writes a message describing the exception to the console and then calls the leave.s instruction to transfer control to the _CONTINUE label. Where was the exception object? The runtime ensure that a reference to the exception is pushed onto the stack before the handler is invoked. You can experiment by commenting out the ldstr instruction in the try block and un-commenting the ldnull instruction. This will push a null reference onto the stack and result in an ArgumentNullException instance being thrown by the Parse method.
Filter
The filter exception handler is a strange construct for a C++ programmer like me. Instead of matching on an exception type, the filter handler provides a scope block where it can evaluate whether it wants to handle the exception. It indicates to the runtime that it wants to handle the exception by pushing a value of 1 onto the stack. If it decides not to handle the exception it pushes a value of 0 onto the stack. Following the “evaluation” block is the handler block that contains the code that will handle the exception.
.try
{
// ldstr "I'm not a number"
ldnull
// ldstr "123"
call int32 [mscorlib]System.Int32::Parse(string)
leave.s _CONTINUE
}
filter
{
ldstr "filter evaluation\n\t"
call void [mscorlib]System.Console::Write(string)
callvirt instance string [mscorlib]System.Exception::get_Message()
call void [mscorlib]System.Console::WriteLine(string)
ldc.i4.1
endfilter
}
{
ldstr "filter handler\n\t"
call void [mscorlib]System.Console::Write(string)
callvirt instance string [mscorlib]System.Exception::get_Message()
call void [mscorlib]System.Console::WriteLine(string)
leave.s _CONTINUE
}
The try block will result in an ArgumentNullException exception because a null reference is pushed onto the stack as an argument to the Int32::Parse method.
The filter is then given a chance to determine whether it wants to handle the exception. In this case it simply writes the error message to the console and then pushes the value 1 onto the stack, using the ldc.i4.1 instruction, to indicate that it wants to handle the exception. The endfilter instruction is called to return from the filter clause.
The filter handler block is then called to handle the exception. Notice that both the evaluation and handler blocks can access the exception in flight by popping it off of the stack. Keep in mind that you can throw any reference type so don’t assume that the object reference you pop off of the stack in the filter handler is an instance of System.Exception. This is not a problem for the catch handler since you will know the type of the exception object.
Finally
The finally exception handler should be recognized by C# programmers and C++ programmers familiar with Structured Exception Handling (SEH). A finally block associated with a try block is always executed, regardless of whether control leaves the protected try block through normal means, using the leave.s instruction, or as a result of an exception, using the throw instruction. It provides a reliable mechanism of ensuring a certain block of code is always run for those languages that do not provide destructors, such as C and C#. Although the C programming language does not target the CLI, SEH is used often enough that the Microsoft C/C++ compiler provides the __finally keyword for the same reason.
.try
{
/* protected code */
leave.s _CONTINUE
}
finally
{
/* cleanup code */
endfinally
}
Fault
The fault exception handler is similar to the finally block except that it is invoked only if it’s associated try block is left as a result of an exception. After the fault handler has been given an opportunity to execute, the exception continues on its way in search or a handler that is willing to catch it.
.try
{
/* protected code */
leave.s _CONTINUE
}
fault
{
/* cleanup code */
endfault
}
With that brief introduction to exception handling out of the way, it is time to change gears a bit and talk about how this all relates to popular programming languages like C# and C++/CLI.
Read part 6 now: Common Language Constructs
© 2004 Kenny Kerr