-5-

 

Operator Overloading

 

Every operator overload that we use in C#, gets converted to a function call in IL. The overloaded > operator translates into the function op_GreaterThan and a + gets converted to op_Addition etc. In the first program of this chapter, we have overloaded the + operator in class yyy to facilitate adding of two yyy objects.

 

a.cs

public class zzz

{

public static void Main()

{

yyy a = new yyy(10);

yyy b = new yyy(5);

yyy c;

c = a + b ;

System.Console.WriteLine(c.i);

}

}

public class yyy

{

public int i;

public yyy( int j)

{

i = j;

}

public static yyy operator + ( yyy x , yyy y) {

System.Console.WriteLine(x.i);

yyy z = new yyy(12);

return z;

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0,class yyy V_1,class yyy V_2)

ldc.i4.s 10

newobj instance void yyy::.ctor(int32)

stloc.0

ldc.i4.5

newobj instance void yyy::.ctor(int32)

stloc.1

ldloc.0

ldloc.1

call class yyy yyy::op_Addition(class yyy,class yyy)

stloc.2

ldloc.2

ldfld int32 yyy::i

call void [mscorlib]System.Console::WriteLine(int32)

ret

ret

}

}

.class public auto ansi yyy extends [mscorlib]System.Object

{

.field public int32 i

.method public hidebysig specialname static class yyy op_Addition(class yyy x,class yyy y) il managed

{

.locals (class yyy V_0,class yyy V_1)

ldarg.0

ldfld int32 yyy::i

call void [mscorlib]System.Console::WriteLine(int32)

ldc.i4.s 12

newobj instance void yyy::.ctor(int32)

stloc.0

ldloc.0

stloc.1

ldloc.1

ret

}

.method public hidebysig specialname rtspecialname instance void .ctor(int32 j) il managed

{

ldarg.0

call instance void [mscorlib]System.Object::.ctor()

ldarg.0

ldarg.1

stfld int32 yyy::i

ret

}

}

 

Output

10

12

 

While using the plus (+) operator on the two yyy objects, C# is aware that IL does not support operator overloading. Therefore, it creates a function called op_Addition in the class yyy.

 

Thus, operator overloading gets represented as a mere function call. The rest of the code is easy for you to figure out.

 

In IL, there is no rule stating that if the > operator is overloaded, then the < operator also has to be overloaded. These rules are imposed by the C# compiler, and not by IL since, IL does not support the concept of overloading at all.

 

a.cs

public class zzz

{

public static void Main()

{

yyy a = new yyy();

System.Console.WriteLine(a);

}

}

public class yyy

{

public static implicit operator string(yyy y)

{

System.Console.WriteLine("operator string");

return "yyy class " ;

}

public override string ToString()

{

System.Console.WriteLine("ToString");

return "mukhi";

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0)

newobj instance void yyy::.ctor()

stloc.0

ldloc.0

call class System.String yyy::op_Implicit(class yyy)

call void [mscorlib]System.Console::WriteLine(class System.String)

ret

}

}

.class public auto ansi yyy extends [mscorlib]System.Object

{

.method public hidebysig specialname static class System.String op_Implicit(class yyy y) il managed

{

.locals (class System.String V_0)

ldstr "operator string"

call void [mscorlib]System.Console::WriteLine(class System.String)

ldstr "yyy class "

stloc.0

ldloc.0

ret

}

.method public hidebysig virtual instance class System.String ToString() il managed

{

.locals (class System.String V_0)

ldstr "ToString"

call void [mscorlib]System.Console::WriteLine(class System.String)

ldstr "mukhi"

stloc.0

ldloc.0

ret

}

}

 

Output

operator string

yyy class

 

The C# compiler is extremely intelligent. Whenever a yyy object has to be converted to a string, it first checks for the presence of an operator called string in the class yyy. If it exists, it calls that operator.

 

The operator named string is a predefined data type in C#. Hence, it is converted into the operator op_Implicit. This operator takes a yyy object as a parameter. It returns a string on the stack for the WriteLine function. The ToString function is not called.

 

C# will generate an error if you alter even a single parameter to the operator string, but such is not a case with IL as it does not support operator overloading and conversions.

 

a.cs

public class zzz

{

public static void Main()

{

yyy a = new yyy();

System.Console.WriteLine(a);

}

}

public class yyy

{

public override string ToString()

{

System.Console.WriteLine("ToString yyy");

return "mukhi";

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0)

newobj instance void yyy::.ctor()

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(class System.Object)

ret

}

}

.class public auto ansi yyy extends [mscorlib]System.Object

