Closed lambdageek closed 4 years ago
/cc @MichalStrehovsky
I haven't debugged through it, but the first thing I noticed is there's a conflicting definition of overrides for the Foo
and Bar
methods. One definition using name matches, and one using overrides. If you try to make such conflict using other means (e.g. two conflicting .override
s), you would get a TypeLoadException
. Maybe this should have been a TypeLoadException
, but at this point there's probably obfuscators relying on this behavior (obfuscators pretty much rely on every virtual resolution corner case that exists).
It's clear there's a conflict because if you reorder Foo
and Bar
in A
, you get different result.
WhatI think is happening is that we do a slot unification in B
for both Foo and Bar (slot unification is described in II.10.3.4 Impact of overrides on derived classes of the spec). There's no more (logical) slot 2 in B
, just one slot.
The slot unification is observable here (I removed the conflict to make it simpler to read):
.assembly TestAssembly { }
.class public A
extends [mscorlib]System.Object
{
.method virtual public instance void Foo()
{
ldstr "A::Foo"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method virtual public instance void Bar()
{
ldstr "A::Bar"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method public rtspecialname instance void .ctor()
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
.class public B
extends A
{
.method virtual public instance void Bar()
{
.override A::Foo
ldstr "B::Bar"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method public rtspecialname instance void .ctor()
{
ldarg.0
call instance void A::.ctor()
ret
}
}
.class public C
extends B
{
.method virtual public instance void Bar()
{
ldstr "C::Bar"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method public rtspecialname instance void .ctor()
{
ldarg.0
call instance void B::.ctor()
ret
}
}
.method static void Main()
{
.entrypoint
.locals init ([0] class A a)
newobj instance void C::.ctor()
stloc.0
ldloc.0
callvirt instance void A::Foo()
ldloc.0
callvirt instance void A::Bar()
ret
}
This will print:
C::Bar
C::Bar
If you try to make such conflict using other means (e.g. two conflicting .overrides), you would get a TypeLoadException. Maybe this should have been a TypeLoadException
As an aside, this was a TypeLoadException in .NET 1.0/1.1, it appears the behavior changed in .NET 2.0, which makes me curious if the change was related to the introduction of generics support somehow.
We could make this throw again, but this would likely break an obfuscator or two. Not sure if there's anything else we can do about this.
This was brought up as an issue for Mono, and I was curious if the .NET Core behavior here is surprising to you, or if everything is working as intended.
Reproduction steps
override.il
.class public A extends [mscorlib]System.Object { .method virtual public instance void Foo() { ldstr "A::Foo" call void [mscorlib]System.Console::WriteLine(string) ret }
}
.class public B extends A { .method virtual public instance void Foo() { .override A::Bar ldstr "B::Foo" call void [mscorlib]System.Console::WriteLine(string) ret }
}
.method static void Main() { .entrypoint .locals init ([0] class A a) newobj instance void B::.ctor() stloc.0 ldloc.0 callvirt instance void A::Foo() ldloc.0 callvirt instance void A::Bar() ret }
{ "runtimeOptions": { "tfm": "netcoreapp2.1", "framework": { "name": "Microsoft.NETCore.App", "version": "2.1.6" } } }
B::Foo B::Foo