-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.