diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs index 4e75d7db895ce..02ecf96568927 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs @@ -462,7 +462,13 @@ internal unsafe struct MethodTable // Additional conditional fields (see methodtable.h). // m_pModule - // m_pAuxiliaryData + + /// + /// A pointer to auxiliary data that is cold for method table. + /// + [FieldOffset(AuxiliaryDataOffset)] + public MethodTableAuxiliaryData* AuxiliaryData; + // union { // m_pEEClass (pointer to the EE class) // m_pCanonMT (pointer to the canonical method table) @@ -523,6 +529,12 @@ internal unsafe struct MethodTable private const int ParentMethodTableOffset = 0x10 + DebugClassNamePtr; +#if TARGET_64BIT + private const int AuxiliaryDataOffset = 0x20 + DebugClassNamePtr; +#else + private const int AuxiliaryDataOffset = 0x18 + DebugClassNamePtr; +#endif + #if TARGET_64BIT private const int ElementTypeOffset = 0x30 + DebugClassNamePtr; #else @@ -610,6 +622,28 @@ public TypeHandle GetArrayElementTypeHandle() public extern uint GetNumInstanceFieldBytes(); } + // Subset of src\vm\methodtable.h + [StructLayout(LayoutKind.Explicit)] + internal unsafe struct MethodTableAuxiliaryData + { + [FieldOffset(0)] + private uint Flags; + + private const uint enum_flag_CanCompareBitsOrUseFastGetHashCode = 0x0001; // Is any field type or sub field type overrode Equals or GetHashCode + private const uint enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode = 0x0002; // Whether we have checked the overridden Equals or GetHashCode + + public bool HasCheckedCanCompareBitsOrUseFastGetHashCode => (Flags & enum_flag_HasCheckedCanCompareBitsOrUseFastGetHashCode) != 0; + + public bool CanCompareBitsOrUseFastGetHashCode + { + get + { + Debug.Assert(HasCheckedCanCompareBitsOrUseFastGetHashCode); + return (Flags & enum_flag_CanCompareBitsOrUseFastGetHashCode) != 0; + } + } + } + /// /// A type handle, which can wrap either a pointer to a TypeDesc or to a . /// diff --git a/src/coreclr/System.Private.CoreLib/src/System/ValueType.cs b/src/coreclr/System.Private.CoreLib/src/System/ValueType.cs index cc13e37e083f0..78301866c36dc 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/ValueType.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/ValueType.cs @@ -10,15 +10,17 @@ ** ===========================================================*/ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System { [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public abstract class ValueType + public abstract partial class ValueType { [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075:UnrecognizedReflectionPattern", Justification = "Trimmed fields don't make a difference for equality")] @@ -36,7 +38,7 @@ public override unsafe bool Equals([NotNullWhen(true)] object? obj) // if there are no GC references in this object we can avoid reflection // and do a fast memcmp - if (CanCompareBits(this)) + if (CanCompareBitsOrUseFastGetHashCode(RuntimeHelpers.GetMethodTable(obj))) // MethodTable kept alive by access to object below { return SpanHelpers.SequenceEqual( ref RuntimeHelpers.GetRawData(this), @@ -66,8 +68,23 @@ ref RuntimeHelpers.GetRawData(obj), return true; } - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern bool CanCompareBits(object obj); + // Return true if the valuetype does not contain pointer, is tightly packed, + // does not have floating point number field and does not override Equals method. + private static unsafe bool CanCompareBitsOrUseFastGetHashCode(MethodTable* pMT) + { + MethodTableAuxiliaryData* pAuxData = pMT->AuxiliaryData; + + if (pAuxData->HasCheckedCanCompareBitsOrUseFastGetHashCode) + { + return pAuxData->CanCompareBitsOrUseFastGetHashCode; + } + + return CanCompareBitsOrUseFastGetHashCodeHelper(pMT); + } + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "MethodTable_CanCompareBitsOrUseFastGetHashCode")] + [return: MarshalAs(UnmanagedType.Bool)] + private static unsafe partial bool CanCompareBitsOrUseFastGetHashCodeHelper(MethodTable* pMT); /*=================================GetHashCode================================== **Action: Our algorithm for returning the hashcode is a little bit complex. We look @@ -79,8 +96,67 @@ ref RuntimeHelpers.GetRawData(obj), **Arguments: None. **Exceptions: None. ==============================================================================*/ - [MethodImpl(MethodImplOptions.InternalCall)] - public extern override int GetHashCode(); + public override unsafe int GetHashCode() + { + // The default implementation of GetHashCode() for all value types. + // Note that this implementation reveals the value of the fields. + // So if the value type contains any sensitive information it should + // implement its own GetHashCode(). + + MethodTable* pMT = RuntimeHelpers.GetMethodTable(this); + ref byte rawData = ref RuntimeHelpers.GetRawData(this); + HashCode hashCode = default; + + // To get less colliding and more evenly distributed hash codes, + // we munge the class index into the hashcode + hashCode.Add((IntPtr)pMT); + + if (CanCompareBitsOrUseFastGetHashCode(pMT)) + { + // this is a struct with no refs and no "strange" offsets + uint size = pMT->GetNumInstanceFieldBytes(); + hashCode.AddBytes(MemoryMarshal.CreateReadOnlySpan(ref rawData, (int)size)); + } + else + { + object thisRef = this; + switch (GetHashCodeStrategy(pMT, ObjectHandleOnStack.Create(ref thisRef), out uint fieldOffset, out uint fieldSize)) + { + case ValueTypeHashCodeStrategy.ReferenceField: + hashCode.Add(Unsafe.As(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode()); + break; + + case ValueTypeHashCodeStrategy.DoubleField: + hashCode.Add(Unsafe.As(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode()); + break; + + case ValueTypeHashCodeStrategy.SingleField: + hashCode.Add(Unsafe.As(ref Unsafe.AddByteOffset(ref rawData, fieldOffset)).GetHashCode()); + break; + + case ValueTypeHashCodeStrategy.FastGetHashCode: + Debug.Assert(fieldSize != 0); + hashCode.AddBytes(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AddByteOffset(ref rawData, fieldOffset), (int)fieldSize)); + break; + } + } + + return hashCode.ToHashCode(); + } + + // Must match the definition in src\vm\comutilnative.cpp + private enum ValueTypeHashCodeStrategy + { + None, + ReferenceField, + DoubleField, + SingleField, + FastGetHashCode, + } + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ValueType_GetHashCodeStrategy")] + private static unsafe partial ValueTypeHashCodeStrategy GetHashCodeStrategy( + MethodTable* pMT, ObjectHandleOnStack objHandle, out uint fieldOffset, out uint fieldSize); public override string? ToString() { diff --git a/src/coreclr/vm/comutilnative.cpp b/src/coreclr/vm/comutilnative.cpp index 027c4ae8903ae..612cb9d72dc0d 100644 --- a/src/coreclr/vm/comutilnative.cpp +++ b/src/coreclr/vm/comutilnative.cpp @@ -1681,226 +1681,127 @@ BOOL CanCompareBitsOrUseFastGetHashCode(MethodTable* mt) return canCompareBitsOrUseFastGetHashCode; } -NOINLINE static FC_BOOL_RET CanCompareBitsHelper(MethodTable* mt, OBJECTREF objRef) +extern "C" BOOL QCALLTYPE MethodTable_CanCompareBitsOrUseFastGetHashCode(MethodTable * mt) { - FC_INNER_PROLOG(ValueTypeHelper::CanCompareBits); - - _ASSERTE(mt != NULL); - _ASSERTE(objRef != NULL); + QCALL_CONTRACT; BOOL ret = FALSE; - HELPER_METHOD_FRAME_BEGIN_RET_ATTRIB_1(Frame::FRAME_ATTR_EXACT_DEPTH|Frame::FRAME_ATTR_CAPTURE_DEPTH_2, objRef); + BEGIN_QCALL; ret = CanCompareBitsOrUseFastGetHashCode(mt); - HELPER_METHOD_FRAME_END(); - FC_INNER_EPILOG(); + END_QCALL; - FC_RETURN_BOOL(ret); + return ret; } -// Return true if the valuetype does not contain pointer, is tightly packed, -// does not have floating point number field and does not override Equals method. -FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj) +enum ValueTypeHashCodeStrategy { - FCALL_CONTRACT; - - _ASSERTE(obj != NULL); - MethodTable* mt = obj->GetMethodTable(); + None, + ReferenceField, + DoubleField, + SingleField, + FastGetHashCode, +}; - if (mt->HasCheckedCanCompareBitsOrUseFastGetHashCode()) - { - FC_RETURN_BOOL(mt->CanCompareBitsOrUseFastGetHashCode()); - } - - OBJECTREF objRef(obj); - - FC_INNER_RETURN(FC_BOOL_RET, CanCompareBitsHelper(mt, objRef)); -} -FCIMPLEND - -static INT32 FastGetValueTypeHashCodeHelper(MethodTable *mt, void *pObjRef) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_COOPERATIVE; - } CONTRACTL_END; - - INT32 hashCode = 0; - INT32 *pObj = (INT32*)pObjRef; - - // this is a struct with no refs and no "strange" offsets, just go through the obj and xor the bits - INT32 size = mt->GetNumInstanceFieldBytes(); - for (INT32 i = 0; i < (INT32)(size / sizeof(INT32)); i++) - hashCode ^= *pObj++; - - return hashCode; -} - -static INT32 RegularGetValueTypeHashCode(MethodTable *mt, void *pObjRef) +static ValueTypeHashCodeStrategy GetHashCodeStrategy(MethodTable* mt, QCall::ObjectHandleOnStack objHandle, UINT32* fieldOffset, UINT32* fieldSize) { CONTRACTL { THROWS; GC_TRIGGERS; - MODE_COOPERATIVE; + MODE_PREEMPTIVE; } CONTRACTL_END; - INT32 hashCode = 0; + // Should be handled by caller + _ASSERTE(!mt->CanCompareBitsOrUseFastGetHashCode()); - GCPROTECT_BEGININTERIOR(pObjRef); + ValueTypeHashCodeStrategy ret = ValueTypeHashCodeStrategy::None; - BOOL canUseFastGetHashCodeHelper = FALSE; - if (mt->HasCheckedCanCompareBitsOrUseFastGetHashCode()) - { - canUseFastGetHashCodeHelper = mt->CanCompareBitsOrUseFastGetHashCode(); - } - else - { - canUseFastGetHashCodeHelper = CanCompareBitsOrUseFastGetHashCode(mt); - } + // Grab the first non-null field and return its hash code or 'it' as hash code + ApproxFieldDescIterator fdIterator(mt, ApproxFieldDescIterator::INSTANCE_FIELDS); - // While we should not get here directly from ValueTypeHelper::GetHashCode, if we recurse we need to - // be able to handle getting the hashcode for an embedded structure whose hashcode is computed by the fast path. - if (canUseFastGetHashCodeHelper) + FieldDesc *field; + while ((field = fdIterator.Next()) != NULL) { - hashCode = FastGetValueTypeHashCodeHelper(mt, pObjRef); - } - else - { - // it's looking ugly so we'll use the old behavior in managed code. Grab the first non-null - // field and return its hash code or 'it' as hash code - // Note that the old behavior has already been broken for value types - // that is qualified for CanUseFastGetHashCodeHelper. So maybe we should - // change the implementation here to use all fields instead of just the 1st one. - // - // - // check this approximation - we may be losing exact type information - ApproxFieldDescIterator fdIterator(mt, ApproxFieldDescIterator::INSTANCE_FIELDS); - - FieldDesc *field; - while ((field = fdIterator.Next()) != NULL) + _ASSERTE(!field->IsRVA()); + if (field->IsObjRef()) { - _ASSERTE(!field->IsRVA()); - if (field->IsObjRef()) + GCX_COOP(); + // if we get an object reference we get the hash code out of that + if (*(Object**)((BYTE *)objHandle.Get()->UnBox() + *fieldOffset + field->GetOffsetUnsafe()) != NULL) { - // if we get an object reference we get the hash code out of that - if (*(Object**)((BYTE *)pObjRef + field->GetOffsetUnsafe()) != NULL) - { - PREPARE_SIMPLE_VIRTUAL_CALLSITE(METHOD__OBJECT__GET_HASH_CODE, (*(Object**)((BYTE *)pObjRef + field->GetOffsetUnsafe()))); - DECLARE_ARGHOLDER_ARRAY(args, 1); - args[ARGNUM_0] = PTR_TO_ARGHOLDER(*(Object**)((BYTE *)pObjRef + field->GetOffsetUnsafe())); - CALL_MANAGED_METHOD(hashCode, INT32, args); - } - else - { - // null object reference, try next - continue; - } + *fieldOffset += field->GetOffsetUnsafe(); + ret = ValueTypeHashCodeStrategy::ReferenceField; } else { - CorElementType fieldType = field->GetFieldType(); - if (fieldType == ELEMENT_TYPE_R8) - { - PREPARE_NONVIRTUAL_CALLSITE(METHOD__DOUBLE__GET_HASH_CODE); - DECLARE_ARGHOLDER_ARRAY(args, 1); - args[ARGNUM_0] = PTR_TO_ARGHOLDER(((BYTE *)pObjRef + field->GetOffsetUnsafe())); - CALL_MANAGED_METHOD(hashCode, INT32, args); - } - else if (fieldType == ELEMENT_TYPE_R4) - { - PREPARE_NONVIRTUAL_CALLSITE(METHOD__SINGLE__GET_HASH_CODE); - DECLARE_ARGHOLDER_ARRAY(args, 1); - args[ARGNUM_0] = PTR_TO_ARGHOLDER(((BYTE *)pObjRef + field->GetOffsetUnsafe())); - CALL_MANAGED_METHOD(hashCode, INT32, args); - } - else if (fieldType != ELEMENT_TYPE_VALUETYPE) + // null object reference, try next + continue; + } + } + else + { + CorElementType fieldType = field->GetFieldType(); + if (fieldType == ELEMENT_TYPE_R8) + { + *fieldOffset += field->GetOffsetUnsafe(); + ret = ValueTypeHashCodeStrategy::DoubleField; + } + else if (fieldType == ELEMENT_TYPE_R4) + { + *fieldOffset += field->GetOffsetUnsafe(); + ret = ValueTypeHashCodeStrategy::SingleField; + } + else if (fieldType != ELEMENT_TYPE_VALUETYPE) + { + *fieldOffset += field->GetOffsetUnsafe(); + *fieldSize = field->LoadSize(); + ret = ValueTypeHashCodeStrategy::FastGetHashCode; + } + else + { + // got another value type. Get the type + TypeHandle fieldTH = field->GetFieldTypeHandleThrowing(); + _ASSERTE(!fieldTH.IsNull()); + MethodTable* fieldMT = fieldTH.GetMethodTable(); + if (CanCompareBitsOrUseFastGetHashCode(fieldMT)) { - UINT fieldSize = field->LoadSize(); - INT32 *pValue = (INT32*)((BYTE *)pObjRef + field->GetOffsetUnsafe()); - for (INT32 j = 0; j < (INT32)(fieldSize / sizeof(INT32)); j++) - hashCode ^= *pValue++; + *fieldOffset += field->GetOffsetUnsafe(); + *fieldSize = field->LoadSize(); + ret = ValueTypeHashCodeStrategy::FastGetHashCode; } else { - // got another value type. Get the type - TypeHandle fieldTH = field->GetFieldTypeHandleThrowing(); - _ASSERTE(!fieldTH.IsNull()); - hashCode = RegularGetValueTypeHashCode(fieldTH.GetMethodTable(), (BYTE *)pObjRef + field->GetOffsetUnsafe()); + *fieldOffset += field->GetOffsetUnsafe(); + ret = GetHashCodeStrategy(fieldMT, objHandle, fieldOffset, fieldSize); } } - break; } + break; } - GCPROTECT_END(); - - return hashCode; + return ret; } -// The default implementation of GetHashCode() for all value types. -// Note that this implementation reveals the value of the fields. -// So if the value type contains any sensitive information it should -// implement its own GetHashCode(). -FCIMPL1(INT32, ValueTypeHelper::GetHashCode, Object* objUNSAFE) +extern "C" INT32 QCALLTYPE ValueType_GetHashCodeStrategy(MethodTable* mt, QCall::ObjectHandleOnStack objHandle, UINT32* fieldOffset, UINT32* fieldSize) { - FCALL_CONTRACT; - - if (objUNSAFE == NULL) - FCThrow(kNullReferenceException); - - OBJECTREF obj = ObjectToOBJECTREF(objUNSAFE); - VALIDATEOBJECTREF(obj); + QCALL_CONTRACT; - INT32 hashCode = 0; - MethodTable *pMT = objUNSAFE->GetMethodTable(); + ValueTypeHashCodeStrategy ret = ValueTypeHashCodeStrategy::None; + *fieldOffset = 0; + *fieldSize = 0; - // We don't want to expose the method table pointer in the hash code - // Let's use the typeID instead. - UINT32 typeID = pMT->LookupTypeID(); - if (typeID == TypeIDProvider::INVALID_TYPE_ID) - { - // If the typeID has yet to be generated, fall back to GetTypeID - // This only needs to be done once per MethodTable - HELPER_METHOD_FRAME_BEGIN_RET_1(obj); - typeID = pMT->GetTypeID(); - HELPER_METHOD_FRAME_END(); - } + BEGIN_QCALL; - // To get less colliding and more evenly distributed hash codes, - // we munge the class index with two big prime numbers - hashCode = typeID * 711650207 + 2506965631U; - BOOL canUseFastGetHashCodeHelper = FALSE; - if (pMT->HasCheckedCanCompareBitsOrUseFastGetHashCode()) - { - canUseFastGetHashCodeHelper = pMT->CanCompareBitsOrUseFastGetHashCode(); - } - else - { - HELPER_METHOD_FRAME_BEGIN_RET_1(obj); - canUseFastGetHashCodeHelper = CanCompareBitsOrUseFastGetHashCode(pMT); - HELPER_METHOD_FRAME_END(); - } + ret = GetHashCodeStrategy(mt, objHandle, fieldOffset, fieldSize); - if (canUseFastGetHashCodeHelper) - { - hashCode ^= FastGetValueTypeHashCodeHelper(pMT, obj->UnBox()); - } - else - { - HELPER_METHOD_FRAME_BEGIN_RET_1(obj); - hashCode ^= RegularGetValueTypeHashCode(pMT, obj->UnBox()); - HELPER_METHOD_FRAME_END(); - } + END_QCALL; - return hashCode; + return ret; } -FCIMPLEND FCIMPL1(UINT32, MethodTableNative::GetNumInstanceFieldBytes, MethodTable* mt) { diff --git a/src/coreclr/vm/comutilnative.h b/src/coreclr/vm/comutilnative.h index 80d35da7b7214..a3c5ea65c3ca7 100644 --- a/src/coreclr/vm/comutilnative.h +++ b/src/coreclr/vm/comutilnative.h @@ -245,18 +245,14 @@ class COMInterlocked extern "C" void QCALLTYPE Interlocked_MemoryBarrierProcessWide(); -class ValueTypeHelper { -public: - static FCDECL1(FC_BOOL_RET, CanCompareBits, Object* obj); - static FCDECL1(INT32, GetHashCode, Object* objRef); -}; - class MethodTableNative { public: static FCDECL1(UINT32, GetNumInstanceFieldBytes, MethodTable* mt); }; extern "C" BOOL QCALLTYPE MethodTable_AreTypesEquivalent(MethodTable* mta, MethodTable* mtb); +extern "C" BOOL QCALLTYPE MethodTable_CanCompareBitsOrUseFastGetHashCode(MethodTable* mt); +extern "C" INT32 QCALLTYPE ValueType_GetHashCodeStrategy(MethodTable* mt, QCall::ObjectHandleOnStack objHandle, UINT32* fieldOffset, UINT32* fieldSize); class StreamNative { public: diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index bd4a209016652..8e68900686a7e 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -569,12 +569,6 @@ DEFINE_METHOD(OBJECT, GET_TYPE, GetType, DEFINE_METHOD(OBJECT, GET_HASH_CODE, GetHashCode, IM_RetInt) DEFINE_METHOD(OBJECT, EQUALS, Equals, IM_Obj_RetBool) -// DEFINE_CLASS(DOUBLE, System, Double) -DEFINE_METHOD(DOUBLE, GET_HASH_CODE, GetHashCode, IM_RetInt) - -// DEFINE_CLASS(SINGLE, System, Single) -DEFINE_METHOD(SINGLE, GET_HASH_CODE, GetHashCode, IM_RetInt) - DEFINE_CLASS(__CANON, System, __Canon) BEGIN_ILLINK_FEATURE_SWITCH(System.Runtime.InteropServices.BuiltInComInterop.IsSupported, true, true) diff --git a/src/coreclr/vm/ecalllist.h b/src/coreclr/vm/ecalllist.h index 1af6a5055e6c3..55b93f1f1ab68 100644 --- a/src/coreclr/vm/ecalllist.h +++ b/src/coreclr/vm/ecalllist.h @@ -76,11 +76,6 @@ FCFuncStart(gStringFuncs) FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_PtrSByt_Int_Int_Encoding_RetVoid, ECall::CtorSBytePtrStartLengthEncodingManaged) FCFuncEnd() -FCFuncStart(gValueTypeFuncs) - FCFuncElement("CanCompareBits", ValueTypeHelper::CanCompareBits) - FCFuncElement("GetHashCode", ValueTypeHelper::GetHashCode) -FCFuncEnd() - FCFuncStart(gDiagnosticsDebugger) FCFuncElement("BreakInternal", DebugDebugger::Break) FCFuncElement("get_IsAttached", DebugDebugger::IsDebuggerAttached) @@ -619,7 +614,6 @@ FCClassElement("Thread", "System.Threading", gThreadFuncs) FCClassElement("ThreadPool", "System.Threading", gThreadPoolFuncs) FCClassElement("Type", "System", gSystem_Type) FCClassElement("TypedReference", "System", gTypedReferenceFuncs) -FCClassElement("ValueType", "System", gValueTypeFuncs) #ifdef FEATURE_COMINTEROP FCClassElement("Variant", "System", gVariantFuncs) #endif diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index ec72d3112f5f8..8e6a670297943 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -100,6 +100,8 @@ static const Entry s_QCall[] = DllImportEntry(QCall_GetGCHandleForTypeHandle) DllImportEntry(QCall_FreeGCHandleForTypeHandle) DllImportEntry(MethodTable_AreTypesEquivalent) + DllImportEntry(MethodTable_CanCompareBitsOrUseFastGetHashCode) + DllImportEntry(ValueType_GetHashCodeStrategy) DllImportEntry(RuntimeTypeHandle_MakePointer) DllImportEntry(RuntimeTypeHandle_MakeByRef) DllImportEntry(RuntimeTypeHandle_MakeSZArray) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ValueTypeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ValueTypeTests.cs index 422f71e11c04f..92a2c006ce204 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ValueTypeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ValueTypeTests.cs @@ -299,6 +299,21 @@ public static void StructContainsPointerCompareTest() Assert.True(obj1.Equals(obj2)); Assert.Equal(obj1.GetHashCode(), obj2.GetHashCode()); } + + [Fact] + public static void StructContainsPointerNestedCompareTest() + { + StructContainsPointerNested obj1 = new StructContainsPointerNested(); + obj1.o = null; + obj1.value.value = 1; + + StructContainsPointerNested obj2 = new StructContainsPointerNested(); + obj2.o = null; + obj2.value.value = 1; + + Assert.True(obj1.Equals(obj2)); + Assert.Equal(obj1.GetHashCode(), obj2.GetHashCode()); + } public struct S { @@ -392,5 +407,11 @@ public struct StructContainsPointer public double value1; public double value2; } + + public struct StructContainsPointerNested + { + public object o; + public StructNonOverriddenEqualsOrGetHasCode value; + } } }