2

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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Tacodiva
  • 391
  • 2
  • 17
  • 2
    I bet because the documentation for the op code was written before the existence of static abstract interface members which basically allows [static interface members to be declared virtual](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/static-abstracts-in-interfaces#interface-members) . – Guru Stron Jul 12 '23 at 05:49
  • If you make the method non-static you will see that it becomes `.constrained .callvirt` ([@sharplab](https://sharplab.io/#v2:C4LglgNgPgAgTARgLACgYAYAEBlAFgQwCcAHAGXwCMA6AJQFcA7YMAWwFMBuVASQBU2AzsEoQ2mAB6YAvJgZsA7pgCyAT35CRbABQBKLilQwAzJjBM2hAGb4AxmL6DhFUZgDeqTJ8zHvAFkzqwLr6AL6oqADaAFJgwADibHKEYDZawCrEbAD2llqqgZo6OgC6kTHxiRYpaRnZufmOmghFpWgm8MpqjhYAPLwAfJjyuBZivJggmA4azmLuKF7e7f70DIG6bh6LXuOSMgAmbNZ0EMD6217iVOt6W55hKA+G7XCdBbOT004urot3S54YP4bpsFl4Hk82pghIQ6DZgG9GrMEJ93j8/mDASY/AFHBt5osIeEUEA==)). – Guru Stron Jul 12 '23 at 05:54
  • I'm not sure what you're trying to say with "the compiler can tell exactly what function is being called" because it feels almost exactly the opposite to me - that we know a type matches the interface but we don't know if that type has the static method or just has one higher up its own inheritance chain. – Damien_The_Unbeliever Jul 12 '23 at 05:54
  • @Damien_The_Unbeliever That is true when compiling to CIL, as the compiler cannot yet know what type interface will be put into the generic parameter. But I am under the impression that when the CIL gets JITed, a different version of the method will be created for each generic parameter it's used with. At that time, given the generic parameter is known, shouldn't the abstract virtual method being called be certain? – Tacodiva Jul 12 '23 at 06:07
  • @GuruStron Yes, I suspect that is what's happened here too, but I still don't understand what it's actually checking in this case (and especially why that check is present at runtime). – Tacodiva Jul 12 '23 at 06:09

2 Answers2

3

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Ah, thank you! That certainly clears up a lot of my confusions. I would accept it, but the answer to the actual titular question is still unclear to me. `.constrained` on a `callvirt` exists to ensure value and reference types are both handled correctly by the same `callvirt`. Here though, I don't see why boxing or anything like that would ever happen. Essentially, my question is still what is actually being checked by `.constrained` in this context? – Tacodiva Jul 12 '23 at 06:24
  • @Tacodiva Was glad to help! Also opened a github issue - see the update. – Guru Stron Jul 12 '23 at 06:43
  • 1
    True, but it still doesn't need `constrained.`, the whole point of which is to allow a single `this` parameter to be used both for value and reference types. This seems unnecessary on a `static` function, as there is no `this`. – Charlieface Jul 12 '23 at 09:35
  • @Charlieface valid point. TBH don't know, maybe it was repurposed to pass some kind of "self" type parameter so the actual method can be resolved. Will wait for comments in the issue and update answer accordingly. – Guru Stron Jul 12 '23 at 09:47
  • 1
    https://github.com/trylek/runtime/blob/e959fb5443cc016557de6dd300196cfa9d64e4a0/docs/design/specs/Ecma-335-Augments.md#ii152-static-instance-and-virtual-methods-page-177 I'm going to write an answer with this – Charlieface Jul 12 '23 at 10:01
  • @Charlieface great find! Will add it to the docs issue. And as I understand it says something similar to my assumption about repurposing of the `constrained`. – Guru Stron Jul 12 '23 at 10:17
  • I'm not sure that the stuff around the JIT emitting different method bodies for when a generic type parameter is a reference/value type is relevant? That happens at runtime when IL is turned into machine code, but virtual calls vs constrained virtual calls are an IL concept – canton7 Jul 30 '23 at 11:20
3

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.

Charlieface
  • 52,284
  • 6
  • 19
  • 43