Subterranean IL: Calling methods

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:

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:

What goes in the method body? Well, we’ll start off with a call instruction:

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:

 

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:

However, if we compile this and try to verify it, we get a verification error:

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:

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:

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!