Programming IL

In this section we will examine how you can perform various tasks using Intermediate Language (yes - we can finally get down to doing some programming!).

Defining Types and Namespaces

The samples we've presented so far contain just one global method, Main(). Of course, as we all know, .NET is supposed to be about object-oriented programming, and one of the principles of OOP is that global functions aren't really that desirable. If you code in C# you have to put the Main() method inside a class, and if you code in any language it's considered good .NET practice to define a namespace for your data types. So we'll now modify our Hello World program so that the entry point method is a static method of a class. This new sample is called HelloWorldClass. We'll call the class EntryPoint, and we'll put this class in a namespace, Wrox.AdvDotNet.ILChapter. I've also decided, just for this sample, to call the entry point method DisplayHelloWorld() rather than Main(), just to emphasize that there's nothing significant in the name Main() - it's the .entrypoint directive that indicates the startup method. Here's the new file:

 // This is a HelloWorld app with a class! .assembly extern mscorlib {} .assembly HelloWorldClass {    .ver 1:0:1:0 } .module HelloWorldClass.exe .namespace Wrox.AdvDotNet.ILChapter.HelloWorldClass {    .class public auto ansi EntryPoint extends [mscorlib]System.object    {       .method public static void DisplayHelloWorld() cil managed       {          .maxstack 1          .entrypoint          ldstr   "Hello, World"          call    void [mscorlib]System.Console::WriteLine(string)          ret       }    } } 

The HelloWorldClass program is almost identical to HelloWorld - I've just highlighted the differences.

The HelloWorldClass code shows that the syntax for declaring classes and namespaces is almost identical to that for high-level languages. In IL we use the .namespace and .class directives. We use the extends keyword to indicate a base class, Notice that the base class must be specified fully, including the name of the assembly in which it is contained. As we saw earlier when invoking the Console.WriteLine() method, there is no equivalent in IL to C#'s using statement or VB's Import statement. All names, including namespace names, must always be given in full.

There are a couple of other flags I've applied to the EntryPoint class in this code:

  • public has exactly the same meaning as in C++ and C#, and as Public in VB, and I've applied it to both the class and the DisplayHelloWorld() method. It indicates that this type is visible outside the assembly in which it is defined (as opposed to private types that are only visible within that assembly).

  • auto specifies the way that the class will be laid out in memory. There are three options here. auto allows the loader to lay the class out in whatever manner it sees fit - which normally means the class will be laid out to minimize its size while remaining consistent with hardware byte alignment requirements.sequential will cause the fields to be laid one after the other in memory (this is how unmanaged C++ classes are laid out).explicit indicates that the relative offset of each field is explicitly specified. As you can see, these options are equivalent to applying the StructLayout attribute in high-level source code.

  • ansi indicates how strings will be converted to native unmanaged strings if this is required by any P/Invoke calls associated with this class. ansi specifies that strings will be converted to ANSI strings. Other options are unicode (strings will be left in Unicode format) and autochar (the conversion will be determined by the platform the code is running on).

Since ansi and auto are the default specifiers for classes, our explicit inclusion of them here doesn't actually have any effect - we could have omitted them, but I wanted to be explicit about what was going on in the code and what options IL gives you. Similarly, marking the EntryPoint class as public won't really change anything, since we are not intending to invoke methods in this assembly from any other assembly.

