File: System\ValueType.cs
Web Access
Project: src\src\runtime\src\coreclr\nativeaot\System.Private.CoreLib\src\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

/*============================================================
**
**
**
** Purpose: Base class for all value classes.
**
**
===========================================================*/

using System.Diagnostics.CodeAnalysis;
using System.Runtime;
using System.Runtime.CompilerServices;

using Internal.Runtime;

using Debug = System.Diagnostics.Debug;

namespace System
{
    // CONTRACT with Runtime
    // Place holder type for type hierarchy, Compiler/Runtime requires this class
    [Serializable]
    [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public abstract class ValueType
    {
        public override string? ToString()
        {
            return this.GetType().ToString();
        }

        private const int GetNumFields = -1;

        // An override of this method will be injected by the compiler into all valuetypes that cannot be compared
        // using a simple memory comparison until the last byte as reported by sizeof.
        // This API is a bit awkward because we want to avoid burning more than one vtable slot on this.
        // The method returns the offset and type handle of the index-th field on this type.
        // When index == GetNumFields, this method is expected to return the number of fields of this
        // valuetype or a negative value. If the value is negative, the struct can be memcompared until
        // the byte specified by the negated return value.
        internal virtual unsafe int __GetFieldHelper(int index, out MethodTable* mt)
        {
            // Value types that don't override this method will use the fast path that looks at bytes, not fields.
            Debug.Assert(index == GetNumFields);
            mt = default;
            return -(int)this.GetMethodTable()->ValueTypeSize;
        }

        private unsafe int GetValueTypeSize(int numFields)
        {
            Debug.Assert(numFields < 0);
            int valueTypeSize = -numFields;
            Debug.Assert(valueTypeSize <= (int)this.GetMethodTable()->ValueTypeSize);

            return valueTypeSize;
        }

        public override unsafe bool Equals([NotNullWhen(true)] object? obj)
        {
            if (obj == null || obj.GetMethodTable() != this.GetMethodTable())
                return false;

            int numFields = __GetFieldHelper(GetNumFields, out _);

            ref byte thisRawData = ref this.GetRawData();
            ref byte thatRawData = ref obj.GetRawData();

            if (numFields < 0)
            {
                // Sanity check - if there are GC references, we should not be comparing bytes
                Debug.Assert(!this.GetMethodTable()->ContainsGCPointers);

                // Compare the memory
                return SpanHelpers.SequenceEqual(ref thisRawData, ref thatRawData, GetValueTypeSize(numFields));
            }
            else
            {
                // Foreach field, box and call the Equals method.
                for (int i = 0; i < numFields; i++)
                {
                    int fieldOffset = __GetFieldHelper(i, out MethodTable* fieldType);

                    // Fetch the value of the field on both types
                    object thisField = RuntimeImports.RhBoxAny(ref Unsafe.Add(ref thisRawData, fieldOffset), fieldType);
                    object thatField = RuntimeImports.RhBoxAny(ref Unsafe.Add(ref thatRawData, fieldOffset), fieldType);

                    // Compare the fields
                    if (thisField == null)
                    {
                        if (thatField != null)
                            return false;
                    }
                    else if (!thisField.Equals(thatField))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        public override unsafe int GetHashCode()
        {
            HashCode hashCode = default;
            hashCode.Add((IntPtr)this.GetMethodTable());

            int numFields = __GetFieldHelper(GetNumFields, out _);

            if (numFields < 0)
            {
                hashCode.AddBytes(new ReadOnlySpan<byte>(ref this.GetRawData(), GetValueTypeSize(numFields)));
            }
            else
            {
                RegularGetValueTypeHashCode(ref hashCode, ref this.GetRawData(), numFields);
            }

            return hashCode.ToHashCode();
        }

        private static unsafe ReadOnlySpan<byte> GetSpanForField(MethodTable* type, ref byte data)
        {
            // Sanity check - if there are GC references, we should not be hashing bytes
            Debug.Assert(!type->ContainsGCPointers);
            return new ReadOnlySpan<byte>(ref data, (int)type->ValueTypeSize);
        }

        private unsafe void RegularGetValueTypeHashCode(ref HashCode hashCode, ref byte data, int numFields)
        {
            // We only take the hashcode for the first non-null field. That's what the CLR does.
            for (int i = 0; i < numFields; i++)
            {
                int fieldOffset = __GetFieldHelper(i, out MethodTable* fieldType);
                ref byte fieldData = ref Unsafe.Add(ref data, fieldOffset);

                Debug.Assert(!fieldType->IsPointer && !fieldType->IsFunctionPointer);

                if (fieldType->ElementType == EETypeElementType.Single)
                {
                    hashCode.Add(Unsafe.As<byte, float>(ref fieldData));
                }
                else if (fieldType->ElementType == EETypeElementType.Double)
                {
                    hashCode.Add(Unsafe.As<byte, double>(ref fieldData));
                }
                else if (fieldType->IsPrimitive)
                {
                    hashCode.AddBytes(GetSpanForField(fieldType, ref fieldData));
                }
                else if (fieldType->IsValueType)
                {
                    // We have no option but to box since this value type could have
                    // GC pointers (we could find out if we want though), or fields of type Double/Single (we can't
                    // really find out). Double/Single have weird requirements around -0.0 and +0.0.
                    // If this boxing becomes a problem, we could build a piece of infrastructure that determines the slot
                    // of __GetFieldHelper, decodes the unboxing stub pointed to by the slot to the real target
                    // (we already have that part), and calls the entrypoint that expects a byref `this`, and use the
                    // data to decide between calling fast or regular hashcode helper.
                    var fieldValue = (ValueType)RuntimeExports.RhBox(fieldType, ref fieldData);
                    if (fieldValue != null)
                    {
                        hashCode.Add(fieldValue);
                    }
                    else
                    {
                        // nullable type with no value, try next
                        continue;
                    }
                }
                else
                {
                    object fieldValue = Unsafe.As<byte, object>(ref fieldData);
                    if (fieldValue != null)
                    {
                        hashCode.Add(fieldValue);
                    }
                    else
                    {
                        // null object reference, try next
                        continue;
                    }
                }
                break;
            }
        }
    }
}