Methods can be called directly or indirectly. In addition, you can also use the special case of a so-called tail call, discussed in this section. Because the method signature indicates whether the method is instance or static, separate instructions for instance and static methods are unnecessary. What the method signature doesn’t hold, however, is information about whether the method is virtual. As a result, separate instructions are used for calling virtual and nonvirtual methods.
Method call instructions have one parameter: a token of the method being called, either a MethodDef or a MemberRef. The arguments of the method call should be loaded on the stack in order of their appearance in the method signature, with the last signature parameter being loaded last. Instance methods have an “invisible” first argument (an instance pointer) not present in the signature; when an instance method is called, this instance pointer should be loaded on the stack first, preceding all arguments corresponding to the method signature.
Unless the called method returns void, the return value is placed on the stack when the call is completed.
The IL instruction set contains three instructions intended for the direct method calls:
jmp <token> (0x27) Abandon the current method and jump to the target method, specified by <token>, transferring the current arguments. At the moment jmp is invoked, the evaluation stack must be empty, and the arguments are transferred automatically. Because of this, the signature of the target method must match the signature of the method invoking jmp. This instruction should not be used within SEH blocks—try, catch, filter, fault, or finally blocks, discussed in Chapter 11—or within a synchronized region. The jmp instruction is unverifiable.
call <token> (0x28) Call a nonvirtual method. You can also call a virtual method, but in this case it is called not through the instance’s v-table but through its type-specific method table. (If this sounds somehow vague to you, you might want to return to Chapter 9 and, more precisely, to the section “Static, Instance, Virtual Methods” and the sample file Virt_not.il.) The real difference between virtual and nonvirtual instance methods becomes obvious when you create an instance of a class, cast it to the parent type of the class, and then call instance methods on this “child-posing-as-parent” instance. Because nonvirtual methods are called through the type’s method table, the parent’s methods will be called in this case. Virtual methods are called through the v-table specific to the class instance, and hence the child’s methods will be called. The call instruction works through the type’s method table and ignores the instance’s v-table, so the parent’s methods will be called whether they are virtual or not. To confirm this, carry out a simple experiment: open the sample file Virt_not.il in a text editor and change callvirt instance void A::Bar( ) to call instance void A::Bar( ). Then recompile the sample and run it.
callvirt <token> (0x6F) Call the virtual method specified by <token>. This type of method call is conducted through the instance’s v-table. It is possible to call a nonvirtual instance method using callvirt. In this case, the method is called through the type’s method table simply because the method cannot be found in the v-table. But unlike call, the callvirt instruction first checks the validity of the object reference (this pointer) before doing anything else, which is a very useful feature. The Microsoft Visual C# .NET compiler exploits it shamelessly, emitting callvirt to call both virtual and nonvirtual instance methods of classes. I say “of classes” because callvirt requires an object reference as the this pointer and will not accept a managed pointer to a value type instance.
Methods in IL can be called indirectly through the function pointer loaded on the evaluation stack. This allows us to make calls to computed targets—for example, to call a method by a function pointer returned by another method. Function pointers used in indirect calls are unmanaged pointers represented by native int. Two instructions load a function pointer to a specified method on the stack, and one other instruction calls a method indirectly:
ldftn <token> (0xFE 0x06) Load the function pointer to the method specified by <token> of MethodDef or MemberRef type. The method is looked up in the class’s method table.
ldvirtftn <token> (0xFE 0x07) Pop the object reference (the instance pointer) from the stack and load the function pointer to the method specified by <token>. The method is looked up in the instance’s v-table.
calli <token> (0x29) Pop the function pointer from the stack, pop all the arguments from the stack, and make an indirect method call according to the method signature specified by <token>. <token> must be a valid StandAloneSig token. The function pointer must be on the top of the stack. If the method returns a value, it is pushed on the stack at the completion of the call. The calli instruction is unverifiable, which is not surprising, considering that the call is made via an unmanaged pointer, which is itself unverifiable.
It’s easy enough to see that the combination ldftn/calli is equivalent to call, as long as we don’t consider verifiability, and the combination ldvirtftn/calli is equivalent to callvirt.
The ILAsm notation requires full specification of the method in the ldftn and ldvirtftn instructions, similar to the call and callvirt instructions. The method signature accompanying the calli instruction is specified as <call_conv> <ret_type>(<arg_list>). For example:
.locals init (native int fnptr) ldftn void [mscorlib]System.Console::WriteLine(int32) stloc.0 // Store function pointer in local variable ldc.i4 12345 // Load argument ldloc.0 // Load function pointer calli void(int32)
Tail calls are similar to method jumps (jmp) in the sense that both lead to abandoning the current method, discarding its stack frame, and passing the arguments to the tail-called (jumped-at) method. However, because the arguments of a tail call have to be loaded on the evaluation stack explicitly, a tail call—unlike a jump—does not require the entire signature of the called method to match the signature of the calling method; only the return types must be the same or compatible. In short, a jump is the equivalent of loading all the current method’s arguments on the stack and invoking a tail call.
Tail calls are distinguished by the prefix instruction tail. immediately preceding a call, callvirt, or calli instruction:
tail. (0xFE 0x14) Mark the following call instruction as a tail call. This instruction has no parameters and does not work with the stack. In ILAsm, this instruction—like the other prefix instructions unaligned. and volatile., discussed earlier—must be separated from the call instruction that follows it by at least a space symbol.
The difference between a jump and a tail call is that the tail call instruction pair is verifiable in principle, subject to the verifiability of the call arguments, as long as it is immediately followed by the ret instruction. As is the case with other prefix instructions, it is illegal to bypass the prefix and branch directly to the prefixed instruction, in this case, call, callvirt, or calli.