-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix virtual static dispatch for variant interfaces when using default implementations #88639
Changes from all commits
5177fc3
0adb8e5
9f582ac
1c72076
6cc20de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6349,7 +6349,7 @@ namespace | |
candidateMaybe = interfaceMD; | ||
} | ||
} | ||
else | ||
else if (!interfaceMD->IsStatic()) | ||
{ | ||
// | ||
// A more specific interface - search for an methodimpl for explicit override | ||
|
@@ -6413,17 +6413,18 @@ namespace | |
} | ||
} | ||
} | ||
else if (pMD->IsStatic() && pMD->HasMethodImplSlot()) | ||
{ | ||
// Static virtual methods don't record MethodImpl slots so they need special handling | ||
candidateMaybe = pMT->TryResolveVirtualStaticMethodOnThisType( | ||
interfaceMT, | ||
interfaceMD, | ||
/* verifyImplemented */ FALSE, | ||
/* level */ level); | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
// Static virtual methods don't record MethodImpl slots so they need special handling | ||
candidateMaybe = pMT->TryResolveVirtualStaticMethodOnThisType( | ||
interfaceMT, | ||
interfaceMD, | ||
/* verifyImplemented */ FALSE, | ||
/* allowVariance */ allowVariance, | ||
/* level */ level); | ||
} | ||
} | ||
|
||
if (candidateMaybe == NULL) | ||
|
@@ -8911,7 +8912,7 @@ MethodTable::ResolveVirtualStaticMethod( | |
// Search for match on a per-level in the type hierarchy | ||
for (MethodTable* pMT = this; pMT != nullptr; pMT = pMT->GetParentMethodTable()) | ||
{ | ||
MethodDesc* pMD = pMT->TryResolveVirtualStaticMethodOnThisType(pInterfaceType, pInterfaceMD, verifyImplemented, level); | ||
MethodDesc* pMD = pMT->TryResolveVirtualStaticMethodOnThisType(pInterfaceType, pInterfaceMD, verifyImplemented, /*allowVariance*/ FALSE, level); | ||
if (pMD != nullptr) | ||
{ | ||
return pMD; | ||
|
@@ -8920,7 +8921,7 @@ MethodTable::ResolveVirtualStaticMethod( | |
if (pInterfaceType->HasVariance() || pInterfaceType->HasTypeEquivalence()) | ||
{ | ||
// Variant interface dispatch | ||
MethodTable::InterfaceMapIterator it = IterateInterfaceMap(); | ||
MethodTable::InterfaceMapIterator it = pMT->IterateInterfaceMap(); | ||
while (it.Next()) | ||
{ | ||
if (it.CurrentInterfaceMatches(this, pInterfaceType)) | ||
|
@@ -8955,7 +8956,7 @@ MethodTable::ResolveVirtualStaticMethod( | |
{ | ||
// Variant or equivalent matching interface found | ||
// Attempt to resolve on variance matched interface | ||
pMD = pMT->TryResolveVirtualStaticMethodOnThisType(pItfInMap, pInterfaceMD, verifyImplemented, level); | ||
pMD = pMT->TryResolveVirtualStaticMethodOnThisType(pItfInMap, pInterfaceMD, verifyImplemented, /*allowVariance*/ FALSE, level); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we be setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, if it's the case that in practice all variant resolution is only limited to the default interface implementation case (which is probably correct), why do we need this additional pass over the interface map in this place? |
||
if (pMD != nullptr) | ||
{ | ||
return pMD; | ||
|
@@ -8966,22 +8967,38 @@ MethodTable::ResolveVirtualStaticMethod( | |
} | ||
|
||
MethodDesc *pMDDefaultImpl = nullptr; | ||
BOOL haveUniqueDefaultImplementation = FindDefaultInterfaceImplementation( | ||
pInterfaceMD, | ||
pInterfaceType, | ||
&pMDDefaultImpl, | ||
/* allowVariance */ allowVariantMatches, | ||
/* throwOnConflict */ uniqueResolution == nullptr, | ||
level); | ||
if (haveUniqueDefaultImplementation || (pMDDefaultImpl != nullptr && (verifyImplemented || uniqueResolution != nullptr))) | ||
BOOL allowVariantMatchInDefaultImplementationLookup = FALSE; | ||
do | ||
{ | ||
// We tolerate conflicts upon verification of implemented SVMs so that they only blow up when actually called at execution time. | ||
if (uniqueResolution != nullptr) | ||
BOOL haveUniqueDefaultImplementation = FindDefaultInterfaceImplementation( | ||
pInterfaceMD, | ||
pInterfaceType, | ||
&pMDDefaultImpl, | ||
/* allowVariance */ allowVariantMatchInDefaultImplementationLookup, | ||
/* throwOnConflict */ uniqueResolution == nullptr, | ||
level); | ||
if (haveUniqueDefaultImplementation || (pMDDefaultImpl != nullptr && (verifyImplemented || uniqueResolution != nullptr))) | ||
{ | ||
*uniqueResolution = haveUniqueDefaultImplementation; | ||
// We tolerate conflicts upon verification of implemented SVMs so that they only blow up when actually called at execution time. | ||
if (uniqueResolution != nullptr) | ||
{ | ||
// Always report a unique resolution when reporting results of a variant match | ||
if (allowVariantMatchInDefaultImplementationLookup) | ||
*uniqueResolution = TRUE; | ||
else | ||
*uniqueResolution = haveUniqueDefaultImplementation; | ||
} | ||
return pMDDefaultImpl; | ||
} | ||
return pMDDefaultImpl; | ||
} | ||
|
||
// We only loop at most twice here | ||
if (allowVariantMatchInDefaultImplementationLookup) | ||
{ | ||
break; | ||
} | ||
|
||
allowVariantMatchInDefaultImplementationLookup = allowVariantMatches; | ||
} while (allowVariantMatchInDefaultImplementationLookup); | ||
} | ||
|
||
// Default implementation logic, which only kicks in for default implementations when looking up on an exact interface target | ||
|
@@ -9001,7 +9018,7 @@ MethodTable::ResolveVirtualStaticMethod( | |
// Try to locate the appropriate MethodImpl matching a given interface static virtual method. | ||
// Returns nullptr on failure. | ||
MethodDesc* | ||
MethodTable::TryResolveVirtualStaticMethodOnThisType(MethodTable* pInterfaceType, MethodDesc* pInterfaceMD, BOOL verifyImplemented, ClassLoadLevel level) | ||
MethodTable::TryResolveVirtualStaticMethodOnThisType(MethodTable* pInterfaceType, MethodDesc* pInterfaceMD, BOOL verifyImplemented, BOOL allowVariance, ClassLoadLevel level) | ||
{ | ||
HRESULT hr = S_OK; | ||
IMDInternalImport* pMDInternalImport = GetMDImport(); | ||
|
@@ -9049,9 +9066,22 @@ MethodTable::TryResolveVirtualStaticMethodOnThisType(MethodTable* pInterfaceType | |
ClassLoader::LoadTypes, | ||
CLASS_LOAD_EXACTPARENTS) | ||
.GetMethodTable(); | ||
if (pInterfaceMT != pInterfaceType) | ||
|
||
if (allowVariance) | ||
{ | ||
continue; | ||
// Allow variant, but not equivalent interface match | ||
if (!pInterfaceType->HasSameTypeDefAs(pInterfaceMT) || | ||
!pInterfaceMT->CanCastTo(pInterfaceType, NULL)) | ||
{ | ||
continue; | ||
} | ||
} | ||
else | ||
{ | ||
if (pInterfaceMT != pInterfaceType) | ||
{ | ||
continue; | ||
} | ||
} | ||
MethodDesc *pMethodDecl; | ||
|
||
|
@@ -9167,6 +9197,7 @@ MethodTable::VerifyThatAllVirtualStaticMethodsAreImplemented() | |
BOOL uniqueResolution; | ||
if (pMD->IsVirtual() && | ||
pMD->IsStatic() && | ||
!pMD->HasMethodImplSlot() && // Re-abstractions are virtual static abstract with a MethodImpl | ||
(pMD->IsAbstract() && | ||
!ResolveVirtualStaticMethod( | ||
pInterfaceMT, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Runtime; | ||
using Xunit; | ||
|
||
// This regression test tracks the issue where variant static interface dispatch crashes the runtime, and behaves incorrectly | ||
|
||
namespace VariantStaticInterfaceDispatchRegressionTest | ||
{ | ||
class Test | ||
{ | ||
static int Main() | ||
{ | ||
Console.WriteLine("Test cases"); | ||
|
||
Console.WriteLine("---FooBar"); | ||
TestTheFooString<FooBar, Base>("IFoo<Base>"); | ||
TestTheFooString<FooBar, Mid>("IFoo<Base>"); | ||
TestTheFooString<FooBar, Derived>("IFoo<Base>"); | ||
|
||
TestTheBarString<FooBar, Base>("IBar<Derived>"); | ||
TestTheBarString<FooBar, Mid>("IBar<Derived>"); | ||
TestTheBarString<FooBar, Derived>("IBar<Derived>"); | ||
|
||
Console.WriteLine("---FooBar2"); | ||
TestTheFooString<FooBar2, Base>("IFoo<Base>"); | ||
TestTheFooString<FooBar2, Mid>("IFoo<Mid>"); | ||
TestTheFooString<FooBar2, Derived>("IFoo<Base>"); | ||
|
||
TestTheBarString<FooBar2, Base>("IBar<Derived>"); | ||
TestTheBarString<FooBar2, Mid>("IBar<Mid>"); | ||
TestTheBarString<FooBar2, Derived>("IBar<Derived>"); | ||
|
||
Console.WriteLine("---FooBarBaz"); | ||
TestTheFooString<FooBarBaz, Base>("IFoo<Base>"); | ||
TestTheFooString<FooBarBaz, Mid>("IBaz"); | ||
TestTheFooString<FooBarBaz, Derived>("IBaz"); | ||
|
||
TestTheBarString<FooBarBaz, Base>("IBaz"); | ||
TestTheBarString<FooBarBaz, Mid>("IBaz"); | ||
TestTheBarString<FooBarBaz, Derived>("IBar<Derived>"); | ||
|
||
Console.WriteLine("---FooBarBazBoz"); | ||
TestTheFooString<FooBarBazBoz, Base>("IBoz"); | ||
TestTheFooString<FooBarBazBoz, Mid>("IBaz"); | ||
TestTheFooString<FooBarBazBoz, Derived>("IBoz"); | ||
|
||
TestTheBarString<FooBarBazBoz, Base>("IBoz"); | ||
TestTheBarString<FooBarBazBoz, Mid>("IBaz"); | ||
TestTheBarString<FooBarBazBoz, Derived>("IBoz"); | ||
|
||
Console.WriteLine("---FooBarBaz2"); | ||
TestTheFooString<FooBarBaz2, Base>("IFoo<Base>"); | ||
TestTheFooString<FooBarBaz2, Mid>("IBaz"); | ||
TestTheFooString<FooBarBaz2, Derived>("IFoo<Base>"); | ||
|
||
TestTheBarString<FooBarBaz2, Base>("IBar<Derived>"); | ||
TestTheBarString<FooBarBaz2, Mid>("IBaz"); | ||
TestTheBarString<FooBarBaz2, Derived>("IBar<Derived>"); | ||
|
||
Console.WriteLine("---FooBarBazBoz2"); | ||
TestTheFooString<FooBarBazBoz2, Base>("IBoz"); | ||
TestTheFooString<FooBarBazBoz2, Mid>("IBaz"); | ||
TestTheFooString<FooBarBazBoz2, Derived>("IBoz"); | ||
|
||
TestTheBarString<FooBarBazBoz2, Base>("IBoz"); | ||
TestTheBarString<FooBarBazBoz2, Mid>("IBaz"); | ||
TestTheBarString<FooBarBazBoz2, Derived>("IBoz"); | ||
return 100; | ||
} | ||
|
||
static string GetTheFooString<T, U>() where T : IFoo<U> { try { return T.GetString(); } catch (AmbiguousImplementationException) { return "AmbiguousImplementationException"; } } | ||
static string GetTheBarString<T, U>() where T : IBar<U> { try { return T.GetString(); } catch (AmbiguousImplementationException) { return "AmbiguousImplementationException"; } } | ||
static string GetTheFooStringInstance<T, U>() where T : IFoo<U>, new() { try { return (new T()).GetStringInstance(); } catch (AmbiguousImplementationException) { return "AmbiguousImplementationException"; } } | ||
static string GetTheBarStringInstance<T, U>() where T : IBar<U>, new() { try { return (new T()).GetStringInstance(); } catch (AmbiguousImplementationException) { return "AmbiguousImplementationException"; } } | ||
|
||
static void TestTheFooString<T, U>(string expected) where T : IFoo<U>, new() | ||
{ | ||
Console.WriteLine($"TestTheFooString {typeof(T).Name} {typeof(T).Name} {expected}"); | ||
Assert.Equal(expected, GetTheFooString<T, U>()); | ||
Assert.Equal(expected, GetTheFooStringInstance<T, U>()); | ||
} | ||
|
||
static void TestTheBarString<T, U>(string expected) where T : IBar<U>, new() | ||
{ | ||
Console.WriteLine($"TestTheBarString {typeof(T).Name} {typeof(T).Name} {expected}"); | ||
Assert.Equal(expected, GetTheBarString<T, U>()); | ||
Assert.Equal(expected, GetTheBarStringInstance<T, U>()); | ||
} | ||
|
||
interface IFoo<in T> | ||
{ | ||
static virtual string GetString() => $"IFoo<{typeof(T).Name}>"; | ||
virtual string GetStringInstance() => $"IFoo<{typeof(T).Name}>"; | ||
}; | ||
|
||
interface IBar<out T> | ||
{ | ||
static virtual string GetString() => $"IBar<{typeof(T).Name}>"; | ||
virtual string GetStringInstance() => $"IBar<{typeof(T).Name}>"; | ||
}; | ||
|
||
|
||
interface IBaz : IFoo<Mid>, IBar<Mid> | ||
{ | ||
static string IFoo<Mid>.GetString() => "IBaz"; | ||
static string IBar<Mid>.GetString() => "IBaz"; | ||
string IFoo<Mid>.GetStringInstance() => "IBaz"; | ||
string IBar<Mid>.GetStringInstance() => "IBaz"; | ||
} | ||
|
||
interface IBoz : IFoo<Base>, IBar<Derived> | ||
{ | ||
static string IFoo<Base>.GetString() => "IBoz"; | ||
static string IBar<Derived>.GetString() => "IBoz"; | ||
string IFoo<Base>.GetStringInstance() => "IBoz"; | ||
string IBar<Derived>.GetStringInstance() => "IBoz"; | ||
} | ||
|
||
class FooBar : IFoo<Base>, IBar<Derived> { } | ||
class FooBar2 : IFoo<Base>, IBar<Derived>, IFoo<Mid>, IBar<Mid> { } | ||
class FooBarBaz : FooBar, IBaz { } | ||
class FooBarBaz2 : IFoo<Base>, IBar<Derived>, IBaz { } // Implementation with all interfaces defined on the same type | ||
class FooBarBazBoz : FooBarBaz, IBoz { } | ||
class FooBarBazBoz2 : IFoo<Base>, IBar<Derived>, IBaz, IBoz { } // Implementation with all interfaces defined on the same type | ||
|
||
class Base { } | ||
class Mid : Base { } | ||
class Derived : Mid { } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
<OutputType>Exe</OutputType> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<Compile Include="$(MSBuildProjectName).cs" /> | ||
</ItemGroup> | ||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this parent type descent also be in a 2-pass loop over
allowVariance = FALSE, TRUE
whenallowVariantMatches
is set i.o.w. shouldn't it also be in the below loop used for default interface resolution?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, after getting my head around the tests I take it back, the variant lookup should take place before descending into parent.