{

.method public hidebysig virtual instance class System.String ToString() il managed

{

.locals (class System.String V_0)

ldstr "ToString yyy"

call void [mscorlib]System.Console::WriteLine(class System.String)

ldstr "mukhi"

stloc.0

ldloc.0

ret

}

.method public hidebysig specialname rtspecialname instance void .ctor() il managed

{

ldarg.0

call instance void [mscorlib]System.Object::.ctor()

ret

}

}

 

Output

ToString yyy

mukhi

 

In the C# code above, we have dispensed with the operator string and instead, have used the ToString function. As usual, we put the object a on the stack. In the IL code given earlier, due to the presence of operator overloads in the C# code, the function op_Implicit was called. In this case, since there are no operator overloads, the object reference to object a is simply put on the stack. In class yyy, even though, the function ToString is not explicitly called, the function does get executed.

 

Since the ToString is virtual in the class Object, at run time, the ToString function is called from the class yyy, instead of being called from the class Object. This is due to the concept of a vtable, where all virtual function addresses reside.

 

If the word virtual is removed from the function, the ToString function gets called from the class Object instead of the class yyy.

 

a.cs

public class zzz

{

public static void Main()

{

yyy a = new yyy();

string s;

s = (string)a;

System.Console.WriteLine(s);

}

}

public class yyy

{

public static explicit operator string(yyy y)

{

System.Console.WriteLine("operator string");

return "string yyy" ;

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0,class System.String V_1)

newobj instance void yyy::.ctor()

stloc.0

ldloc.0

call class System.String yyy::op_Explicit(class yyy)

stloc.1

ldloc.1

call void [mscorlib]System.Console::WriteLine(class System.String)

ret

}

}

.class public auto ansi yyy extends [mscorlib]System.Object

{

.method public hidebysig specialname static class System.String op_Explicit(class yyy y) il managed

{

.locals (class System.String V_0)

ldstr "operator string"

call void [mscorlib]System.Console::WriteLine(class System.String)

ldstr "string yyy"

stloc.0

ldloc.0

ret

}

}

Output

operator string

string yyy

 

In the above code, we have cast a yyy object into a string using an explicit cast. IL does not understand C# keywords like implicit or explicit. It converts the cast to an actual function such as op_Explicit or op_Implicit. Thus writing a C# compiler requires a lot of grey matter.

 

a.cs

public class zzz

{

public static void Main()

{

yyy a ;

a = 10;

}

}

public class yyy

{

static public implicit operator yyy(int v)

{

System.Console.WriteLine(v);

yyy z = new yyy();

return z;

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0)

ldc.i4.s 10

call class yyy yyy::op_Implicit(int32)

stloc.0

ret

}

}

.class public auto ansi yyy extends [mscorlib]System.Object

{

.method public hidebysig specialname static class yyy op_Implicit(int32 v) il managed

{

.locals (class yyy V_0,class yyy V_1)

ldarg.0

call void [mscorlib]System.Console::WriteLine(int32)

newobj instance void yyy::.ctor()

stloc.0

ldloc.0

stloc.1

ldloc.1

ret

}

}

 

Output

10

 

In the code above, we are not creating an object that is an instance of class yyy. Instead, we are simply initializing it to a numeric value of 10. This results in a call to the implicit operator yyy, which takes an int value as a parameter and creates a yyy object.

 

The IL code does not understand any of this. It simply calls the relevant operator, which in this case is op_Implicit, with an int value. It is the responsibility of this function to create an object that is an instance of class yyy. We are, in effect, creating two locals that look like yyy, and initializing them to the new yyy like object on the stack. Finally its value,10, is put on the stack.

 

a.cs

class zzz

{

public static void Main()

{

yyy a = new yyy();

yyy b = new yyy();

System.Console.WriteLine( a && b);

System.Console.WriteLine( a & b);

}

}

class yyy

{

public static yyy operator & (yyy x,yyy y)

{

System.Console.WriteLine("op &" );

return new yyy();

}

public static bool operator true(yyy x)

{

System.Console.WriteLine("true ");

return true;

}

public static bool operator false(yyy x)

{

System.Console.WriteLine("false " );

return true;

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class yyy V_0,class yyy V_1)

newobj instance void yyy::.ctor()

stloc.0

newobj instance void yyy::.ctor()

stloc.1

ldloc.0

dup

call bool yyy::op_False(class yyy)

brtrue.s IL_001b

ldloc.1

call class yyy yyy::op_BitwiseAnd(class yyy,class yyy)

IL_001b: call void [mscorlib]System.Console::WriteLine(class System.Object)

ldloc.0

ldloc.1

call class yyy yyy::op_BitwiseAnd(class yyy,class yyy)

call void [mscorlib]System.Console::WriteLine(class System.Object)

ret

}

}

.class private auto ansi yyy extends [mscorlib]System.Object

