Complex Tasks

Complex Tasks

Until now, the focus has been on individual instructions. Most programs consist of complex tasks, such as creating a new class, creating an array, or executing a for loop. Complex tasks consist of multiple instructions.

Managing Types

Classes contain static and instance members. The static members are accessible through the class name, whereas instance members are bound to an object. The WriteLine method is a static method. As demonstrated, WriteLine is called directly on the Console class (for example, System.Console::WriteLine). Instance members require an object. The newobj instruction creates an instance of a class and then invokes the constructor to initialize the object. It also deposits a reference to the object onto the evaluation stack. This reference can be used to call a member method or access a field. Such an action consumes the reference and removes it from the evaluation stack. Several actions require several references. The dup instruction is convenient for duplicating a reference or whatever is on the top of the evaluation stack.

This is the syntax of the newobj instruction:

  • newobj instance instruction:

    • new ctorsignature

Constructors are specially named methods. The name of a constructor is ctor, and a constructor is declared with the .ctor directive. By convention in C#, constructors return void, which is enforced by the C# compiler. In MSIL code, a constructor can return a value. Static constructors are named cctor and are declared with the identically named directive. The static constructor is called when the class or instance is first accessed.

The .field directive adds a field to the class. The ldfld and ldsfld instructions load an instance and static field onto the evaluation stack, respectively. Conversely, the stfld and stsfld instructions store data on the evaluation stack into a field. The stfld and stsfld instructions consume a reference to the related object. The load and store instructions have a single operand, which is the field signature.

The following program creates a class that contains an instance and static field in which the instance and static constructors initialize the fields. The AddField and SubtractField methods return the total and difference of the fields. An instance of the object is created in the Main method. The resulting reference is duplicated with the dup instruction. Why? Both the AddField and SubtractField methods are called, which require two references:

.assembly extern mscorlib {}
.assembly application {}

.namespace Donis.CSharpBook {

    .class Starter {

        .method static public void Main() il managed {
        .entrypoint
        .locals (int32 temp)
        newobj instance void Donis.CSharpBook.ZClass::.ctor()
        dup
        call instance int32 Donis.CSharpBook.ZClass::AddFields()
        stloc.0
        ldstr "The total is {0}"
        ldloc.0
        box int32
        call void [mscorlib] System.Console::WriteLine(string, object)
        call instance int32 Donis.CSharpBook.ZClass::SubtractFields()
        stloc.0
        ldstr "The difference is {0}"
        ldloc.0
        box int32
        call void [mscorlib] System.Console::WriteLine(string, object)
        ret
    }
 }

 .class ZClass {

        .method private hidebysig specialname rtspecialname
        static void .cctor() cil managed {
        ldstr "In static constructor"
        call void [mscorlib] System.Console::WriteLine(string)
        ldc.i4.s 10
        stsfld int32 Donis.CSharpBook.ZClass::fielda
        ret
    }

    .method public hidebysig specialname rtspecialname
            instance void .ctor() cil managed {
          ldstr "In constructor"
        call void [mscorlib] System.Console::WriteLine(string)
        ldarg.0

        ldc.i4.s 5
        stfld int32 Donis.CSharpBook.ZClass::fieldb
        ret
    }

    .method public int32 AddFields() cil managed {
        ldsfld int32 Donis.CSharpBook.ZClass::fielda
        ldarg.0
        ldfld int32 Donis.CSharpBook.ZClass::fieldb
        add
        ret
    }

    .method public int32 SubtractFields() cil managed {
        ldsfld int32 Donis.CSharpBook.ZClass::fielda
        ldarg.0
        ldfld int32 Donis.CSharpBook.ZClass::fieldb
        sub
        ret
    }

 .field static private int32 fielda
 .field private int32 fieldb

 }
}

Boxing The previous code uses the box instruction.

ldstr "The total is {0}"
ldloc.0
box int32
call void [mscorlib] System.Console::WriteLine(string, object)

The box instruction prevents an exception. The Console::WriteLine method has two parameters, which are both reference types. However, the top of the evaluation stack has a value type and a reference type. The assignment of a value to a reference type is the problem. The memory models are inconsistent. The box instruction removes the value type from the evaluation stack, creates an object on the managed heap that boxes the value type, and places a reference to the newly created object back on the evaluation stack. Now that the value type has been replaced with a reference type, Console::WriteLine is called successfully. The unbox instruction works in reverse. It unboxes a reference that is on the evaluation stack. This instruction unboxes the reference type to the specified value type. The reference is replaced on the evaluation stack with the unboxed value.

This is the syntax of the box and unbox instructions:

  • box valuetype

  • unbox valuetype

Inheritance In previous examples in this chapter, no class has directly inherited from another class. The classes implicitly inherited System.Object. Most classes can be inherited explicitly, including System.Object, by using the extends keyword. The child class will inherit most members of the base class, except constructors. In the base class, methods that are expected to be overridden in the child should be prefixed with the keyword virtual. The callvirt instruction calls a function overridden in a child. A child instance should be on the evaluation stack. Here is sample code of inheritance in MSIL code:

