Stop me if I make a mistake here.
If I understand correctly, when I call a method on an instance of a class, the JIT compiler locates the type object corresponding to the type of the instance and then locates a reference therein to the actual method code.
My question is how does this work for value types? I was under the impression that value types did not have a type object pointer like reference types do. If this is the case, how does the CLR manage to navigate to the method code when one is called?
Method Calls A method is a routine that applies to a particular class of objects. Once an object is declared, you can refer to it by its identifier when calling methods. The following example calls the SetActive method on the Find dialog box: Find.SetActive ()
There are two kinds of types in Visual Basic: reference types and value types. Variables of reference types store references to their data (objects), while variables of value types directly contain their data.
A Value Type holds the data within its own memory allocation and a Reference Type contains a pointer to another memory location that holds the real data. Reference Type variables are stored in the heap while Value Type variables are stored in the stack.
Consider an example, suppose we have the following structure:
public struct Test
{
public void TestMethod()
{
}
}
Here's IL code for it:
.class public sequential ansi sealed beforefieldinit ConsoleApplication.Test
extends [mscorlib]System.ValueType
{
.pack 0
.size 1
.method public hidebysig
instance void TestMethod () cil managed
{
// Method begins at RVA 0x21dc
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} // end of method Test::TestMethod
}
Ok, now because the C# compiler statically knows the type of Test
, it can do overload resolution and find the exact TestMethod
being called. Then it emits MSIL to push the arguments onto the MSIL virtual stack, it expects a parameter of type pointer to Test
, which the compiler handles without boxing and issues a call instruction containing a metadata reference to that particular method.
.locals init (
[0] valuetype ConsoleApplication.Test test
)
IL_0000: ldloca.s test
IL_0002: initobj ConsoleApplication.Test
IL_0008: ldloca.s test
IL_000a: call instance void ConsoleApplication.Test::TestMethod()
For ToString
and GetHashCode
the compiler uses the Constrained OpCode, because these methods can be overloaded.
IL_0002: initobj ConsoleApplication.Test
IL_0008: ldloca.s test
IL_000a: constrained. ConsoleApplication.Test
IL_0010: callvirt instance int32 [mscorlib]System.Object::GetHashCode()
The constrained opcode allows IL compilers to make a call to a virtual function in a uniform way independent of whether ptr is a value type or a reference type. Using the constrained prefix also avoids potential versioning problems with value types. If the constrained prefix is not used, different IL must be emitted depending on whether or not a value type overrides a method of System.Object. For example, if a value type V overrides the Object.ToString() method, a call V.ToString() instruction is emitted; if it does not, a box instruction and a callvirt Object.ToString() instruction are emitted. A versioning problem can arise in the former case if the override is later removed, and in the latter case if an override is later added.
For the GetType
method requires boxing because it's non-virtual and defined in the Object
type.
IL_0002: initobj ConsoleApplication.Test
IL_0008: ldloc.0
IL_0009: box ConsoleApplication.Test
IL_000e: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With