{

.method public hidebysig specialname static class yyy op_BitwiseAnd(class yyy x,class yyy y) il managed

{

.locals (class yyy V_0)

ldstr "op &"

call void [mscorlib]System.Console::WriteLine(class System.String)

newobj instance void yyy::.ctor()

stloc.0

ldloc.0

ret

}

.method public hidebysig specialname static bool op_True(class yyy x) il managed

{

.locals (bool V_0)

ldstr "true "

call void [mscorlib]System.Console::WriteLine(class System.String)

ldc.i4.1

stloc.0

ldloc.0

ret

}

.method public hidebysig specialname static bool op_False(class yyy x) il managed

{

.locals (bool V_0)

ldstr "false "

call void [mscorlib]System.Console::WriteLine(class System.String)

ldc.i4.1

stloc.0

ldloc.0

ret

}

}

 

Output

false

System.Object

op &

System.Object

 

In the above code, we have created two objects, a and b, that are instances of a class yyy. Then, we have employed the overloaded operators & and && to determine as to how IL handles them internally. If we can grasp the intricacies of IL, our understanding of C# will become so much better. Maybe, a programmer should be allowed to program in C# only if he/she has learnt IL.

 

The dup operator duplicates the value present at the top of the stack. In this case, it is the local V_0. All occurences of && and & in the C# code are replaced by the functions op_False and op_BitwiseAnd respectively, on conversion to IL code.

 

The op_False operator returns either TRUE or FALSE.

 

If it returns TRUE, then the answer is TRUE, and the rest of the condition is not checked. This is how the code is short-circuited. We simply jump past code that is not to be executed.

If it returns FALSE, the & operator gets called. This operator gets converted to op_BitwiseAnd. In order to enhance the efficiency, the two objects were already present on the stack for the op_BitwiseAnd operator to act upon.

 

You will be appreciate that IL makes our understanding of abstract concepts of C# much easier to understand.

 

 

 

a.cs

class zzz

{

public static void Main()

{

System.Type m;

m = typeof(int);

System.Console.WriteLine(m.FullName);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed {

.entrypoint

.locals (class [mscorlib]System.Type V_0)

ldtoken [mscorlib]System.Int32

call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(value class [mscorlib]System.RuntimeTypeHandle)

stloc.0

ldloc.0

callvirt instance class System.String [mscorlib]System.Type::get_FullName()

call void [mscorlib]System.Console::WriteLine(class System.String)

ret

}

}

 

Output

System.Int32

 

In IL, the object m is a local named V_0 of type System.Type. In C#, the typeof keyword returns a Type object, but in IL, a large number of steps have to be executed to achieve the same result.

 

Firstly, a type is placed on the stack using the instruction ldtoken. This loads a token that represents a type or a field or a method.

Next, the function GetTypeFromHandle is called that picks up a token, i.e. a structure or value class from the stack.

The function thereafter returns a Type object representing a type, which in this case is an int. This is stored in the local V_0 and then again loaded on the stack.

Next, the function get_FullName is called. The function is not called FullName but get_FullName as it is a property. This property returns a string on the stack that is displayed using the WriteLine function.

 

a.cs

class zzz

{

public static void Main()

{

zzz z = new zzz();

z.abc(z);

object o = new object();

z.abc(o);

}

void abc(object a)

{

if ( a is zzz)

System.Console.WriteLine("zzz");

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0,class System.Object V_1)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

ldloc.0

call instance void zzz::abc(class System.Object)

newobj instance void [mscorlib]System.Object::.ctor()

stloc.1

ldloc.0

ldloc.1

call instance void zzz::abc(class System.Object)

ret

}

.method private hidebysig instance void abc(class System.Object a) il managed

{

ldarg.1

isinst zzz

brfalse.s IL_0012

ldstr "zzz"

call void [mscorlib]System.Console::WriteLine(class System.String)

IL_0012: ret

}

.method public hidebysig specialname rtspecialname instance void .ctor() il managed

{

ldarg.0

call instance void [mscorlib]System.Object::.ctor()

ret

}

}

 

Output

zzz

 

The keyword is lets us determine the data type of an object at run-time. Thus the is keyword of C# has an equivalent instruction in IL.We are passing a zzz like object and an object that is an instance of class object to the function abc. This function demotes every parameter it receives to a class object, but the is keyword is intelligent enough to know that the run time data type can be of a type other than an object. Thus, it returns TRUE for the z object, but not for the a object.

 

The assembler code in Main or vijay remains the same. The relevant source code is present in the function abc.

The instruction ldarg.1 pushes the value of parameter 1 onto the stack. The data type of this parameter is Object.

Next, the instruction isinst is called. The type with which we want to compare the object on the stack is passed as a parameter to isinst. This instruction determines the data type of the value present on the stack.

If the type of the isint instruction matches what is already there on the stack, the object remains on the stack. If it does not match, a NULL is placed on the stack.