Just as for most high-level languages, the IL assembler will assume [mscorlib]System.Object is the base class if we do not specify it explicitly. In place of the above code we could have written this for the class definition:

   .class public ansi auto EntryPoint   { 

If we want to define a value type instead of a class, we must declare [mscorlib]System.ValueType as the base class. In this case, you should also explicitly mark the class as sealed - since the .NET Framework requires value types to be sealed:

 .class public ansi auto sealed EntryPoint extends [mscorlib]System.ValueType { 

So far we've seen how to define classes that contain static methods. And, using Console.WriteLine() as an example, we've seen how to invoke static methods. That's actually all we'll be seeing of classes in this chapter. Dealing with instance methods is more complex, since that involves actually instantiating an object and passing an object reference to the method, so we'll leave that for the next chapter. Also, to keep the code displayed simple, for the remainder of this chapter we'll often not show the class and namespace that contains the Main() method, although it is present in the code downloads.

Member Accessibility Flags

We've seen that types can be public or private. Members of types (fields, methods, and so on) of course have a much greater range of accessibilities. The accessibilities allowed in IL are broadly the same as those in high-level languages, but the names may be different, and IL has a couple of additional accessibilities that are not available in languages like C# and VB.

The full list in IL is:

Accessibility

Visible to

C# Equivalent

VB Equivalent

public

All other code

public

Public

private

Code within the same class only

private

Private

family

Code in this class and derived classes

protected

Protected

assembly

Code in the same assembly

internal

Friend

familyandassem

Code in derived classes in this assembly

N/A

N/A

familyorassem

Code in derived classes, and any code in the same assembly

protected internal

Protected Friend

Privatescope

As for private, but privatescope items can have the same name and signature

N/A

N/A

As noted in the table, the privatescope accessibility is similar to private, but allows two methods to have the same signature or two fields to have the same name. You might wonder how this can work - it works within a single module because methods are always referred to in the actual PE file by an integer token, not by their signature (recall that the text-based name is just an artefact of the IL source code, or of the high-level language). privatescope is really intended for internal use by compilers; I wouldn't recommend you ever use it for writing direct IL.

You can also apply the above accessibilities to types that are defined inside other types, if you prefix the accessibility with nested:

 // Public class containing nested inner class, inner class only // visible in this assembly .class public OuterClass {    .class nested family TestClass    {       // etc. 

Conditional Statements and Branches

IL offers a number of commands to perform branching. These commands are equivalent to if, else if, and similar commands in higher-level languages.

Unconditional Branches

The simplest branching command is br, which performs an unconditional branch to a labeled statement. There is also a shortened form, br.s.

 br   GoHere   //or you can use br.s    GoHere // // Any other code here will be skipped after the br command // : GoHere: // The statement here will be the first one executed after the br command 

The syntax for labeling statements is similar to that in many high-level languages: you can label any statement by preceding it with some string (the label) followed by a colon. The colon is not included in references to the label.

This IL assembly syntax to some extent hides the way branching works in actual IL. The labels are only present in IL assembly, and are not propagated to the binary assembly. IL itself has no concept of a statement label. In the assembly itself, branching commands are actually followed by a signed integer that indicates the relative offset - how many bytes in the .exe or .dll file the execution flow should jump by. This number will be positive if we are branching to a point further on in the method or negative if we are branching to a command further back near the beginning of the method. An offset of zero will cause execution to immediately follow the statement following the branch command, as if the branch command weren't there.

However, working out the value of the offset is something that the ilasm assembler handles, so you don't need to worry about it. (If you prefer, you can indicate the numerical offset in the IL assembly code instead of supplying a label, but due to the difficulties of trying to work out manually what the offset is, that approach isn't recommended.)

The br statement and other branch statements we present here can only be used to branch within a method. You cannot transfer control to a different method (which makes sense, since that would cause problems about what to do with the contents of the evaluation stack, which is supposedly local to each method, as well as what to do about method arguments).

The br command allows for an offset between minus 0x80000000 bytes and plus 0x7fffffff bytes. If you know that the target of the branch is within -128 or +127 bytes of the branch statement, you can take advantage of the br.s statement, a shortened form of br, which takes an int8 instead of an int32 as its offset. Obviously, you should only use br.s in your IL assembly code if you are fairly sure the offset is less than 128 bytes. If you do use br.s and ilasm.exe computes the offset to be too large, it will refuse to assemble the file.

The br command has no effect on the stack contents, and so has a stack delta of zero:

Important 

... ...

Conditional Branches

IL offers a number of instructions that perform conditional branching, dependent on the contents of the top elements of the evaluation stack. We'll illustrate how these commands work by using the ble command, which compares two numbers on the stack and will transfer execution if the first number is less than or equal to the second. The following code snippet loads two integers onto the stack, then branches if the first is less than or equal to the second. Since the first number in this sample, -21, is less than the second number, +10, the condition is satisfied, and the control flow will branch. After the ble command is executed, the next statement to be executed will be whatever statement follows the FirstSmaller label:

 ldc.i4   -21 ldc,i4   10 ble      FirstSmaller // Intervening code. Any code here will not be executed after processing the // ble command FirstSmaller: // More code 

You may have guessed by now that in the process of taking the comparison, the ble command will pop the top two elements off the stack, and so has a stack delta of -2:

Important 

.., value, value...

In order to use this command, we need to understand what is meant by "first item" and "second item". The first item is the one that was pushed onto the stack first, and the second item is the one that was pushed onto the stack second. If you think about how the stack works, you'll see that this means the second item is the one that is at the top of the stack.

This is a general rule that applies to all IL commands that take more than one operand from the evaluation stack: the last operand will be the one right at the top of the stack - the one that can get popped off first. When we come to examine how to invoke methods that take more than one parameter, we will see that the same principle applies. The parameters are supplied by taking the final one from the top of the stack and working back to the first parameter. This means that if you are calling a method that takes multiple parameters, you must load the numbers onto the stack in the same order as the order of the parameters.

Besides the ble command, IL offers conditional branches based on all the usual arithmetic comparisons.

In all cases there is also an abbreviated command that is written by appending .s to the mnemonic for the usual form of the statement: ble.s. Just as with br.s, the shortened form restricts the offset to being within -128 or +127 bytes of the branch. Not only that, but there are also unsigned versions of all the comparative branch commands. The "unsigned" refers to the operands we are comparing, not the relative offset. In the previous example, the ble code snippet above would branch; the following code on the other hand, which uses the unsigned version of ble, will not branch because it will treat the two values as if they were unsigned numbers. This means it will treat the sign bit as if it were the most significant bit and will therefore conclude the first number is greater:

 ldc.i4   -21 ldc.i4   10 ble.un   FirstSmaller // Intervening code. This code will be executed since ble.un will not // branch here. FirstSmaller: // More code 

Unsigned comparison of signed numbers will always yield the (incorrect) result that a negative number is bigger than a positive number. So don't do unsigned comparison unless you know you are manipulating unsigned types!

The full list of conditional branches based on comparing two numbers is as follows:

Commands

Program flow will branch if...

beq, beq.s, beq.un, beq.un.s

First operand == Second operand

bne, bne.s, bne.un, bne.un.s

First operand != Second operand

bge, bge.s, bge.un, bge.un.s

First operand >= Second operand

bgt, bgt.s, bgt.un, bgt.un.s

First operand > Second operand

ble, ble.s, ble.un, ble.un.s

First operand <= Second operand

blt, blt.s, blt.un, blt.un.s

First operand < Second operand

There are also a couple of branch instructions based on an examination of just the top element of the evaluation stack. These will branch according to whether the top element is 0:

Commands

Program flow will branch if...

brfalse, brfalse.s

Top item on evaluation stack is zero

brtrue, brtrue.s

Top item on evaluation stack is not zero

We'll illustrate the use of the branch statements by writing another program, which we'll call CompareNumbers. This program will invite the user to input two numbers, and will inform the user which number was greater. This code will not only illustrate branching, but also show how the evaluation stack can be efficiently used. To make this code simpler, it contains the absolute minimum in terms of assembly and module directives that you can get away with. Here's the code:

 .assembly CompareNumbers {} .method static void Main() cil managed {    .maxstack 2    .entrypoint    ldstr   "Input first number."    call    void [mscorlib]System.Console::WriteLine(string)    call    string [mscorlib]System.Console::Readliine()    call    int32 [mscorlib]System.Int32::Parse(string)    Idstr   "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    Idstr   "The first number was larger than the second one"    call    void [mscorlib]System.Console::WriteLine(string)    br.s    Finish FirstSmaller:    ldstr   "The first number was less than or equal to the second one"    call    void [mscorliblSystem.Console::WriteLine(string) Finish:    ldstr   "Thank you!"    call    void [mscorlib]System.Console::WriteLine(string)    ret } 

It's quite instructive to work out what is happening to the evaluation stack as this program executes. Let's suppose that the user types in -21 for the first number and 10 for the second number. The program first loads a reference to the string "Input first number" to the stack, and writes it to the console.Calling Console.WriteLine() removes the string reference from the stack, which will now be empty. Then we call Console.ReadLine(). Since this method returns a string reference, the evaluation stack will now have a reference to a string on it. Calling Int32.Parse() to convert the string to an integer will pop the string reference from the stack and push the integer result onto the stack (the return value from this method call). Then we go through the process again for the second number, but in this case, the first number - the -21 - will sit unaffected on the stack while we input and parse the second string. When we come to the ble.s command, the stack will contain just the two numbers, -21 and 10. Since -21, the first number, is smaller, the branch will occur. The following table shows the contents of the stack as each statement is executed (note that for clarity I've abbreviated most statements in this table into a form that isn't syntactically correct IL):

Statement

Stack contents after executing statement

ldstr "Input first number."

Ref to "input first number."

call Console.WriteLine()

<Empty>

call Console.ReadLine()

"-21"

call Int32.Parse(string)

-21

ldstr "Input second number."

-21, Ref to "input second number."

call Console.WriteLine()

-21

call Console.ReadLine()

-21, "10"

call Int32.Parse(string)

-21, 10

ble.s FirstSmaller

<Empty>

... Branch happens

 

FirstSmaller:

ldstr "The first number was less than or equal to the second one"

Ref to "The first number was less than or equal to the second one"

call Console. WriteLine()

<Empty>

Finish:

ldstr "Thank you!"

Ref to "Thank you!"

call Console.WriteLine()

<Empty>

ret

 

We can see that in the CompareNumbers sample, I've neatly arranged the code so that the data we need is naturally stored on the evaluation stack in the correct order, so we haven't had to - for example - use any local variables to store any intermediate results. (Just as well really, since we haven't covered declaring local variables in IL yet!) This sample is actually quite a good illustration of how small and efficient you can make your IL if you code it up by hand. As an exercise, it's worth writing the same program in your favourite high-level language, compiling it, and using ildasm to examine the IL code produced. You'll almost certainly find that it is significantly longer - when I tried it in C# I found several intermediate results being stored in local variables. Whether this would lead to significantly higher performance after JIT compiling in such a simple case as this small sample is doubtful, since the JIT compiler itself will peform its own optimizations, but it does illustrate the potential for optimizing by writing directly in IL.

If you do try this comparison, be sure to do a Release build - Debug builds place a lot of extra code in the assemblies that is intended solely for debugging purposes. You should also use a debugger like cordbg instead of VS.NET because VS.NET may turn off optimizations. I'll show you how to examine optimized native code in .

Defining Methods with Arguments

So far, all our samples have only contained one method, the main entry point to the program, and this method has not taken any parameters. Although we've seen examples of calling a method that takes one parameter, we've not seen how to define such a method or access its arguments from within the method body. That's the subject of this section.

We are going to create a new sample, CompareNumbers2, by modifying the CompareNumbers sample so that the processing of figuring out which number is larger is carried out in a separate method, which will be invoked by the Main() method. For this purpose we will define a new class, MathUtils, which will contain one static method, FirstIsGreater(). This method takes two integers and returns a bool that will be true if first parameter is greater than the second. Here is what the class definition and method body look like:

 .namespace Wrox.AdvDotNet.CompareNumbers2 {    .class MathUtils extends [mscorlib]System.Object    {       .method public static bool       FirstIsGreater(int32 x, int32 y) cil managed       {          .maxstack 2          ldarg.0          ldarg.1          ble.s     FirstSmaller             ldc.i4.1             ret FirstSmaller:             ldc.i4.0             ret       }    } } 

The actual definition of the method shouldn't contain any surprises in syntax: we simply listed the parameter types in the brackets after the method name. Hence, as far as at this method is concerned, the first argument is simply argument 0, while the second is argument 1. Also, be aware that these arguments are passed by value - the same as the default behaviour in C#, C++, and VB.NET.

Within the method we indicate the maximum size of the evaluation stack as usual, and we use a couple of new commands, ldarg.0 and ldarg.1, to load the two arguments onto the stack:

          ldarg.0          ldarg.1 

This is where the first surprise occurs. The arguments are not referred to by name, but only by index, with the first argument being argument 0. The ldarg.0 command copies the value of the first argument onto the evaluation stack, while ldarg.1 does the same thing for the second argument. Having done that, we are back to roughly the same program logic as in our previous sample: we check to see which value is greater, and branch accordingly. In this case we want to return true if the first argument is greater than the second. To do this we simply place any non-zero value onto the stack and return:

             ldc.i4.1             ret 

Since the FirstIsGreater (> method returns a value, the definition of IL requires that this value should be the only item on the stack when the method returns - as is the case here. If we need to return false, then we simply place zero onto the evaluation stack and return:

 FirstSmaller:             ldc.i4.0             ret 

The bool return value is of course stored as an int32 on the evaluation stack. We use the usual convention that zero is false, while a non-zero value (normally one) is true.

Now let's examine the code we use to invoke this method:

       .method static void Main() cil managed       {          .maxstack 2          .entrypoint          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)          call    bool Wrox.AdvDotNet.CompareNumbers2.                           MathUtils::FirstIsGreater(int32, int32)          brfalse.s  FirstSmaller             ldstr   "The first number was larger than the second one"             call    void [mscorlib]System.Console::WriteLine(string)             br.s    Finish FirstSmaller:             ldstr   "The first number was less than or equal to the " +                      "second one"             call    void [mscorlib]System.Console::WriteLine(string) Finish:          ldstr   "Thank you!"          call    void [mscorlib]System.Console::WriteLine(string)          ret       } 

This code is pretty similar to the Main() method in the previous CompareNumbers sample, so only the differences have been highlighted above. After the user has typed in his chosen numbers, we call the FirstIsGreater() method. Notice how we've left the two numbers on the evaluation stack in the correct order to be passed to the method. Then we use the brfalse.s command to separate the execution paths according to whether the method returned true or false, in order to display an appropriate message.

More About Method Arguments

Let's have a look at those method arguments and also at the ldarg.* commands in more detail. The first observation we want to make is that, since we never actually used the names of the arguments in the method body of the FirstIsGreater() method, there wasn't strictly speaking any need to supply names. The sample would have compiled and worked just as well if we'd declared the method like this:

       .method public static bool FirstIsGreater(int32, int32) cil managed       { 

Indeed, if the method had been declared as private, privatescope, assembly, or familyandassem, it could have been advantageous to declare it like this (at least in a release build), on the basis that the less information you supply in the assembly, the harder it is for someone else to reverse-engineer your code. The lack of names also knocks a few bytes off the assembly size. However, for methods that are declared public, you really want the names of the parameters there in order to provide documentation for other people who might wish to use your code (actually, this argument is really relevant more for a library than an executable assembly, but you get the idea anyway.)

A good tip if you want your IL source code files to be easy to maintain without giving away variable names to users of your assembly is to add comments in your IL source code that give the names of variables. Since ilasm.exe will ignore the text of the comments, the information in them will not be propagated to the assemblies that you ship.

The ldarg.* family of commands includes several instructions, based on the usual principle of having the smallest commands for the most frequently used scenarios. Four commands, ldarg.0, ldarg.l, ldarg.2, and ldarg.3 are available to load the first four arguments onto the stack. For arguments beyond that, you can use ldarg.s, which takes one unsigned int8 argument specifying the parameter index, and hence can load method parameters with index up to 255. For example, to load parameter number 4 (the fifth parameter since the index is zero-based) of a method onto the stack, you could do this:

 ldarg.s   4 

If you have more than 255 parameters, ldarg is for you - this command takes an int32 as an argument, which gives you - shall we say - a lot of parameters. But if you do write a method where you actually have so many parameters that you need to use the ldarg command, please don't ask me to debug it!

Incidentally, if you do name your parameters, ilasm.exe gives you the option to specify the name with your ldarg.s or ldarg commands:

       .method public static bool       FirstIsGreater(int32 x, int32 y) cil managed       {          .maxstack 2          ldarg.s   x          ldarg.s   y 

ilasm.exe will automatically convert the names into the parameter indices when it emits the assembly. This may make your code easier to read, but will obviously (at least for the first four parameters) increase the size of the assembly, since ldarg.s takes up more space than ldarg.0, and so on.

Storing to Arguments

As a quick aside, we will mention one feature that can occasionally be useful. Besides loading a method argument onto the stack, you can also pop a value from the stack into an argument. The instructions that do this are starg.s and starg. These instructions both require an argument that gives the index of the argument whose value should be replaced. For example, to write the number 34 into the first argument, you would do this:

    ldc.s   34    starg.s 0 

However, it is important to understand that, because arguments to methods are always passed by value (and yes, I do mean always), the value so stored will not be passed back to the calling function. Using starg in this way amounts to using a slot in the argument table as a cheap way of providing an extra local variable, on the assumption that you no longer need the original value of that parameter - exactly like the following C# code (and this isn't really good programming practice anyway):

    void DoSomething(int x)    {       // some code       x = 34;   // using x like a local variable 

If you actually do want to return a value to a calling method via a parameter that is passed by reference then you'll need to use the stind instruction, which we'll examine soon.

Because starg isn't such a widely used instruction, there are no shorthand starg versions for the first four parameters, equivalent to ldarg.0.

This aside brings up the obvious question of how you declare and use genuine local variables.

Local Variables

Declaring and using local variables is done using a directive, .locals, at the beginning of a method. Here's how you would declare a void static method that takes no parameters, but has two local variables, an unsigned int32 and a string:

 .method static void DoSomething() cil managed {    .locals init (unsigned int32, string)    // code for method 

Just as for parameters, you don't need to name local variables. You can if you wish, but since local variables are never seen outside the method in which they are defined, there is no reason to make any names available to people who use your libraries - so for confidentiality reasons you'll probably want to leave local variables unnamed in release code - and for this reason high-level language compilers usually leave locals unnamed in release builds.

The init flag following the .locals directive will cause ilasm.exe to mark the method in the emitted assembly with a flag that tells the JIT compiler to initialize all local variables by zeroing them out. I would suggest you always set the init flag. If you don't, you may gain a tiny bit performance-wise, but your code will become less robust and will be unverifiable. Without the initialization flag set, you'll be responsible for making sure that you initialize each variable explicitly before you use it, and there's always the risk that when you next modify your code you'll make a mistake and end up with code that reads an uninitialized variable. As far as verifiability is concerned, it is possible in principle for a verifier to check that all variables are set before they are used, and it's always possible this might get added in a future version of .NET. But as of version 1, the .NET runtime takes the easy way out and says that if any method is invoked that doesn't have the init flag set, the code is not type-safe.

Lecture about initialization aside, using a local variable is done in much the same way as for a parameter. The instructions are ldloc.* to load a local variable to the stack and stloc.* to pop the value off the stack into a local variable. The actual ldloc.* and stloc.* instructions available follow the same pattern as ldarg. You have a choice between ldloc.0, ldloc.1, ldloc.2, ldloc.3, ldloc.s, and ldloc. Similarly for storing, we have stloc.0, stloc.1, stloc.2, stloc.3, stloc.s, and stloc. I'm sure by now you can guess the difference between these variants. And just as for ldarg, I trust that as an advanced .NET developer, you will never, ever, write a method so complex that you need to use the full ldloc and stloc commands.

As a quick example, this code snippet copies the contents of the second local variable into the fifth local variable (for this code snippet to be verifiable, the types of these variables must match):

 ldloc.1      // Remember that locals are indexed starting at zero stloc.s 4 

Loops

In high-level languages, you will be used to using a number of quite sophisticated constructs for loops, such as for, while, and do statements. IL, being a low-level language, does not have any such constructs. Instead, you have to build up the logic of a loop from the branching statements that we've already encountered. In this section we'll present a sample that demonstrates how to do this. The sample will also have the bonus of illustrating local variables in action.

For this sample, we'll add a new method to our MathUtilities class, called SumBetween().SumBetween() takes two integers as parameters, and then adds up all the numbers between the first parameter and the second. In other words, if you invoke SumBetween(), passing it the numbers 6 and 10, it will return the value 40 (6+7+8+9+10=40). If the first number is bigger than the second, you'll get zero back. So SumBetween() does exactly the same as this C# code:

 int CSharpSumBetween(int lower, int higher) {    int total = 0;    for (int i=lower; i<=higher; i++)       total += i;    return total; } 

For the purposes of this sample, we'll pretend we don't know that there is a mathematical formula available to compute the sum: (higher* (higher+1) -lower* (lower-1)) /2. This formula has the advantage of performance, but the disadvantage that it can't be used in a sample to demonstrate a loop.

Here's the IL code for the method (note that, as with all the IL code in this chapter, this is code that I've written by hand - it's not been generated by compiling the above C# code):

 .method assembly static int32 SumBetween(int32 lower, int32 higher) cil managed {    .maxstack 2    .locals init (int32, int32)    // local.0 is index, local.1 is running total    // initialize count    ldarg.0    stloc.0 Loop:    ldloc.0    ldarg.1    bgt Finished    // increment running total       ldloc.0       ldloc.1       add       stloc.l       // increment count       ldloc.0       ldo.i4.1       add       stloc.0       br.s    Loop Finished:    ldloc.1    ret } 

There are no new concepts in this code, but the manipulation we're doing is a bit more complex than anything we've done up to now, so I'll go over the code briefly. As usual, we start with the method signature and .maxstack declaration (2 again - it's amazing how far we've got in this chapter without wanting to put more than two items simultaneously on the evaluation stack). I've also declared two int32s as local variables, which will respectively store the index and the running total (equivalent to i and total in the C# code I previously presented).

The first thing I need to do in this code is initialize the count - it needs to start off having the same value as the first, hopefully lower, parameter:

    // initialize count    ldarg.0    stloc.0 

The next statement is labeled Loop, because that's where we'll need to keep branching back to until the loop is complete. We load our count and the second parameter - what should be the upper bound for our loop - and compare them. If the count is bigger, we've finished working out the sum, and can jump to the final bit of code, in which we load the value of the sum that we want to return, and call ret to exit the method. Otherwise, we need to go round the loop: we increment the total by adding the value of the count to it. Then we add one to the value of the count before using the br.s command to branch back to our comparison to test if we've completed the loop yet. And that's it! So simple that it almost makes you wonder why we ever needed for loops...

The full sample, including a suitable Main() method to call the loop, is downloadable as the SumBetween sample.

Passing by Reference

In this section we're going to start playing with managed pointers. We're going to investigate how you can use parameters to pass values back to calling methods - what in high-level languages you call passing by reference. This is going to bring in several new topics and IL instructions: we'll be manipulating managed pointers, we'll be loading addresses of variables, and we'll be using a stind instruction to do some indirect memory addressing.

I titled this section Passing by Reference with some reluctance, because strictly speaking, there's no such thing as passing by reference. When you call a method - any method - the values of the parameters are always copied across, that is to say passed by value. In high-level languages such as C#, C++, and VB, we often talk about 'passing by reference' (and despite all my denials I will use the same terminology here, as does some of the IL documentation), but what this actually means is that we are passing the address of a variable by value into the method. In other words, the method takes a copy of the address of some data in the calling method, and can de-reference this address to get at and possibly modify that data. C++, C#, and VB are all guilty of using some nifty syntax to hide what's actually happening, such as the ref keyword in C# and the ByRef keyword in VB. But in IL there is no such hiding. In IL, if you want the behavior that in high-level languages is termed passing by reference, then you'll have to explicitly declare and use the addresses of variables - which means using managed pointers.

Suppose you have a C# method declaration with a signature that looks like this:

 static void DoSomething(int x, ref int y) { 

The equivalent IL code will look like this:

 .method static void DoSomething(int32 x, int32 & y) cil managed { 

As you can see, the first parameter in the C# version, an int passed by value, maps smoothly onto the int32 type - which is just IL's name for the exact same type. However, the second parameter, an int passed by reference, has turned into an int32 & in IL - in other words, a managed pointer to an int32. Recall earlier in the chapter, when we listed the IL data types, we indicated that & denoted a managed pointer.

C++ developers should beware, since C++ uses the & syntax just like IL. But, like the C# ref keyword, C++ is using & as a bit of clever syntax to hide what's actually happening: & in C++ doesn't have quite the same meaning as & in IL. In IL, & really does mean we are declaring a pointer - to that extent & in IL is closer in concept to * in C++.

That's the theory, so how do we use it? The easiest way is probably to demonstrate the principle in action, so we're going to present another sample. For this sample we'll keep with our MathUtils class name, but this time add a method called Max(). This method is somewhat unusual - it is designed to work out which is the bigger of two integers that it is passed, and will return the index of the larger one - 0 or 1 (it will return -1 if the two integers are the same). However, it also maximizes the integers. Whichever one is the smaller one will get reset to the value of the larger one. In other words, if I call Max() passing it two variables containing 45 and 58, the first variable will get changed to 58, and I'll get 0 as the return value. Because I am expecting Max() actually to change the value of the data I pass to it, I'll have to pass in managed pointers. Or, to use the parlance of high-level languages, I'll have to pass the integers in by reference.

I'll admit this is a slightly odd spec for a method. It's not totally unreasonable, though - Max() is the sort of short method I might write for my own use in code as a private or assembly-visible method if I know there are several places in my code where I need to do something like what Max() does. So, to make the sample a bit more realistic, I've defined Max() to have assembly accessibility.

First of all, I'll present the code that actually invokes Max(). It's my Main() method, and it asks the user for two numbers, passes them to Max() to get them both maximized, then displays the results:

 .namespace Wrox.AdvDotNet.ILChapter.Max {    .class EntryPoint extends [mscorlib]System.Object    {       .method static void Main() cil managed       {          .maxstack 2          .locals init (int32, int32)          .entrypoint          ldstr    "Input First number."          call     void [mscorlib]System.Console::WriteLine(string)          call     string [mscorlib]System.Console::ReadLine()          call     int32 [mscorlib]System.Int32::Parse(string)          stloc.0          ldstr    "Input Second number."          call     void [mscorlib]System.Console::WriteLine(string)          call     string [mscorlib]System.Console::ReadLine()          call     int32 [mscorlib]System.Int32::Parse(string)          stloc.1          ldloga.s 0          ldloca.s 1          call     int8 Wrox.AdvDotNet.ILChapter.Max.MathUtils::Max(                                                            int32 &, int32 &)          ldstr    "Index of larger number was "          call     void [mscorliblsystem.Console::Write(string)          call     void [mscorlib]System.Console::WriteLine(int32)          ldstr    "After maximizing numbers, the numbers are:"          call     void [mscorlib]System.Console::WriteLine(string)          ldloc.0          call     void [mscorlib]System.Console::WriteLine(int32)          ldloc.1          call,    void [mscorlib]System.Console::WriteLine(int32)          ldstr    "Thank you!"          call     void (mscorlib]System.Console::WriteLine(string)          ret       }    } 

This code starts off in a similar manner to code in previous samples that asks the user to enter two numbers. However, there is one crucial difference: in previous samples, we've been able to get away with permanently leaving the two numbers the user types in on the evaluation stack, without having to store them anywhere else. We can't do that here. Now the numbers the user types in will have to be stored in local variables because we need to pass addresses for this data to the Max() method. IL gives us no way to get an address for data on the evaluation stack for the very good reason that the evaluation stack is no more than a useful IL abstraction and doesn't actually exist any more once the code has been JIT-compiled! The only way we can get addresses is actually to store these integers somewhere, so we declare two local variables and go on to ask the user for the numbers. As before, we grab the first string from the user and use Int32::Parse() to convert the string to an integer. But this time we store the result in the first local variable:

 ldstr    "Input Second number." call     void [mscorlib]System.Console::WriteLine(string) call     string [mscorlib]System.Console::ReadLine() call     int32 [mscorlib]System.Int32::Parse(string) stloc.l 

Then we do the same thing for the second number, this time storing it in the second local variable.

Next comes a new instruction that we've not encountered before, ldloca.s:

 ldloca.s 0 ldloca.s l call     int8 Wrox.AdvDotNet.ILChapter.Max.MathUtils::Max(int32 &, int32 &) 

ldloca.s is similar to ldloc.s. However, where ldloc.s pushes the value of the specified local variable onto the stack, ldloca.s instead loads the address of that variable, as a managed pointer. As you'll no doubt guess, ldloca.s is the short form of ldloca, and you can use ldloca.s for locals with index less than 256.

It's important to understand how IL treats the types here. In our code, local variable 0 is of type int32. The JIT compiler will see this and will therefore know that ldloca.s 0 will load a managed pointer to int32 - int32 &. Hence, after executing this instruction, as far as the JIT compiler is concerned, the top item on the stack is an int32 &. And type safety requires that that is what we treat the data as. The same thing applies to the second ldloca.s statement in the above code snippet. The JIT compiler will complain about any attempt to use the data on the stack as anything else (for example as an int32 instead of an int32 &). Fortunately, we are OK because the next command is a call to invoke Max().Max() will be defined as expecting two int32 & parameters, which will be popped off the stack. During its execution, the Max() method may or may not use the managed pointers now in its possession to modify the data in our two local variables, and after the call returns, whatever index Max() has returned with will be on the stack.

The rest of the code for the Main() method is relatively straightforward. We simply display the return value from Max() along with the values of the local variables:

 call     int8 Wrox.AdvDotNet.ILChapter.Max.MathUtils::Max(int32 &,                                                           int32 &) ldstr    "Index of larger number was;" call     void [mscorlib]System.Console::Write(string) call     void [mscorlib]System.Console::WriteLine(int32) 

The only point I would draw your attention to is the cunning way we've used the evaluation stack to avoid having to create a third local variable to hold the return value from Max(). We leave this value on the stack, push a string onto the stack above this value, then pop the string off again as we display it, so that the return value from Max() is once again at the top of the stack, ready to be passed to the Console.WriteLine() call.

Next we'll look at the implementation of Max():

    .class Mathutils extends [mscorlib]System.Object    {       // Max() works out which of two numbers is greater,       // returns the index of the greater number       // or -1 if numbers are equal,       // and sets lower number to equal higher one       .method assembly static int8 Max(int32 &, int32 &) cil managed       {          .maxstack 2          .locals init (int32, int32)          // Copy argument values to locals          ldarg.0          ldind.i4          stloc.0          ldarg.1          ldind.i4          stloc.1          // Now start comparing their values          ldloc.0          ldloc.1          blt.s    FirstIsLess          ldloc.0          ldloc.1          bgt.s    FirstIsBigger          // Both numbers are equal          ldc.i4.ml          ret FirstIsLess:          ldarg.0          ldloc.1          stind.i4          ldc.i4.1          ret FirstIsBigger:          Idarg.1          ldloc.0          stind.i4          ldc.i4.0          ret       } 

Let's go through this code in detail. This method also has two local variables, which I'm going to use to store local copies of the integers we want to maximize. The reason for taking local copies is that we're going to have to push them onto the evaluation stack several times, and I don't want to have to go through the process of de-referencing managed pointers every time. The first thing we're going to do is de-reference those managed pointers and copy the result into our local variables:

       // Copy argument values to locals       ldarg.0       ldind.i4       stloc.0       ldarg.1       ldind.i4       stloc.1 

We use ldarg.0 to copy the first parameter onto the evaluation stack. Now recall that the first parameter passed to this method is the address, of type int32 &. So after executing this command, the stack will contain one item, of type int32 &. Next is a new instruction, ldind.i4.ldind stands for "load indirect", and takes the top item on the stack, which must be a pointer, pops that item off the stack, de-references it, and loads the data at that address.

Important 

..., address..., value

So, after executing the ldind.i4 instruction, the stack will contain a copy of the first number the user typed in Main(), which we then copy to our first local variable using stloc.0. Then we do the whole thing again for the second number.

ldind.i4 is just one of a whole family of ldind.* instructions, including ldind.i1 ldind.i2, ldind.i4, ldind.i8, ldind.r4, ldind.r8, as well as unsigned equivalents and a couple of others - the full list is in the appendix. The difference between these instructions lies in the data type that they are expecting to de-reference. For example, ldind.i4 is expecting to find an int32 & (or an unmanaged pointer to int32, int32*) on the stack, de-reference it, and store it in a local variable of type int32. If we'd used (say) ldind.r4, which expects a float32 &, in our code instead, the JIT compiler would refuse to compile it. This is both because the wrong pointer data type is on the stack, and because local variable slot 0 would be of the wrong data type. IL might be a low-level language but it is still exceedingly type-safe.

The next instructions don't contain anything particularly new. We simply load the newly stored local variables on the stack and test first of all to see if the first one is smaller than the second and then to see if the first one is bigger than the second. We have two tests because we need to distinguish three cases: the first is bigger, the first is smaller, or both numbers are the same:

       // Now start comparing their values       ldloc.0       ldloc.1       blt.s FirstIsLess       ldloc.0       ldloc.1       bgt.s FirstIsBigger 

If the two numbers are equal, then both tests in the above code will fail, and we can go straight on to returning a value of -1 to the calling routine:

       // both numbers are equal       ldc.i4.m1       ret 

However, if one number is bigger, we need to modify the value of the smaller number in the dereferenced parameter to the method. We'll only present one of the cases, since the logic is the same in both cases. This is how we do it if the first number is smaller:

 FirstIsLess:          ldarg.0          ldloc.1          stind.i4          ldc.i4.1          ret 

We need to change the value of the first number, and return 1, the index of the larger number. In order to change the de-referenced first parameter, we need a new instruction, stind.i4. stind.i4 is similar to ldind.i4, except that it stores data to a given address rather than loading the data at the given address. This means it will pop two values off the stack: the data to be stored and the address at which it will be stored.

Important 

..., address, value...

We set up the evaluation stack ready for stind by first pushing the address onto the stack (that's obtained from ldarg.0), then pushing the data onto the stack (that's the value of the second local variable). Then we call stind.i4, before finally pushing the return value of 1 onto the stack and returning.

Like ldind.i4, stind. i4 is one of a family of stind instructions, each of which deals with a different data type. And once again, the full list of instructions is in the appendix.



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