Calling a method directly

The simplest (and fastest, as we will see) way to call a method, is to call it directly. Here is the generated code for the CallDirectly method:

                
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
V_0 = L_0;
Important_t1 * L_1 = V_0;
NullCheck(L_1);
Important_Method_m1(L_1, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_Method_m1_MethodInfo);
                
            

The last line is the actual method call. Note that it does nothing special, just calls a free function defined in the C++ code. Recall from the earlier post about generated code, that il2cpp.exe generates all methods as C++ free functions. The IL2CPP scripting backend does not use C++ member functions or virtual functions for any generated code. It follows then that calling a static method directory should be similar. Here is the generated code from the CallStaticMethodDirectly method:

Important_StaticMethod_m3(NULL /*static, unused*/, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_StaticMethod_m3_MethodInfo);
            

We could say there is less overhead calling a static method, since we don’t need to create and initialize an object instance. However, the method call itself is exactly the same, a call to a C++ free function. The only difference here is that the first argument is always passed with a value of NULL.

Since the difference between calls to static and instance methods is so minimal, we’ll focus on instance methods only for the rest of this post, but the information applies to static methods as well.

Calling a method via a compile-time delegate

What happens with a slightly more exotic method call, like an indirect call via delegate? We’ll first look at a what I’ll call a compile-time delegate, meaning that we know at compile time which method will be called on which object instance. The code for this type of call is in the CallViaDelegate method. It looks like this in the generated code:

                
// Get the object instance used to call the method.
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
V_0 = L_0;
Important_t1 * L_1 = V_0;

// Create the delegate.
IntPtr_t L_2 = { &Important_Method_m1_MethodInfo };
ImportantMethodDelegate_t4 * L_3 = (ImportantMethodDelegate_t4 *)il2cpp_codegen_object_new (InitializedTypeInfo(&ImportantMethodDelegate_t4_il2cpp_TypeInfo));
ImportantMethodDelegate__ctor_m4(L_3, L_1, L_2, /*hidden argument*/&ImportantMethodDelegate__ctor_m4_MethodInfo);
V_1 = L_3;
ImportantMethodDelegate_t4 * L_4 = V_1;

// Call the method
NullCheck(L_4);
VirtFuncInvoker1< int32_t, String_t* >::Invoke(&ImportantMethodDelegate_Invoke_m5_MethodInfo, L_4, (String_t*) &_stringLiteral1);

I’ve added a few comments to indicate the different parts of the generated code

Note that the actual method called here is not part of the generated code. The method is located in the GeneratedVirtualInvokers.h file. This file is generated by il2cpp.exe, but it doesn’t come from any IL code. Instead, il2cpp.exe creates this file based on the usage of virtual functions that return a value (VirtFuncInvokerN) and those that don’t (VirtActionInvokerN), where N is the number of arguments to the method.

The Invoke method here looks like this:

                
template 
struct VirtFuncInvoker1
{
typedef R (*Func)(void*, T1, MethodInfo*);

static inline R Invoke (MethodInfo* method, void* obj, T1 p1)
{
VirtualInvokeData data = il2cpp::vm::Runtime::GetVirtualInvokeData (method, obj);
return ((Func)data.methodInfo->method)(data.target, p1, data.methodInfo);
}
};

            

The call into libil2cpp GetVirtualInvokeData looks up a virtual method in the vtable struct generated based on the managed code, then it makes a call to that method.

Why don’t we used C++11 variadic templates to implement these VirtFuncInvokerN methods? This looks like a situation begging for variadic templates, and indeed it is. However, the C++ code generated by il2cpp.exe has to work with some C++ compilers which don’t yet support all C++ 11 features, including variadic templates. In this case at least, we did not think that forking the generated code for C++11 compilers was worth the additional complexity.

But why is this a virtual method call? Aren’t we calling an instance method in the C# code? Recall that we are calling the instance method via a C# delegate. Look again at the generated code above. The actual method we are going to call is passed in via the MethodInfo* (method metadata) argument: ImportantMethodDelegate_Invoke_m5_MethodInfo. If we search for the method named "ImportantMethodDelegate_Invoke_m5" in the generated code, we see that the call is actually to the managed Invoke method on the ImportantMethodDelegate type. This is a virtual method, so we need to make a virtual call. It is this ImportantMethodDelegate_Invoke_m5 function which will actually make the call to the method named Method in the C# code.

Wow, that was certainly a mouth-full. By making what looks like a simple change to the C# code, we’ve now gone from a single call to a C++ free function to multiple function calls, plus a table lookup. Calling a method via a delegate is significantly more costly than calling the same method directly.

Note that in the process of looking at a delegate method call, we’ve also seen how a call via a virtual method works.