Next in my ‘Subterranean IL’ series, I’ll be looking at method calls, and in particular, the difference between reference and value types. To help, I’ll be using the following types (definitions given in C# for brevity)1:
1 |
public class IncrementableClass { public int Value; public void Increment(int incrementBy) { Value += incrementBy; }}public struct IncrementableStruct { public int Value; public void Increment(int incrementBy) { Value += incrementBy; }} |
Reference types
We’ll be looking at how you call the Increment
method on instances of these types in IL. To start off, we’ll consider the IncrementableClass
type:
1 |
.method public static void CallIncrement( class IncrementableClass obj) { // ????} |
What goes in the method body? Well, we’ll start off with a call
instruction:
1 |
call instance void IncrementableClass::Increment(int) |
All good so far. What about the method arguments? Well, from the call
opcode documentation, it requires the method arguments pushed onto the stack bottom-to-top. And as this is an instance method, the first method argument is the object we’re calling the method on (the this
pointer), and the second is the incrementBy
parameter (for this example, the constant 5).
Because IncrementableClass
is a reference type, the this
pointer on the stack will be an object reference, type O
. So, the code we end up with is this, which I’ve annotated to show what the stack looks like after each instruction:
1 |
// push obj argumentldarg.0 // O// push constant 5ldc.i4.5 // O,int32// call the methodcall instance void IncrementableClass::Increment(int) // stack empty// obligatory return from CallIncrement methodret |
Value types
What about IncrementableStruct
? Well, the call
instruction works the same way, except the this
pointer is a value type instead of an object reference, and ldarg.0
will copy the value type onto the stack for us. So, we try the same code we used for IncrementableClass
:
1 |
.method public static void CallIncrement( valuetype IncrementableStruct obj) { ldarg.0 ldc.i4.5 call instance void IncrementableStruct::Increment(int) ret} |
However, if we compile this and try to verify it, we get a verification error:
1 |
<Module>::CallIncrement[offset 0x00000002] [found value 'IncrementableStruct'] [expected address of value 'IncrementableStruct'] Unexpected type on the stack. |
What’s going on here? The error message specifies that the call
instruction needs a pointer to an IncrementableStruct
instance as the this
argument, rather than the instance itself. If we have a look at the stack transition, it currently does this:
1 |
ldarg.0 // IncrementableStructldc.i4.5 // IncrementableStruct,int32call instance void IncrementableStruct::Increment(int)ret |
Why does it want a pointer? If we think about what happens when the call
instruction is run, all the method arguments, including the this
pointer, are popped from the stack. If the this
was an actual value type instance stored on the stack, then that instance would be popped off the stack by the call
and used as the this
for the Increment
method (wherever that is). When Increment
returns, the mutated value type instance simply disappears – it isn’t returned to anywhere, and isn’t pushed back onto the stack of CallIncrement
.
Therefore, if call
operated directly on value type instances on the stack, it would be impossible to mutate value type instances, and the VES has to support mutable value types as they aren’t explicitly disallowed by the ECMA spec1. What we actually want is this:
1 |
ldarga 0 // &ldc.i4.5 // &,int32call instance void IncrementableStruct::Increment(int)ret |
The ldarga
instruction is pushing a managed pointer (&
) to the value type instance in the method arguments onto the stack, rather than a copy of the value type instance itself. That instance, stored in the method parameters, is the one that is mutated, and everything works as expected.
Conclusion
Calling a method on reference and value types both require a pointer as the this
argument, but while reference types require a pointer of type O
, value types require a pointer of type &
. Although they are the same sort of thing (a pointer to a block of memory), the VES treats them as distinct types with no conversions between them allowed in verifiable code. This distinction will be very important when we look at generic methods. Even at this early stage, calling a method can be quite a complicated business!
Next time: Virtual methods, callvirt
, and interface types.
1 As everyone should know, mutable value types are pure undiluted evil. I’m only using one to demonstrate the IL. Don’t try this at home!
Load comments