.assembly extern mscorlib {}
.assembly application {}

.namespace Donis.CSharpBook {

    .class Starter {

        .method static public void Main() il managed {
            .entrypoint
            newobj instance void Donis.CSharpBook.XClass::.ctor()
            dup
            call instance void Donis.CSharpBook.ZClass::MethodA()
            callvirt instance void Donis.CSharpBook.XClass::MethodC()
            ret
        }
    }

.class abstract ZClass {

   .method public instance void MethodA() il managed {
       ldstr "ZClass::MethodA"
       call void [mscorlib] System.Console::WriteLine(string)
       ret
   }

   .method public virtual instance void MethodC() il managed {
       ldstr "ZClass::MethodC"
       call void [mscorlib] System.Console::WriteLine(string)
       ret
   }
}

.class XClass {

   .method public specialname rtspecialname
           instance void .ctor() cil managed {
       ret
   }

   .method public instance void MethodB() il managed {
       ldstr "XClass::MethodB"
       call void [mscorlib] System.Console::WriteLine(string)
       ret
   }

    .method public virtual instance void MethodC() il managed {
        ldstr "XClass::MethodC"
        call void [mscorlib] System.Console::WriteLine(string)
        ret
    }
 }
}

Did you notice the following statement in the preceding code?

call instance void Donis.CSharpBook.ZClass::MethodA()

This statement is interesting. Before this instruction is executed, there is a reference to an XClass object on the evaluation stack. XClass has inherited MethodA from the ZClass. XClass does not override or hide the interface of MethodA in the base class. Therefore, the implementation of MethodA resides solely in the base class. Therefore, you must call the base function on the derived reference to successfully invoke the method.

Interfaces Classes are permitted to inherit from one class but can implement multiple interfaces. There is no interface directive; instead, add the interface keyword to the details of the class directive. The interface keyword enforces the semantics of an interface type on the class. Member methods must be public, abstract, and virtual. Fields are not allowed in an interface class. In addition, constructors and destructors are not permitted. The ILASM compiler enforces these and other rules. A class identifies interfaces to implement with the implements keyword. The class must implement all the members of the interface to prevent run-time errors. This code demonstrates an interface and the implementation of an interface:

.assembly extern mscorlib {}
.assembly application {}

.namespace Donis.CSharpBook {

    .class interface public abstract IA {
        .method public abstract virtual instance void MethodA() il managed {
        }
   }

   .class Starter{
       .method static public void Main() il managed {
          .entrypoint
          newobj instance void Donis.CSharpBook.ZClass::.ctor()
          callvirt instance void Donis.CSharpBook.ZClass::MethodA()

           ret
        }
   }

   .class ZClass implements Donis.CSharpBook.IA {
       .method public specialname rtspecialname
               instance void .ctor() cil managed {
           ret
       }

        .method public virtual instance void MethodA() il managed {
            ldstr "ZClass:MethodA"
            call void [mscorlib] System.Console::WriteLine(string)
            ret
        }
    }
}

MethodA is defined as part of interface IA and is an abstract function. In C#, abstract functions do not have a function body. In MSIL code, however, abstract methods have a body, but the body cannot contain an implementation. An abstract method has a body but no code.

Structures As discussed, interfaces are defined with the class directive. Structures are similarly defined. To define a structure, declare a type with the class directive and add the value keyword to the class detail. The semantics of a structure are then enforced by the MSIL compiler. For example, a structure does not have a default constructor or destructor, a structure is sealed, and so on. The value keyword is implemented by the ILASM compiler as an explicit inheritance of System.ValueType. You could drop the value keyword and inherit the System. ValueType directly. The compiler also adds keywords required for a structure, such as the sealed keyword.

As a value type, structures are defined as local variables with the .locals directive. Value types are stored on the stack, not on the managed heap. Accessing a member, such as calling a member method, requires binding to the address of the structure. This entails loading the address of the structure onto the evaluation stack before accessing a member. The ldloc instruction would load the structure on the evaluation stack. We need the address of the structure. To load the address of a local variable, use the ldloca instruction. (The "a" variation of an MSIL instruction refers to an address.)

Call the constructor of a structure explicitly with the call instruction, not the newobj instruction. Structures are not created on the heap. When the constructor is called directly, the type is simply initialized.

This code creates and initializes a structure:

.assembly extern mscorlib {}
.assembly application {}

.namespace Donis.CSharpBook {

    .class Starter {
        .method static public void Main() il managed {
            .entrypoint
            .locals init (valuetype Donis.CSharpBook.ZStruct obj)
            ldloca.s obj
            ldc.i4.s 10
            call instance void Donis.CSharpBook.ZStruct::.ctor(int32)
             ldloca.s obj

             ldfld int32 Donis.CSharpBook.ZStruct::fielda
             call void [mscorlib]System.Console::WriteLine(int32)
             ret
        }
    }

 .class value ZStruct {

        .method public specialname rtspecialname
                instance void .ctor(int32) cil managed {
            ldarg.0
            ldarg.1
            stfld int32 Donis.CSharpBook.ZStruct::fielda
            ret
        }

        .field public int32 fielda
    }
}