The brfalse instruction executes the jump to a label if the result is TRUE in the il code.

 

a.cs

class zzz {

public static void Main()

{

abc(100);

abc("hi");

}

static void abc( object a) {

string s;

s = a as string;

System.Console.WriteLine(s);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32 V_0)

ldc.i4.s 100

stloc.0

ldloca.s V_0

box [mscorlib]System.Int32

call void zzz::abc(class System.Object)

ldstr "hi"

call void zzz::abc(class System.Object)

ret

}

.method private hidebysig static void abc(class System.Object a) il managed

{

.locals (class System.String V_0)

ldarg.0

isinst [mscorlib]System.String

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(class System.String)

ret

}

}

 

Output

 

hi

 

The keyword as is similar to the is. Two objects have been placed on the stack and the function abc is called. This function requires an object on the stack. The type of the variable a has to be converted from int to an Object. The isinst instruction takes value at the top of the stack and converts it into the data type specified. If it is unable to do so, it puts a NULL on the stack.

In the second call, on the stack, a string is obtained for the WriteLine function. Since an int32 value cannot be converted into a string, a NULL value is placed on the stack. Hence the WriteLine function displays a blank line.

 

Unsafe Code

 

a.cs

class zzz

{

unsafe public static void Main()

{

System.Console.WriteLine(sizeof(byte *));

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

sizeof unsigned int8*

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

4

 

All pointers in C# have a size of 4 bytes each. The sizeof keyword is an instruction in IL that returns the size of the variable that is passed as a parameter to it. It can only be used on a value type variable, not on a reference type.

 

In C# we use the modifier unsafe while introducing pointers. This modifier does not exist in IL, as IL regards everything as unsafe. Note that a byte in C# is converted into an int8 in IL.

 

a.cs

class zzz

{

public static void Main()

{

zzz a = new zzz();

a.abc();

}

unsafe public void abc()

{

int *i;

int j=1;

i = &j;

System.Console.WriteLine((int)i);

*i = 10;

System.Console.WriteLine(j);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

call instance void zzz::abc()

ret

}

.method public hidebysig instance void abc() il managed {

.locals (int32* V_0,int32 V_1)

ldc.i4.1

stloc.1

ldloca.s V_1

stloc.0

ldloc.0

conv.i4

call void [mscorlib]System.Console::WriteLine(int32)

ldloc.0

ldc.i4.s 10

stind.i4

ldloc.1

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

6552336

10

 

In the following program, the main function calls a function called abc. That part of the code has already been explained previously. The remaining part of the code is explained in the next few lines in bullet form.

 

In C#, whenever we want to obtain the address of a variable, we have to precede the name of the variable with the symbol &. The & places the address of a variable on the stack. IL interprets a pointer as a data type.

 

We start by creating a pointer to an int i in C#. V_0 is interpreted as a pointer due to the * sign that precedes it.

Next, we initialize the variable j or V_1 to the value 1.

The instruction ldloca.s places the address of j or V_1 on the stack.

The instruction stloc.0 initializes V_0 to this value i.e. the address of j or V_1.

The instruction ldloc.0 then places the value of the pointer on the stack and calls the WriteLine function with an int as a parameter.

We then place the value of the pointer that is pointing to int j in memory, on the stack.

Next, we place the number 10 on the stack.

The instruction stind places the current value on the stack i.e. 10 into the memory location placed earlier on the stack. Thus, we have utilised stind to fill up a certain memory location with a specific value. This value is the address of the variable j in memory.

The WriteLine function is finally called to display the new value of the variable j.

 

a.cs

class zzz

{

public static void Main()

{

zzz a = new zzz();

a.abc();

}

unsafe public void abc()

{

int *i;

int j=1;

i = &j;

System.Console.WriteLine((int)i);

i++;

System.Console.WriteLine((int)i);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

call instance void zzz::abc()

ret

}

.method public hidebysig instance void abc() il managed

{

.locals (int32* V_0,int32 V_1)

ldc.i4.1

stloc.1

ldloca.s V_1

stloc.0

ldloc.0

conv.i4

call void [mscorlib]System.Console::WriteLine(int32)

ldloc.0

ldc.i4.4

add

stloc.0

ldloc.0

conv.i4

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

6552336

6552340

 

The above program is presented to demonstrate that the C# compiler understands pointer arithmetic, whereas IL does not.

 

The crucial line in the above code is the one that contains the code ldc.i4.4. The C# compiler calculates that a pointer to an int has a size of 4 and therefore, it puts this instruction in the IL code to facilitate pointer arithmetic.

 

Had we replaced the int by a short, the C# compiler would have replaced the ldc instruction with the code ldc.i4.2 because, it is aware that the size of a pointer to short is 2. Thus, we can safely conclude that it is the C# compiler that understands pointer arithmetics and not IL.

 

a.cs

class zzz

{

public static unsafe void Main()

{

int* i = stackalloc int[100];

}

}

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32* V_0)

ldc.i4.4

ldc.i4.s 100

mul

localloc

stloc.0

ret

}

}

 

In C#, the stackalloc function allocates a certain amount of memory on the stack whereas, new allocates memory on the heap. Heap memory is longer lasting than stack memory.

 

The equivalent of this function in the IL instruction set is localloc. The parameter to this function specifies the amount of memory to be allocated. In the C# program, we have specified that we want to allocate memory for 100 ints. Since each int requires 4 bytes of memory, in IL, the numbers 4 and 100 are put on the stack and they are multiplied using the mul operator. Thus, a total of 400 bytes of memory are finally allocated.

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay(class System.String[] a, int32 i ) il managed

{

.entrypoint

ldarg.0

ldlen

conv.i4

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

Exception occurred: System.MethodAccessException: The signature for the entry point has too many arguments.

 

 

The assembler does not check the signature for the entrypoint function. But at run-time, the signature is checked to confirm whether it has only one parameter or not. Since there are two parameters in the entrypoint function, the run time exception has been generated. If there had been a single int parameter, no exception would have occurred at run-time.

The directive entrypoint cannot be present in more that one function, even if they are in separate classes. This is already illustrated in Chapter 1.

 

Enums

 

a.cs

class zzz

{

public static void Main()

{

System.Console.WriteLine(yyy.black);

}

}

enum yyy

{

a1,black,hell

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (value class yyy V_0)

ldc.i4.1

stloc.0

ldloca.s V_0

box yyy

call void [mscorlib]System.Console::WriteLine(class System.Object)

ret

}

}

.class value private auto ansi serializable sealed yyy extends [mscorlib]System.Enum

{

.field public specialname rtspecialname int32 value__

.field public static literal value class yyy a1 = int32(0x00000000)

.field public static literal value class yyy black = int32(0x00000001)

.field public static literal value class yyy hell = int32(0x00000002)

}

 

Output

1

 

An enum is implemented as a class that is serializable. This means that the CLR can write it to a disk or send it over a network. It extends the class System.Enum.

 

In the C# program, three enums are created. On conversion to IL, three corresponding literal fields with the same names are created. The values of the enum variables are calculated at compile time. There is a special variable introduced called value__.

 

Also, in the function vijay, the value of enum 'black' is being displayed. Observe carefully, there is no mention of 'black' in the generated IL code.

 

IL handles this situation in the following chronological steps:

 

First, it puts the number 1 on the stack.

Then, it stores this value 1 in the yyy value class or structure V_0.

Next, it uses ldloca.s to place the address of the variable V_0 on the stack.

Thereafter, it uses box to convert it into an object.

Finally, the value 1 is stored in the value class yyy using instruction stloc.0.

 

Thus, it may be appreciated that IL discards all the enum names and only deals with the values. However, we cannot get rid of the special variable value__ because its omission will result in an error at run time.

 

a.cs

public enum aa : byte

{

a1,a2,a3

}

class zzz

{

public static void Main()

{

System.Console.WriteLine(10 + aa.a2);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (value class aa V_0)

ldc.i4.s 11

stloc.0

ldloca.s V_0

box aa

call void [mscorlib]System.Console::WriteLine(class System.Object)

ret

}

}

.class value public auto ansi serializable sealed aa extends [mscorlib]System.Enum

{

.field public specialname rtspecialname unsigned int8 value__

.field public static literal value class aa a1 = int8(0x00)

.field public static literal value class aa a2 = int8(0x01)

.field public static literal value class aa a3 = int8(0x02)

}

 

Output

11

 

It can be seen from the code above that in the IL file, the expression 10 + aa.a2 is conspicuous by its absence. On generation of the IL code, the expression gets  converted to its actual value i.e. 11.

 

After examining the above code, we can be rest assured that enums, like other artefacts mentioned earlier, exist only in the realm of C# and have no direct representation in IL.

 

a.cs

class zzz

{

public static void Main() {

System.Console.WriteLine(yyy.a1 == yyy.a2);

}

}

enum yyy

{

a1 = 1,a2 = 4

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

ldc.i4.0

call void [mscorlib]System.Console::WriteLine(bool)

ret

}

}

.class value private auto ansi serializable sealed yyy extends [mscorlib]System.Enum

{

.field public specialname rtspecialname int32 value__

.field public static literal value class yyy a1 = int32(0x00000001)

.field public static literal value class yyy a2 = int32(0x00000004)

}

Output

False

 

When we try to compare an enum with a number using the comparison operator ==, this operator gets replaced with the value FALSE at run time. Therefore, the IL code that is generated is vastly at variance with the original C# code.

 

Switch

 

a.cs

public class zzz

{

public static void Main()

{

zzz a = new zzz();

a.abc(1);

a.abc(10);

}

void abc(int i)

{

switch (i)

{

case 0:

System.Console.WriteLine("zero");

break;

case 1:

System.Console.WriteLine("one");

break;

default:

System.Console.WriteLine("end");

}

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed {

.entrypoint

.locals (class zzz V_0)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

ldc.i4.1

call instance void zzz::abc(int32)

ldloc.0

ldc.i4.s 10

call instance void zzz::abc(int32)

ret

}

.method private hidebysig instance void abc(int32 i) il managed

{

.locals (int32 V_0)

ldarg.1

stloc.0

ldloc.0

switch ( IL_0012,IL_001e)

br.s IL_002a

IL_0012: ldstr "zero"

call void [mscorlib]System.Console::WriteLine(class System.String)

br.s IL_0034

IL_001e: ldstr "one"

call void [mscorlib]System.Console::WriteLine(class System.String)

br.s IL_0034

IL_002a: ldstr "end"

call void [mscorlib]System.Console::WriteLine(class System.String)

IL_0034: ret

}

}

 

Output

one

end

 

The switch statement of C# is converted to the switch instruction in IL. This instruction checks the value at the top of the stack and accordingly branches to the relevant label.

If the value is 0, it branches to the label IL_0012.

If the value is 1, it branches to the label IL_001e and so on.

If none of the cases match, the default clause will apply. In this case, the br.s IL_002a instruction is executed.

 

a.cs

public class zzz

{

public static void Main()

{

zzz a = new zzz();

a.abc(0);

a.abc(10);

}

void abc(int i)

{

switch (i)

{

case 0:

System.Console.WriteLine("zero");

break;

case 5:

System.Console.WriteLine("one");

break;

default:

System.Console.WriteLine("end");

}

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

ldc.i4.0

call instance void zzz::abc(int32)

ldloc.0

ldc.i4.s 10

call instance void zzz::abc(int32)

ret

}

.method private hidebysig instance void abc(int32 i) il managed

{

.locals (int32 V_0)

ldarg.1

stloc.0

ldloc.0

ldc.i4.0

beq.s IL_000c

ldloc.0

ldc.i4.5

beq.s IL_0018

br.s IL_0024

IL_000c: ldstr "zero"

call void [mscorlib]System.Console::WriteLine(class System.String)

br.s IL_002e

IL_0018: ldstr "one"

call void [mscorlib]System.Console::WriteLine(class System.String)

br.s IL_002e

IL_0024: ldstr "end"

call void [mscorlib]System.Console::WriteLine(class System.String)

IL_002e: ret

}

}

 

Output

zero

end

 

In the previous example, we consciously used consecutive values such as 0, 1 and so on. In this example, we have used discontinuous values like 0 and 5.

 

On conversion to IL code, we do not see the instruction switch, but instead, we see a series of jumps. The instruction beq.s is based on ceq and brtrue.s.

We place the individual case values on the stack and use beq.s to check whether it returns TRUE or FALSE.

 

If it is TRUE, we execute the relevant code and jump to the ret instruction.

If it is FALSE, the next case value on the stack is checked.

Finally, if none of the beq.s instructions result in TRUE, the default clause, which is at the end of the switch constuct, is executed.

 

Just as we do not have the equivalent of the if statement in IL, we also do not have a pure corresponding switch instruction in IL. The switch is more of a convenience to programmers of C#. The rule that a case has to end with a break statement, do not apply in IL.

 

Checked and Unchecked

 

a.cs

class zzz

{

int b = 1000000;

int c = 1000000;

public static void Main()

{

zzz a = new zzz();

a.pqr(a.b,a.c);

a.xyz(a.b,a.c);

}

int pqr( int x, int y)

{

return unchecked(x*y);

}

int xyz( int x, int y)

{

return checked(x*y);

}

}

 

 

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.field private int32 b

.field private int32 c

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0)

newobj instance void zzz::.ctor()

stloc.0

ldloc.0

ldloc.0

ldfld int32 zzz::b

ldloc.0

ldfld int32 zzz::c

call instance int32 zzz::pqr(int32,int32)

pop

ldloc.0

ldloc.0

ldfld int32 zzz::b

ldloc.0

ldfld int32 zzz::c

call instance int32 zzz::xyz(int32,int32)

pop

ret

}

.method private hidebysig instance int32 pqr(int32 x,int32 y) il managed

{

.locals (int32 V_0)

ldarg.1

ldarg.2

mul

stloc.0

br.s IL_0006

IL_0006: ldloc.0

ret

}

.method private hidebysig instance int32 xyz(int32 x,int32 y) il managed

{

.locals (int32 V_0)

ldarg.1

ldarg.2

mul.ovf

stloc.0

br.s IL_0006

IL_0006: ldloc.0

ret

}

.method public hidebysig specialname rtspecialname instance void .ctor() il managed

{

ldarg.0

ldc.i4 0xf4240

stfld int32 zzz::b

ldarg.0

ldc.i4 0xf4240

stfld int32 zzz::c

ldarg.0

call instance void [mscorlib]System.Object::.ctor()

ret

}

}

 

