How does C # handle calling an interface method in a structure?

Consider:

interface I { void M(); } struct S: I { public void M() {} } // in Main: S s; I i = s; sM(); iM(); 

And IL for Main:

 .maxstack 1 .entrypoint .locals init ( [0] valuetype S s, [1] class I i ) IL_0000: nop IL_0001: ldloc.0 IL_0002: box S IL_0007: stloc.1 IL_0008: ldloca.ss IL_000a: call instance void S::M() IL_000f: nop IL_0010: ldloc.1 IL_0011: callvirt instance void I::M() IL_0016: nop IL_0017: ret 

First ( IL_000a ), S::M() is called with the value type for this . Further ( IL_0011 ), it is called with a link (boxed).

How it works?

I can imagine three ways:

  • Two versions of I::M compiled for the value of / ref type. In vtable, it stores one for type ref, but statically sent calls use one for value types. It is ugly and unlikely, but possible.
  • The wrapper method is stored in the vtable, which unpacks this and then calls the actual method. This sounds inefficient because all arguments to the method must be copied with two calls.
  • There is a special logic that checks this in callvirt . Even more inefficient: all callvirt carry a fine (negligible).
+7
c # struct interface cil
source share
1 answer

The short answer is that in the method itself, the value of struct always accessible through a pointer. This means that this method does not work, as if the struct was passed as a regular parameter, it is more like a ref parameter. It also means that the method does not know if it works by box value or not.

Long answer:

Firstly, if I compile your code, then sM(); does not generate any code. The JIT compiler is smart enough to embed a method, and nesting an empty method does not result in missing code. So, I did to apply [MethodImpl(MethodImplOptions.NoInlining)] on SM to avoid this.

Now, here is the native code that your method generates (skipping the proog and epilog function):

 // initialize s in register AX xor eax,eax // move s from register AX to stack (SP+28h) mov qword ptr [rsp+28h],rax // load pointer to MethodTable for S to register CX mov rcx,7FFDB00C5B08h // allocate memory for i on heap call JIT_TrialAllocSFastMP_InlineGetThread (07FFE0F824C10h) // copy contents of s from stack to register C movsx rcx,byte ptr [rsp+28h] // copy from register CX to heap mov byte ptr [rax+8],cl // copy pointer to i from register AX to register SI mov rsi,rax // load address to c on stack to register CX lea rcx,[rsp+28h] // call S::M call 00007FFDB01D00C8 // copy pointer to i from register SI to register CX mov rcx,rsi // move address of stub for I::M to register 11 mov r11,7FFDB00D0020h // ??? cmp dword ptr [rcx],ecx // call stub for I::M call qword ptr [r11] 

In both cases, the call ends with a call to the same code (which is just one ret statement). The first time, the CX register indicates the allocation of the stack s (SP + 28h in the above code), the second time for the allocated heap i (AX + 8 immediately after the heap allocation function is called).

+2
source share

All Articles