Tacodiva
Tacodiva

Reputation: 518

Why is there a `.constrained` opcode preceeding the `call` opcode when calling an abstract static function?

When I compile this simple C# code:

public interface ITestable {
    public abstract static void Test();
}

public class MyTester<T> where T : ITestable {
    public void RunTest() {
        T.Test();
    }
}

I get the following three IL opcodes for MyTester<T>.RunTest():

IL_0000: constrained. !T
IL_0006: call void ITestable::Test()
IL_000b: ret

This doesn't seem to align with what the documentation says about the constrained prefix, stating it can only appear before a callvirt instruction, but here it's before a call. You can see this on SharpLab.

This .constrained prefix seems to translate in the jitted assembly too. Using SharpLab's JitGeneric to compile MyTester for a given generic argument (see here), the assembly seems to be performing a check before it calls the Test function. I was under the impression that static abstract method calls should compile to a simple call because the compiler can tell exactly what function is being called, and I don't understand why it is more complex than that.

Upvotes: 2

Views: 222

Answers (2)

Charlieface
Charlieface

Reputation: 71119

According to the ECMA-335 spec, as augmented here by the DotNet Runtime team, the specification states that static functions on interfaces need to be called using constrained. so that the runtime knows which class to dispatch the method to. This is despite using call or ldftn, and not callvirt, as the interface alone is not enough to dispatch the method correctly, given that there is no this parameter.

This is a separate use of constrained. from its original use, which was for callvirt instructions, so that this parameters could be dispatched in the same way regardless of value/reference-types.

The documentation of OpCodes.Constrained has not been updated yet for this.

Upvotes: 3

Guru Stron
Guru Stron

Reputation: 141565

I was under the impression that static abstract method calls should compile to a simple call because the compiler can tell exactly what function is being called, and I don't understand why it is more complex than that.

You should note one thing - for reference types the generics are compiled only once(compared to the value types, where for every one passed as generic parameter the separate implementation will be compiled) to prevent code bloat (see this or this) and the same compiled code is used for all reference types used as generic type parameter, so compiler does not actually know exactly which function being called.

If you update your second snippet with several value and reference types (note that implementations are a bit different so the difference becomes more clear):

[JitGeneric(typeof(MyTestable))]
[JitGeneric(typeof(MyTestable11))]
[JitGeneric(typeof(MyTestable1))]
[JitGeneric(typeof(MyTestable2))]
public class MyTester<T> where T : ITestable {
    public void RunTest() {
        T.Test();
    }
}

public class MyTestable: ITestable {    
    public static void Test() =>Console.WriteLine("Hello!");
}
public class MyTestable11: ITestable {    
    public static void Test() =>Console.Write("Hello!");
}

public struct MyTestable1: ITestable {    
    public static void Test() =>Console.WriteLine("Hello!");
}

public struct MyTestable2: ITestable {    
    public static void Test() =>Console.Write("Hello!");
}

You will see that for all reference types the same MyTester`1[[System.__Canon, System.Private.CoreLib]].RunTest() is emmited while for value types you will have separate ones:

MyTester`1[[MyTestable1, _]].RunTest()
    L0000: mov ecx, [0x8d2daa0]
    L0006: call dword ptr [0x10a29ac8]
    L000c: ret

MyTester`1[[MyTestable2, _]].RunTest()
    L0000: mov ecx, [0x8d2daa0]
    L0006: call dword ptr [0x10a29cc0]
    L000c: ret

Code @sharplab.io

As for the question in title - my guess is that docs were not updated to support the introduction of the static abstract methods in the interfaces (note - the docs are not always correct, even I have bumped into several such cases - for example github PR 1, github PR 2, github PR 3, or even for the IL OpCodes - github issue for OpCodes.Ldind_I8)

Created a new issue for docs @github.

UPD

As written in the answer by @Charlieface there is another piece of documentation which explains the usage of the constrained in this case.

Upvotes: 3

Related Questions