Output

Exception occurred: System.OverflowException: An exception of type System.OverflowException was thrown.

at zzz.vijay()

 

This program demonstrates the use of the checked and unchecked operators and their implementation in IL.

 

The fields b and c are initialised to a decimal value of 1000 or a hex value of Oxf4240 in the constructor. Then, in the function vijay, they are put on the stack, and functions pqr and xyz are called. These functions return values that are not subsequently used anywhere. Thus, the pop instruction is used to remove them off the stack.

The function pqr does not achieve anything useful. The br.s instruction also does not achieve anything of significance. This function uses the unchecked operator in C#, which happens to be the default operator.

 

The function xyz only introduces a small variation: the mul instruction has been replaced by the mul.ovf instruction. The term ovf is the short form for the word overflow. In case an overflow occurs, the mul.ovf instruction will throw an exception.

 

Thus, overflow handling is done internally by employing IL instructions. If IL was unable to provide for handling overflows, the C# compiler would have had to provide the code for generation of an exception.

 

In conclusion, whenever we use the checked operator, the compiler tells IL to use the ovf family of instructions, so that the program can check for an overflow and generate an exception.

 

a.cs

class zzz

{

const int x = 1000000;

const int y = 1000000;

static int abc() {

return checked(x * y);

}

static int pqr() {

return unchecked(x * y);

}

static void Main()

{

int i ;

i = abc();

System.Console.WriteLine(i);

i = pqr();

System.Console.WriteLine(i);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.field private static literal int32 x = int32(0x000F4240)

.field private static literal int32 y = int32(0x000F4240)

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32 V_0)

call int32 zzz::abc()

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(int32)

call int32 zzz::pqr()

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

.method private hidebysig static int32 abc() il managed

{

.locals (int32 V_0)

ldc.i4 0xd4a51000

stloc.0

br.s IL_0008

IL_0008: ldloc.0

ret

}

.method private hidebysig static int32 pqr() il managed

{

.locals (int32 V_0)

ldc.i4 0xd4a51000

stloc.0

br.s IL_0008

IL_0008: ldloc.0

IL_0009: ret

}

}

 

