diff --git a/src/coreclr/dlls/mscorrc/mscorrc.rc b/src/coreclr/dlls/mscorrc/mscorrc.rc index 67962014926b63..724394d9911bb2 100644 --- a/src/coreclr/dlls/mscorrc/mscorrc.rc +++ b/src/coreclr/dlls/mscorrc/mscorrc.rc @@ -397,6 +397,10 @@ BEGIN IDS_EE_CANNOTCAST "Unable to cast object of type '%1' to type '%2'." IDS_EE_CANNOTCASTSAME "[A]%1 cannot be cast to [B]%2. %3. %4." + IDS_EE_CANNOTCASTSAME_DETAIL_BYTE_ARRAY "Type %1 originates from '%2' in the context '%3' in a byte array" + IDS_EE_CANNOTCASTSAME_DETAIL_LOCATION "Type %1 originates from '%2' in the context '%3' at location '%4'" + IDS_EE_CANNOTCASTSAME_GENARG_BYTE_ARRAY "Type %1 has a generic argument '%2' that originates from '%3' in the context '%4' in a byte array" + IDS_EE_CANNOTCASTSAME_GENARG_LOCATION "Type %1 has a generic argument '%2' that originates from '%3' in the context '%4' at location '%5'" IDS_EE_INVALID_VT_FOR_CUSTOM_MARHALER "Type of the VARIANT specified for a parameter with a custom marshaler is not supported by the custom marshaler." IDS_EE_BAD_COMEXTENDS_CLASS "Types extending from COM objects should override all methods of an interface implemented by the base COM class." IDS_EE_MARSHAL_UNMAPPABLE_CHAR "Cannot marshal: Encountered unmappable character." diff --git a/src/coreclr/dlls/mscorrc/resource.h b/src/coreclr/dlls/mscorrc/resource.h index 542e8cfbba4b77..9906a03515c1f3 100644 --- a/src/coreclr/dlls/mscorrc/resource.h +++ b/src/coreclr/dlls/mscorrc/resource.h @@ -523,3 +523,7 @@ #define IDS_EE_NDIRECT_DISABLEDMARSHAL_LCID 0x264E #define IDS_EE_NDIRECT_DISABLEDMARSHAL_PRESERVESIG 0x264F #define IDS_EE_NDIRECT_DISABLEDMARSHAL_VARARGS 0x2650 +#define IDS_EE_CANNOTCASTSAME_DETAIL_BYTE_ARRAY 0x2651 +#define IDS_EE_CANNOTCASTSAME_DETAIL_LOCATION 0x2652 +#define IDS_EE_CANNOTCASTSAME_GENARG_BYTE_ARRAY 0x2653 +#define IDS_EE_CANNOTCASTSAME_GENARG_LOCATION 0x2654 diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index d53c2052479bf3..57e69ebec1fc26 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -10130,35 +10130,107 @@ VOID DECLSPEC_NORETURN RealCOMPlusThrowHR(EXCEPINFO *pExcepInfo) // Throw an InvalidCastException //========================================================================== -VOID GetAssemblyDetailInfo(SString &sType, - SString &sAssemblyDisplayName, - PEAssembly *pPEAssembly, - SString &sAssemblyDetailInfo) +static VOID GetAssemblyDetailInfo(SString &sType, + SString &sAssemblyDisplayName, + PEAssembly *pPEAssembly, + SString &sAssemblyDetailInfo) { - WRAPPER_NO_CONTRACT; + STANDARD_VM_CONTRACT; + + SString sAlcName; + pPEAssembly->GetAssemblyBinder()->GetNameForDiagnostics(sAlcName); + SString assemblyPath{ pPEAssembly->GetPath() }; + + SString resStr; + SString formatted; + if (assemblyPath.IsEmpty()) + { + if (resStr.LoadResource(IDS_EE_CANNOTCASTSAME_DETAIL_BYTE_ARRAY)) + { + formatted.FormatMessage(FORMAT_MESSAGE_FROM_STRING, (LPCWSTR)resStr, 0, 0, + sType, sAssemblyDisplayName, sAlcName); + sAssemblyDetailInfo.Append(formatted); + } + } + else + { + if (resStr.LoadResource(IDS_EE_CANNOTCASTSAME_DETAIL_LOCATION)) + { + formatted.FormatMessage(FORMAT_MESSAGE_FROM_STRING, (LPCWSTR)resStr, 0, 0, + sType, sAssemblyDisplayName, sAlcName, assemblyPath); + sAssemblyDetailInfo.Append(formatted); + } + } +} - SString detailsUtf8; +static VOID GetGenericArgAssemblyDetailInfo(SString &sType, + SString &sGenericArgName, + SString &sAssemblyDisplayName, + PEAssembly *pPEAssembly, + SString &sAssemblyDetailInfo) +{ + STANDARD_VM_CONTRACT; SString sAlcName; pPEAssembly->GetAssemblyBinder()->GetNameForDiagnostics(sAlcName); SString assemblyPath{ pPEAssembly->GetPath() }; + + SString resStr; + SString formatted; if (assemblyPath.IsEmpty()) { - detailsUtf8.Printf("Type %s originates from '%s' in the context '%s' in a byte array", - sType.GetUTF8(), - sAssemblyDisplayName.GetUTF8(), - sAlcName.GetUTF8()); + if (resStr.LoadResource(IDS_EE_CANNOTCASTSAME_GENARG_BYTE_ARRAY)) + { + formatted.FormatMessage(FORMAT_MESSAGE_FROM_STRING, (LPCWSTR)resStr, 0, 0, + sType, sGenericArgName, sAssemblyDisplayName, sAlcName); + sAssemblyDetailInfo.Append(formatted); + } } else { - detailsUtf8.Printf("Type %s originates from '%s' in the context '%s' at location '%s'", - sType.GetUTF8(), - sAssemblyDisplayName.GetUTF8(), - sAlcName.GetUTF8(), - assemblyPath.GetUTF8()); + if (resStr.LoadResource(IDS_EE_CANNOTCASTSAME_GENARG_LOCATION)) + { + formatted.FormatMessage(FORMAT_MESSAGE_FROM_STRING, (LPCWSTR)resStr, 0, 0, + sType, sGenericArgName, sAssemblyDisplayName, sAlcName, assemblyPath); + sAssemblyDetailInfo.Append(formatted); + } } +} + +static BOOL FindFirstDifferingGenericArgument(TypeHandle thFrom, TypeHandle thTo, + TypeHandle *pthArgFrom, TypeHandle *pthArgTo, + DWORD depth = 0) +{ + WRAPPER_NO_CONTRACT; + + if (depth > 8) + return FALSE; + + Instantiation instFrom = thFrom.GetInstantiation(); + Instantiation instTo = thTo.GetInstantiation(); + + if (instFrom.IsEmpty() || instTo.IsEmpty() || instFrom.GetNumArgs() != instTo.GetNumArgs()) + return FALSE; - sAssemblyDetailInfo.Append(detailsUtf8.GetUnicode()); + for (DWORD i = 0; i < instFrom.GetNumArgs(); i++) + { + if (instFrom[i] == instTo[i]) + continue; + + Module *pModFrom = instFrom[i].GetModule(); + Module *pModTo = instTo[i].GetModule(); + if (pModFrom != NULL && pModTo != NULL && pModFrom == pModTo) + { + if (FindFirstDifferingGenericArgument(instFrom[i], instTo[i], pthArgFrom, pthArgTo, depth + 1)) + return TRUE; + } + + *pthArgFrom = instFrom[i]; + *pthArgTo = instTo[i]; + return TRUE; + } + + return FALSE; } VOID CheckAndThrowSameTypeAndAssemblyInvalidCastException(TypeHandle thCastFrom, @@ -10198,6 +10270,60 @@ VOID CheckAndThrowSameTypeAndAssemblyInvalidCastException(TypeHandle thCastFrom, thCastFrom.GetName(strCastFromName); thCastTo.GetName(strCastToName); + // If the outermost types come from the same module, the actual difference + // must be in their generic type arguments. Report the differing generic + // argument's assembly info instead, which is more helpful to the user. + TypeHandle thDifferingArgFrom, thDifferingArgTo; + if (pModuleTypeFrom == pModuleTypeTo && + FindFirstDifferingGenericArgument(thCastFrom, thCastTo, &thDifferingArgFrom, &thDifferingArgTo)) + { + Module *pModuleArgFrom = thDifferingArgFrom.GetModule(); + Module *pModuleArgTo = thDifferingArgTo.GetModule(); + + if ((pModuleArgFrom != NULL) && (pModuleArgTo != NULL)) + { + StackSString sGenericArgName; + thDifferingArgFrom.GetName(sGenericArgName); + + Assembly *pAssemblyArgFrom = pModuleArgFrom->GetAssembly(); + Assembly *pAssemblyArgTo = pModuleArgTo->GetAssembly(); + + _ASSERTE(pAssemblyArgFrom != NULL); + _ASSERTE(pAssemblyArgTo != NULL); + + PEAssembly *pPEAssemblyArgFrom = pAssemblyArgFrom->GetPEAssembly(); + PEAssembly *pPEAssemblyArgTo = pAssemblyArgTo->GetPEAssembly(); + + _ASSERTE(pPEAssemblyArgFrom != NULL); + _ASSERTE(pPEAssemblyArgTo != NULL); + + StackSString sArgAssemblyFromDisplayName; + StackSString sArgAssemblyToDisplayName; + pPEAssemblyArgFrom->GetDisplayName(sArgAssemblyFromDisplayName); + pPEAssemblyArgTo->GetDisplayName(sArgAssemblyToDisplayName); + + SString typeA = SL(W("A")); + GetGenericArgAssemblyDetailInfo(typeA, + sGenericArgName, + sArgAssemblyFromDisplayName, + pPEAssemblyArgFrom, + sAssemblyDetailInfoFrom); + SString typeB = SL(W("B")); + GetGenericArgAssemblyDetailInfo(typeB, + sGenericArgName, + sArgAssemblyToDisplayName, + pPEAssemblyArgTo, + sAssemblyDetailInfoTo); + + COMPlusThrow(kInvalidCastException, + IDS_EE_CANNOTCASTSAME, + strCastFromName.GetUnicode(), + strCastToName.GetUnicode(), + sAssemblyDetailInfoFrom.GetUnicode(), + sAssemblyDetailInfoTo.GetUnicode()); + } + } + SString typeA = SL(W("A")); GetAssemblyDetailInfo(typeA, sAssemblyFromDisplayName, diff --git a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs index 3292f12de6d7b8..31af15049c26ec 100644 --- a/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs +++ b/src/libraries/System.Runtime.Loader/tests/AssemblyLoadContextTest.cs @@ -5,6 +5,7 @@ using System.IO; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; using Xunit; namespace System.Runtime.Loader.Tests @@ -275,5 +276,55 @@ public static void LoadNonRuntimeAssembly() Exception error = Assert.Throws(() => alc.LoadFromAssemblyName(new AssemblyName("MyAssembly"))); Assert.IsType(error.InnerException); } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAssemblyLoadingSupported), nameof(PlatformDetection.IsCoreCLR))] + public static void InvalidCastException_DifferentALC_ShowsAssemblyInfo() + { + var alc = new AssemblyLoadContext("TestALC"); + Assembly alcAssembly = alc.LoadFromAssemblyPath(typeof(AssemblyLoadContextTest).Assembly.Location); + + Type alcType = alcAssembly.GetType(typeof(InvalidCastSharedType).FullName!, throwOnError: true)!; + object instance = Activator.CreateInstance(alcType)!; + + // Cast directly to InvalidCastSharedType from the Default ALC. + var ice = Assert.Throws(() => + { + InvalidCastSharedType _ = (InvalidCastSharedType)instance; + }); + + // The message should report both ALC contexts for the same-named type. + Assert.Contains(nameof(InvalidCastSharedType), ice.Message); + Assert.Contains("Default", ice.Message); + Assert.Contains("TestALC", ice.Message); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAssemblyLoadingSupported), nameof(PlatformDetection.IsCoreCLR))] + public static void InvalidCastException_GenericTypeArg_DifferentALC_ShowsAssemblyInfo() + { + var alc = new AssemblyLoadContext("TestALC"); + Assembly alcAssembly = alc.LoadFromAssemblyPath(typeof(AssemblyLoadContextTest).Assembly.Location); + + // The outer type (StrongBox) is from CoreLib (same in all contexts), + // but the generic argument comes from a different ALC. + Type alcType = alcAssembly.GetType(typeof(InvalidCastSharedType).FullName!, throwOnError: true)!; + Type boxType = typeof(StrongBox<>).MakeGenericType(alcType); + object instance = Activator.CreateInstance(boxType)!; + + var ice = Assert.Throws(() => + { + StrongBox _ = (StrongBox)instance; + }); + + // The message should include the types with the differing generic argument types + // and the ALC context names. + Assert.Contains(nameof(StrongBox), ice.Message); + Assert.Contains($"generic argument '{typeof(InvalidCastSharedType).FullName}", ice.Message); + Assert.Contains("Default", ice.Message); + Assert.Contains("TestALC", ice.Message); + } + } + + public class InvalidCastSharedType + { } }