File: Internal\Reflection\Execution\TypeLoader\TypeCast.cs
Web Access
Project: src\src\runtime\src\coreclr\nativeaot\System.Private.Reflection.Execution\src\System.Private.Reflection.Execution.csproj (System.Private.Reflection.Execution)
// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

using Debug = System.Diagnostics.Debug;

namespace Internal.Reflection.Execution
{
    /////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    //                                    **** WARNING ****
    //
    // A large portion of the logic present in this file is duplicated in ndp\rh\src\rtm\system\runtime\typecast.cs
    //
    //                                    **** WARNING ****
    //
    /////////////////////////////////////////////////////////////////////////////////////////////////////

    // This is not a general purpose type comparison facility. It is limited to what constraint validation needs.
    internal static partial class ConstraintValidator
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern",
            Justification = "Looking at interface list is safe because we wouldn't remove reflection-visible interface from a reflection-visible type")]
        private static bool ImplementsInterface(Type pObjType, Type pTargetType)
        {
            Debug.Assert(!pTargetType.IsArray, "did not expect array type");
            Debug.Assert(pTargetType.IsInterface, "IsInstanceOfInterface called with non-interface MethodTable");

            foreach (var pInterfaceType in pObjType.GetInterfaces())
            {
                if (AreTypesEquivalentInternal(pInterfaceType, pTargetType))
                {
                    return true;
                }
            }

            // We did not find the interface type in the list of supported interfaces. There's still one
            // chance left: if the target interface is generic and one or more of its type parameters is co or
            // contra variant then the object can still match if it implements a different instantiation of
            // the interface with type compatible generic arguments.
            //
            // An additional edge case occurs because of array covariance. This forces us to treat any generic
            // interfaces implemented by arrays as covariant over their one type parameter.
            // if (pTargetType.HasGenericVariance || (fArrayCovariance && pTargetType.IsGenericType))
            //
            if (pTargetType.IsGenericType)
            {
                bool fArrayCovariance = pObjType.IsArray;
                Type pTargetGenericType = pTargetType.GetGenericTypeDefinition();

                // Fetch the instantiations lazily only once we get a potential match
                Type[] pTargetInstantiation = null;
                Type[] pTargetGenericInstantiation = null;

                foreach (var pInterfaceType in pObjType.GetInterfaces())
                {
                    // We can ignore interfaces which are not also marked as having generic variance
                    // unless we're dealing with array covariance.
                    // if (pInterfaceType.HasGenericVariance || (fArrayCovariance && pInterfaceType.IsGenericType))

                    if (!pInterfaceType.IsGenericType)
                        continue;

                    // If the generic types aren't the same then the types aren't compatible.
                    if (!pInterfaceType.GetGenericTypeDefinition().Equals(pTargetGenericType))
                        continue;

                    Type[] pInterfaceInstantiation = pInterfaceType.GetGenericArguments();

                    if (pTargetInstantiation == null)
                    {
                        pTargetInstantiation = pTargetType.GetGenericArguments();

                        if (!fArrayCovariance)
                            pTargetGenericInstantiation = pTargetGenericType.GetGenericArguments();
                    }

                    // Compare the instantiations to see if they're compatible taking variance into account.
                    if (TypeParametersAreCompatible(pInterfaceInstantiation,
                                                    pTargetInstantiation,
                                                    pTargetGenericInstantiation,
                                                    fArrayCovariance))
                        return true;

                    if (fArrayCovariance)
                    {
                        Debug.Assert(pInterfaceInstantiation.Length == 1, "arity mismatch for array generic interface");
                        Debug.Assert(pTargetInstantiation.Length == 1, "arity mismatch for array generic interface");

                        // Special case for generic interfaces on arrays. Arrays of integral types (including enums)
                        // can be cast to generic interfaces over the integral types of the same size. For example
                        // int[] . IList<uint>.
                        if (ArePrimitiveTypesEquivalentSize(pInterfaceInstantiation[0],
                                                           pTargetInstantiation[0]))
                        {
                            // We have checked that the interface type definition matches above. The checks are ordered differently
                            // here compared with rtm\system\runtime\typecast.cs version because of TypeInfo does not let us do
                            // the HasGenericVariance optimization.
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        // Compare two types to see if they are compatible via generic variance.
        private static bool TypesAreCompatibleViaGenericVariance(Type pSourceType, Type pTargetType)
        {
            Type pTargetGenericType = pTargetType.GetGenericTypeDefinition();
            Type pSourceGenericType = pSourceType.GetGenericTypeDefinition();

            // If the generic types aren't the same then the types aren't compatible.
            if (pTargetGenericType.Equals(pSourceGenericType))
            {
                // Compare the instantiations to see if they're compatible taking variance into account.
                if (TypeParametersAreCompatible(pSourceType.GetGenericArguments(),
                                                pTargetType.GetGenericArguments(),
                                                pTargetGenericType.GetGenericArguments(),
                                                false))
                {
                    return true;
                }
            }

            return false;
        }

        // Compare two sets of generic type parameters to see if they're assignment compatible taking generic
        // variance into account. It's assumed they've already had their type definition matched (which
        // implies their arities are the same as well). The fForceCovariance argument tells the method to
        // override the defined variance of each parameter and instead assume it is covariant. This is used to
        // implement covariant array interfaces.
        private static bool TypeParametersAreCompatible(Type[] pSourceInstantiation,
                                                        Type[] pTargetInstantiation,
                                                        Type[] pVarianceInfo,
                                                        bool fForceCovariance)
        {
            // The types represent different instantiations of the same generic type. The
            // arity of both had better be the same.
            Debug.Assert(pSourceInstantiation.Length == pTargetInstantiation.Length, "arity mismatch between generic instantiations");

            Debug.Assert(fForceCovariance || pTargetInstantiation.Length == pVarianceInfo.Length, "arity mismatch between generic instantiations");

            // Walk through the instantiations comparing the cast compatibility of each pair
            // of type args.
            for (int i = 0; i < pTargetInstantiation.Length; i++)
            {
                Type pTargetArgType = pTargetInstantiation[i];
                Type pSourceArgType = pSourceInstantiation[i];

                GenericParameterAttributes varType;
                if (fForceCovariance)
                    varType = GenericParameterAttributes.Covariant;
                else
                    varType = pVarianceInfo[i].GenericParameterAttributes & GenericParameterAttributes.VarianceMask;

                switch (varType)
                {
                    case GenericParameterAttributes.None:
                        // Non-variant type params need to be identical.

                        if (!AreTypesEquivalentInternal(pSourceArgType, pTargetArgType))
                            return false;

                        break;

                    case GenericParameterAttributes.Covariant:
                        // For covariance (or out type params in C#) the object must implement an
                        // interface with a more derived type arg than the target interface. Or
                        // the object interface can have a type arg that is an interface
                        // implemented by the target type arg.
                        // For instance:
                        //   class Foo : ICovariant<String> is ICovariant<Object>
                        //   class Foo : ICovariant<Bar> is ICovariant<IBar>
                        //   class Foo : ICovariant<IBar> is ICovariant<Object>

                        if (!AreTypesAssignableInternal(pSourceArgType, pTargetArgType, false, false))
                            return false;

                        break;

                    case GenericParameterAttributes.Contravariant:
                        // For contravariance (or in type params in C#) the object must implement
                        // an interface with a less derived type arg than the target interface. Or
                        // the object interface can have a type arg that is a class implementing
                        // the interface that is the target type arg.
                        // For instance:
                        //   class Foo : IContravariant<Object> is IContravariant<String>
                        //   class Foo : IContravariant<IBar> is IContravariant<Bar>
                        //   class Foo : IContravariant<Object> is IContravariant<IBar>

                        if (!AreTypesAssignableInternal(pTargetArgType, pSourceArgType, false, false))
                            return false;

                        break;

                    default:
                        Debug.Fail("unknown generic variance type");
                        return false;
                }
            }

            return true;
        }

        // Internally callable version of the export method above. Has two additional parameters:
        //  fBoxedSource            : assume the source type is boxed so that value types and enums are
        //                            compatible with Object, ValueType and Enum (if applicable)
        //  fAllowSizeEquivalence   : allow identically sized integral types and enums to be considered
        //                            equivalent (currently used only for array element types)
        private static bool AreTypesAssignableInternal(Type pSourceType, Type pTargetType, bool fBoxedSource, bool fAllowSizeEquivalence)
        {
            //
            // Are the types identical?
            //
            if (AreTypesEquivalentInternal(pSourceType, pTargetType))
                return true;

            //
            // Handle cast to interface cases.
            //
            if (pTargetType.IsInterface)
            {
                // Value types can only be cast to interfaces if they're boxed.
                if (!fBoxedSource && pSourceType.IsValueType)
                    return false;

                if (ImplementsInterface(pSourceType, pTargetType))
                    return true;

                // Are the types compatible due to generic variance?
                // if (pTargetType.HasGenericVariance && pSourceType.HasGenericVariance)
                if (pTargetType.IsGenericType && pSourceType.IsGenericType)
                    return TypesAreCompatibleViaGenericVariance(pSourceType, pTargetType);

                return false;
            }
            if (pSourceType.IsInterface)
            {
                // The only non-interface type an interface can be cast to is Object.
                return pTargetType.IsSystemObject();
            }

            //
            // Handle cast to array cases.
            //
            if (pTargetType.IsArray)
            {
                if (pSourceType.IsArray)
                {
                    if (pSourceType.GetElementType().IsPointer)
                    {
                        // If the element types are pointers, then only exact matches are correct.
                        // As we've already called AreTypesEquivalent at the start of this function,
                        // return false as the exact match case has already been handled.
                        // int** is not compatible with uint**, nor is int*[] oompatible with uint*[].
                        return false;
                    }
                    else
                    {
                        // Source type is also a pointer. Are the element types compatible? Note that using
                        // AreTypesAssignableInternal here handles array covariance as well as IFoo[] . Foo[]
                        // etc. Pass false for fBoxedSource since int[] is not assignable to object[].
                        return AreTypesAssignableInternal(pSourceType.GetElementType(), pTargetType.GetElementType(), false, true);
                    }
                }

                // Can't cast a non-array type to an array.
                return false;
            }
            if (pSourceType.IsArray)
            {
                // Target type is not an array. But we can still cast arrays to Object or System.Array.
                return pTargetType.IsSystemObject() || pTargetType.IsSystemArray();
            }

            //
            // Handle pointer cases
            //
            if (pTargetType.IsPointer)
            {
                if (pSourceType.IsPointer)
                {
                    if (pSourceType.GetElementType().IsPointer)
                    {
                        // If the element types are pointers, then only exact matches are correct.
                        // As we've already called AreTypesEquivalent at the start of this function,
                        // return false as the exact match case has already been handled.
                        // int** is not compatible with uint**, nor is int*[] compatible with uint*[].
                        return false;
                    }
                    else
                    {
                        // Source type is also a pointer. Are the element types compatible? Note that using
                        // AreTypesAssignableInternal here handles array covariance as well as IFoo[] . Foo[]
                        // etc. Pass false for fBoxedSource since int[] is not assignable to object[].
                        return AreTypesAssignableInternal(pSourceType.GetElementType(), pTargetType.GetElementType(), false, true);
                    }
                }

                return false;
            }
            else if (pSourceType.IsPointer)
            {
                return false;
            }

            //
            // Handle cast to other (non-interface, non-array) cases.
            //

            if (pSourceType.IsValueType)
            {
                // Certain value types of the same size are treated as equivalent when the comparison is
                // between array element types (indicated by fAllowSizeEquivalence). These are integer types
                // of the same size (e.g. int and uint) and the base type of enums vs all integer types of the
                // same size.
                if (fAllowSizeEquivalence && pTargetType.IsValueType)
                {
                    if (ArePrimitiveTypesEquivalentSize(pSourceType, pTargetType))
                        return true;

                    // Non-identical value types aren't equivalent in any other case (since value types are
                    // sealed).
                    return false;
                }

                // If the source type is a value type but it's not boxed then we've run out of options: the types
                // are not identical, the target type isn't an interface and we're not allowed to check whether
                // the target type is a parent of this one since value types are sealed and thus the only matches
                // would be against Object, ValueType or Enum, all of which are reference types and not compatible
                // with non-boxed value types.
                if (!fBoxedSource)
                    return false;
            }

            //
            // Are the types compatible via generic variance?
            //
            // if (pTargetType.HasGenericVariance && pSourceType.HasGenericVariance)
            if (pTargetType.IsGenericType && pSourceType.IsGenericType)
            {
                if (TypesAreCompatibleViaGenericVariance(pSourceType, pTargetType))
                    return true;
            }

            // Is the source type derived from the target type?
            if (IsDerived(pSourceType, pTargetType))
                return true;

            return false;
        }

        private static bool IsDerived(Type pDerivedType, Type pBaseType)
        {
            Debug.Assert(!pBaseType.IsInterface, "did not expect interface type");

            for (; ; )
            {
                if (AreTypesEquivalentInternal(pDerivedType, pBaseType))
                    return true;

                Type baseType = pDerivedType.BaseType;
                if (baseType == null)
                    return false;

                pDerivedType = baseType;
            }
        }

        // Method to compare two types pointers for type equality
        // We cannot just compare the pointers as there can be duplicate type instances
        // for cloned and constructed types.
        private static bool AreTypesEquivalentInternal(Type pType1, Type pType2)
        {
            if (!pType1.IsInstantiatedTypeInfo() && !pType2.IsInstantiatedTypeInfo())
                return pType1.Equals(pType2);

            if (pType1.IsGenericType && pType2.IsGenericType)
            {
                if (!pType1.GetGenericTypeDefinition().Equals(pType2.GetGenericTypeDefinition()))
                    return false;

                Type[] args1 = pType1.GetGenericArguments();
                Type[] args2 = pType2.GetGenericArguments();
                Debug.Assert(args1.Length == args2.Length);

                for (int i = 0; i < args1.Length; i++)
                {
                    if (!AreTypesEquivalentInternal(args1[i], args2[i]))
                        return false;
                }

                return true;
            }

            if (pType1.IsArray && pType2.IsArray)
            {
                if (pType1.GetArrayRank() != pType2.GetArrayRank())
                    return false;

                return AreTypesEquivalentInternal(pType1.GetElementType(), pType2.GetElementType());
            }

            if (pType1.IsPointer && pType2.IsPointer)
            {
                return AreTypesEquivalentInternal(pType1.GetElementType(), pType2.GetElementType());
            }

            return false;
        }

        private static bool ArePrimitiveTypesEquivalentSize(Type pType1, Type pType2)
        {
            int normalizedType1 = NormalizedPrimitiveTypeSizeForIntegerTypes(pType1);
            if (normalizedType1 == 0)
                return false;

            int normalizedType2 = NormalizedPrimitiveTypeSizeForIntegerTypes(pType2);

            return normalizedType1 == normalizedType2;
        }
    }
}