This new attribute will be used by the covariant return types and records C# language features posed for C# 9.0.
Today when we have an explicit override to a method (using MethodImpls), this override gets a new vtable slot on the type, and the slot for the overriden method gets updated to point to the new overriding method. Example:
``` C#
// Pseudo code with some IL
class A {
void VirtualFunction() { }
}
class B : A {
void VirtualFunction() { .override A.VirtualFunction }
}
// VTable for type A:
// [0]: A.VirtualFunction
// VTable for type B:
// [0]: B.VirtualFunction
// [1]: B.VirtualFunction
Now when a new derived type explicitly overrides `A.VirtualFunction`, it would also get a new vtable slot, and only the vtable slot of the method being overriden gets updated with the new override. Example:
``` C#
class C : B {
void VirtualFunction() { .override A.VirtualFunction }
}
// VTable for type A:
// [0]: A.VirtualFunction
//
// VTable for type B:
// [0]: B.VirtualFunction
// [1]: B.VirtualFunction
//
// VTable for type C:
// [0]: C.VirtualFunction
// [1]: B.VirtualFunction -> This slot does not get overriden
// [2]: C.VirtualFunction
With the new records and covariant return feature, a change of behavior is required to vtable slot overrides when a method gets explictly overriden. In the example above, the second slot on type C would also need to be updated to point to the latest override of the method:
``` C#
// VTable for type C:
// [0]: C.VirtualFunction
// [1]: C.VirtualFunction -> This slot needs to be updated as well
// [2]: C.VirtualFunction
The reason for this change is to ensure that any virtual call to the method, whether it uses the base signature or derived signature of the method, we always execute the most derived override:
``` c#
// Given an object of type C:
callvirt A.VirtualFunction -> Executes C.VirtualFunction
callvirt B.VirtualFunction -> Executes C.VirtualFunction
callvirt C.VirtualFunction -> Executes C.VirtualFunction
To achieve this, a new attribute will be introduced and applied to the MethodImpls that need to propagate to all applicable vtable slots: RequireMethodImplToRemainInEffect attribute. Example:
``` C#
class A {
void VirtualFunction() { }
}
class B : A {
void VirtualFunction() { .override A.VirtualFunction }
}
class C : B {
[RequireMethodImplToRemainInEffect]
void VirtualFunction() { .override A.VirtualFunction }
}
This attribute will only be applicable to methods with explicit overrides (MethodImpls), and takes no arguments. The proposed namespace for this attribute is the `System.Runtime.CompilerServices` namespace. Here is the proposed implementation of the attribute:
``` C#
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class RequireMethodImplToRemainInEffectAttribute : Attribute
{
}
}
Quick clarification as well: we do not expect any developer to directly use this attribute. It will be placed by the Roslyn compiler appropriately on methods.
cc @dotnet/crossgen-contrib
@vargaz @lambdageek should be looped in on this issue.
cc @janvorli
We will need to teach the IL linker to keep this attribute /cc @vitek-karas
We will need to teach the IL linker to keep this attribute /cc @vitek-karas
IL Linker will not strip attributes it knows nothing about because it would be a bug farm. https://github.com/mono/linker/issues/952#issuecomment-603110388
A few questions:
voidvirtual functionsRequireMethodImplToRemainInEffectAttribute this name seems a bit abstract. Can make this a bit clearer, like PreserveBaseOverridesAttribute?MethodImplAttributes?PreserveBaseOverridesAttribute naming to be reasonable as well, and if that makes more sense to the api review board, I'm all for it.TestClassDerived2 to override the original Method() on TestClassWithVirtualMethod.class BaseType {}
class DerivedType : BaseType {}
class TestClassWithVirtualMethod
{
public virtual BaseType Method() {}
}
class TestClassDerived1 : TestClassWithVirtualMethod
{
public override DerivedType Method() {}
}
class TestClassDerived2 : TestClassDerived1
{
// ERROR!, TypeLoadException!!
public override BaseType Method() {}
}
MethodImpl flag
If we choose to encode this in metadata, it should be a bit in the MethodAttributes column where the existing bits that control virtual method resolution live (Virtual, NewSlot, Strict, etc.). MethodImplAttributes does not sound right for this.
If we choose to encode this in metadata, it should be a bit in the MethodAttributes column
We don't have bits in the MethodAttribute column anymore, unless we want to do creative combinations (mdVirtual + mdUnmanagedExport?), or rev the file format version and reinterpret legacy useless bits differently (mdHasSecurity or mdRequireSecObject).
However, we need to be slightly careful with that as it needs to be hidden from the C# developer, as the flag is NOT something that is acceptable to adjust by setting a flag on the attribute from the C# language. It would need to be set by the compiler itself
Doesn't the same concern apply to the custom attribute? Roslyn already enfoces that user can't set bits in the MethodImpl flag (e.g. you can't set bit 1 and say the method body is native code).
Also, using a MethodImpl flag would imply that we need to update ildasm, and ilasm, which otherwise do not require adjustment.
We'll need to touch ILASM/ILDASM for covariant returns anyway: #37045.
Let's go with PreserveBaseOverridesAttribute then.
C#
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class PreserveBaseOverridesAttribute : Attribute
{
}
}
Fixed by #35308