Output

-727379968

-727379968

 

In the case of a constant, it does not matter whether a function uses the checked or unchecked operators. This is because, constants are a compile time issue. They are converted to actual constants by the compiler, as has oft been repeated.

 

The compiler actually multiples the constants x and y and replaces them with the value of the resultant product. Thus, the mul operator does not make an appearance anywhere as there is no trace of the checked operator.

 

 

It can be appreciated that the treatment of constants is different in C# and IL. So, given the IL code, it is very difficult to use reverse engineering to arrive back at the original C# code.

 

Please note that most of the arithmetic operators in IL can be suffixed with .ovf thereby ensuring that they check for overflow.

 

a.cs

class zzz {

public static void Main() {

int i,j = 8;

i = j >> 2;

System.Console.WriteLine(i);

i = j << 2;

System.Console.WriteLine(i);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed {

.entrypoint

.locals (int32 V_0,int32 V_1)

ldc.i4.8

stloc.1

ldloc.1

ldc.i4.2

shr

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(int32)

ldloc.1

ldc.i4.2

shl

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

2

32

 

The bitwise left shift and right shift operators of C# are converted to instructions shl and shr respectively.

 

Every time we use the bitwise right shift operator, it is equivalent to dividing by 2.

Every time we use the bitwise left shift operator, it is equivalent to multiplying by 2.

 

These instructions execute much faster than the division and multiplication instructions.

 

a.cs

class zzz

{

public static void Main()

{

int i=2;

i = ++i/++i;

System.Console.WriteLine(i);

}

}

 

 

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32 V_0)

ldc.i4.2

stloc.0

ldloc.0

ldc.i4.1

add

dup

stloc.0

ldloc.0

ldc.i4.1

add

dup

stloc.0

div

stloc.0

ldloc.0

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

0

.75 ( when int is changed to float)

 

The C# compiler executes code as it sees it. It starts from left to right. It first encounters ++i. The value of i is thus increased from 2 to 3.

 

The dup instruction of IL duplicates the value at the top of the stack. The stloc.0 assigns the number 3 to i. Then the number 1 is added to the variable i, making its resultant value 4.

 

The div instruction now sees 3 and 4 on the stack and thus, divides 3 by 4. The final answer is 0 or .75, depending upon the data type of i.

 

In programming languages like C, the result is not pre-determinable, but in C#, the order of evaluation is very lucid and clear - it executes the code from left to right using the principle of "first come first served".

 

a.cs

class zzz

{

public static void Main()

{

zzz a = new zzz();

int i = 0;

a.abc(i++,i++,i++);

System.Console.WriteLine(i);

}

public void abc( int x, int y, int z)

{

System.Console.WriteLine(z);

}

}

 

a.il

.assembly mukhi {}

.class public auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (class zzz V_0,int32 V_1)

newobj instance void zzz::.ctor()

stloc.0

ldc.i4.0

stloc.1

ldloc.0

ldloc.1

dup

ldc.i4.1

add

stloc.1

ldloc.1

dup

ldc.i4.1

add

stloc.1

ldloc.1

dup

ldc.i4.1

add

stloc.1

call instance void zzz::abc(int32,int32,int32)

ldloc.1

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

.method public hidebysig instance void abc(int32 x,int32 y,int32 z) il managed

{

ldarg.3

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

2

3

 

The above example again demonstrates that the compiler is unambiguous about the order of execution of code on a "first come first served basis". It builds on the earlier example.

 

The variable i is first placed on the stack and then incremented by one, making its value 1, but the value 0 is placed on the stack. Thus x becomes zero. Thereafter, 1 is placed on the stack and i is again incremented by 1, making its value 2. The value of the parameter y is 1. Finally, 2 is placed on the stack. Parameter z has the value 2 and the value of the variable i now becomes 3.

 

The IL code is much easier to understand.

 

We have created a zzz like object as local V_0 and only one int32 representing the variable i. The instruction ldc.i4.0 places the initial value of i on the stack. Then, stloc.1 assigns the value 0 to i. When the function abc is called, the this pointer is placed on the stack using ldloc.0.

 

Now the fun starts. The value of i, which is 0, is placed on the stack and duplicated using the dup instruction. Thus, two zeroes are placed on the stack. Next, the number 1 is placed on the stack and the add instruction adds this number to the 0 already on the stack, resulting in the sum of 1. The numbers 1 and 0, which were present on the stack earlier, are removed.

 

 

We store this value in i using ldloc.1 and place the new value 1 on the stack. We again use dup to duplicate this value and put it on the stack and use the add instruction to add the original and the duplicated values.

 

By now, the value of i is now 3 and the this pointer and the values 0, 1 and 2 are present on the stack. Hence WriteLine shows 2 in abc.

 

All this IL code has been written by a compiler and not a human being. If you are not clear about the above code, you can draw the stack diagrams.

 

a.cs

class zzz {

public static void Main() {

int i = 32768;

int j = ~i;

System.Console.WriteLine(j);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed {

.entrypoint

.locals (int32 V_0,int32 V_1)

ldc.i4 0x8000

stloc.0

ldloc.0

not

stloc.1

ldloc.1

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

 

Output

-32769

 

The bitwise operator ~ complements the bits, converting the 0s to 1s and 1s to 0s. This operator has a very simple equivalent in IL, which is the not instruction.

 

a.cs

class zzz

{

public static void Main()

{

int i;

i = 19;

System.Console.WriteLine(i%5);

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32 V_0)

ldc.i4.s 19

stloc.0

ldloc.0

ldc.i4.5

rem

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

4

 

The remainder operator % is converted to the rem instruction in IL. Thus, you must have noticed that, all the basic operators of C# have simple equivalent IL instructions.

 

a.cs

class zzz

{

public static void Main()

{

int i = 21,j = 15, k = 11;

System.Console.WriteLine(i & j );

System.Console.WriteLine(i | k );

System.Console.WriteLine(i ^ k );

}

}

 

a.il

.assembly mukhi {}

.class private auto ansi zzz extends [mscorlib]System.Object

{

.method public hidebysig static void vijay() il managed

{

.entrypoint

.locals (int32 V_0,int32 V_1,int32 V_2)

ldc.i4.s 21

stloc.0

ldc.i4.s 15

stloc.1

ldc.i4.s 11

stloc.2

ldloc.0

ldloc.1

and

call void [mscorlib]System.Console::WriteLine(int32)

ldloc.0

ldloc.2

or

call void [mscorlib]System.Console::WriteLine(int32)

ldloc.0

ldloc.2

xor

call void [mscorlib]System.Console::WriteLine(int32)

ret

}

}

 

Output

5

31

30

 

The bitwise anding, oring and xoring are also supported in IL by the equivalent instructions and, or and xor. Thus, IL has most of the instructions present in its assembler.

 

In addition, it has a number of higher level constructs. However, there is no logical ANDing and ORing in IL because, IL does not understand the logical values TRUE and FALSE.