Exception Handling

Exception handling is quite an important topic in .NET programs, and so you might have expected quite a large section on the subject in this chapter. However, in fact we're not going to spend that much time going over exceptions. The reason is that exception handling in Intermediate Language is not very different from high-level languages. Even the syntax in IL assembly is very similar to the syntax in C# or VB. Just as in high-level languages, you can define try, catch, and finally blocks. And exceptions are thrown using the IL throw command. There's also a rethrow command that you can use inside catch blocks and that rethrows the current exception. There are also two new types of block known as fault and filter blocks, which we'll explain soon. As in high-level languages, try blocks are also known as guarded or protected blocks.

Typical code that uses exception handling would look a bit like this in IL assembly:

 .try {    .try    {       // Code inside the try block or methods called from here,       // which will likely contain some throw statements    }    catch [mscorlib]System.Exception    {       // Code for the catch block.    } } finally {    // Code for the finally block } 

This IL code corresponds to the following C# code:

 try {    // Code inside the try block or methods called from here,    // which will likely contain some throw statements } catch (Exception e) {    // Code for the catch block. } finally {    // Code for the finally block } 

Although the syntax is quite similar in IL and C#, notice that the IL version contains two try blocks, where in C# there is only one. In fact, in C# and other high-level languages, the compiler would add a second try block behind the scenes for the above code. The reason is that if a guarded block is associated with a finally (or fault) handler, then it cannot have any other associated handler blocks. So it's not possible to have one guarded block that is associated with both a catch and a finally block, as is common in high-level languages. You need instead to insert a nested try block, which will be associated with the catch block.

In IL, .try is a directive (it has a preceding dot), but catch and finally are simple keywords associated with the .try. .try, catch, and finally have exactly the same meanings as in C#, MC++, and VB. In the above code snippet I've supplied a catch block that handles System.Exception, but obviously you can specify whatever class you wish - the normal rules about the system searching until it finds a suitable catch handler for each exception thrown apply. The exception does not have to be derived from System.Exception - that requirement is imposed by languages such as C# and VB, but is not a requirement of the CLR. Obviously, however, it is good programming practice only to throw exceptions derived from System.Exception.

The concept of guarded blocks doesn't exist in the JIT-compiled native code, since native assembly language doesn't have any concept of exceptions. The JIT compiler will internally sort out all the logic behind how to convert the try, catch, and finally blocks and the throw statements into suitable branch and conditional branch instructions at the native executable level.

That's the basics, but there are some other IL-specific issues you need to be aware of. Firstly, let's explain what filter and fault blocks are:

  • fault blocks are similar to catch (...) in C++ and catch without supplying an exception type in C#, but with one subtle difference: a fault block is always executed if any exception is thrown from the guarded block. In other words, if an exception is thrown, the program will first execute the catch block that most closely matches the exception type, if one is present; then it will execute the fault block, if present, and lastly it will execute the finally block if present. If no exception is thrown, execution goes straight to any finally block after leaving the guarded block.

  • filter is similar to catch, but provides a means of checking whether to actually execute the catch handler. Using a filter provides an alternative to placing a rethrow command in a catch block. The VB compiler actively uses filter blocks, since VB allows constructs such as Catch When x > 56: that kind of code can be translated into a filter block. However, C# and C++ do not expose any similar feature.

We won't go into fault or filter in detail in this book. If you are interested, they are detailed in the documentation for IL.

There are also a couple of other restrictions on the use of the guarded blocks:

  • The evaluation stack must be empty when entering a .try block. When entering or leaving most of the other blocks, it must be empty apart from the relevant thrown exception. There are no evaluation stack restrictions for exiting a finally or fault block.

  • It is not possible to use any of the branch instructions we've met so far to transfer control in or out of exception handling blocks. Instead, there is a new command, leave, and an equivalent shortened form, leave.s, which can be used to exit .try, catch, and filter blocks.

  • The only way that you can leave a finally or fault block is by using an IL instruction, endfinally/endfault. This is a single instruction with two mnemonics, and you should use whichever one is most suitable. (Common sense dictates that if you use it in a finally block, then using the endfault mnemonic will not help other people to understand your code!) endfinally/endfault works in a similar way to br (it doesn't empty the evaluation stack). However, since the JIT compiler knows where the end of the finally block is and where control must therefore be transferred to, endfinally/endfault doesn't need an argument to indicate where to branch to. This instruction therefore only occupies one byte in the assembly. leave works almost exactly like br - it's an unconditional branch, taking an argument that indicates the relative offset by which to branch. The difference between br and leave is that before branching, leave clears out the evaluation stack. This means that leave has quite an unusual stack transition diagram:

    Important 

    ... <empty>

All these restrictions are really for the benefit of the JIT compiler. Converting throw statements and exception handling blocks into straight branch instructions as required in the native executable code is not a trivial task, and writing an algorithm to do it would become ridiculously complicated if the JIT compiler had to cope with possible different states of the evaluation stack as well.

Exception Handling Sample

To illustrate exception handling, we'll return to the CompareNumbers sample from the previous chapter. Recall that CompareNumbers asked the user to type in two numbers, and then indicated which one was the greater one. Because CompareNumbers didn't do any exception handling, if the user typed in something that wasn't a number, the program would simply crash and display the usual unhandled exception message. (The exception will actually be thrown by the System.Int32.Parse() method if it is unable to convert the string typed in by the user into an integer.) Here we will modify the code to add appropriate exception handling:

 .method static void Main() cil managed {    .maxstack 2    .entrypoint    .try    {       .try       {          ldstr   "Input first number."          call    void [mscorlib]System.Console::WriteLine(string)          call    string [mscorlib]System.Console::ReadLine()          call    int32 [mscorlib]System.Int32;:Parse(string)          ldstr   "Input second number."          call    void [mscorlib]System.Console::WriteLine(string)          call    string [mscorlib]System.Console::ReadLine()          call    int32 [mscorlib]System.Int32:: Parse(string)          ble.s   FirstSmaller          ldstr   "The first number was larger than the second one"          call    void [mscorlib]System.Console::WriteLine(string)          leave.s Finish FirstSmaller:          ldstr   "The first number was less than or equal to the second one"          call    void [mscorlib]System.Console::WriteLine(string)          leave.s Finish       }       catch [mscorlib]System.Exception       {          pop          ldstr   "That wasn't a number"          call    void [mscorlib]System.Console::WriteLine(string)          leave.s Finish       }    }    finally    {        ldstr   "Thank you!"        call    void [mscorlib]System.Console::WriteLine(string)        endfinally    } Finish:    ret } 

The code contains two nested .try directives, one that is associated with the catch block, and one that is associated with the finally block. finally blocks are usually there to do essential cleanup of resources, but it's a bit hard to come up with a small IL sample that would use much in the way of resources, so we've used the finally block to display the Thank you! message, ensuring that this message will always be displayed.

You should be able to follow the logic of the code fairly well. However, notice that the concluding ret statement is located outside the .try blocks. That's because ret, in common with other branch instructions, cannot be used to leave a guarded block. The only way to leave a guarded block is with a leave or leave.s instruction. Because of this, we leave the .try or catch blocks with leave.s - specifying the Finish label as the branching point. Of course, in accordance with the normal rules for always executing finally, when execution hits the leave.s Finish instruction, control will actually transfer to the finally block. Execution will leave the finally block when the endfinally is reached - and at that point control will transfer to the destination of the leave.s instruction.

One other point to notice is that the catch block starts with a pop instruction. That's because when entering a catch block, the CLR will ensure that the caught exception is placed on the evaluation stack. We're not going to use this exception object here - all we're going to do is display a message, so we pop it from the stack before we do anything else.

One final point that we should mention: you might be wondering how a .try directive can appear in the middle of the instruction stream for a method, when .try, being a directive rather than an instruction, doesn't have an opcode. The answer is that it doesn't. The syntax we've been using above for .try blocks is supported by ILAsm, but isn't really related to the actual representation of guarded blocks in the assembly. Instead, there are separate structures at the end of the definition and code for each method, that define any guarded blocks (one token for each .try directive), and these tokens indicate the relative offsets in the method of the beginnings and ends of the guarded blocks, as well as any associated handler blocks. It is possible to write ILAsm code that uses a syntax that more directly maps onto this representation, but it's not recommended as it makes your code harder to read. Full details of the alternative syntax are in the Partition III documentation for IL.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net