File: Compiler\CompilerTypeSystemContext.BoxedTypes.cs
Web Access
Project: src\src\runtime\src\coreclr\tools\aot\ILCompiler.Compiler\ILCompiler.Compiler.csproj (ILCompiler.Compiler)
// 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 Internal;
using Internal.TypeSystem;
using Internal.IL;
using Internal.IL.Stubs;

using Debug = System.Diagnostics.Debug;

//
// Functionality related to instantiating unboxing thunks
//
// To support calling canonical interface methods on generic valuetypes,
// the compiler needs to generate unboxing+instantiating thunks that bridge
// the difference between the two calling conventions.
//
// As a refresher:
// * Instance methods on shared generic valuetypes expect two arguments
//   (aside from the arguments declared in the signature): a ByRef to the
//   first byte of the value of the valuetype (this), and a generic context
//   argument (MethodTable)
// * Interface calls expect 'this' to be a reference type (with the generic
//   context to be inferred from 'this' by the callee).
//
// Instantiating and unboxing stubs bridge this by extracting a managed
// pointer out of a boxed valuetype, along with the MethodTable of the boxed
// valuetype (to provide the generic context) before dispatching to the
// instance method with the different calling convention.
//
// We compile them by:
// * Pretending the unboxing stub is an instance method on a reference type
//   with the same layout as a boxed valuetype (this matches the calling
//   convention expected by the caller).
// * Having the unboxing stub load the m_pEEType field (to get generic
//   context) and a byref to the actual value (to get a 'this' expected by
//   valuetype methods)
// * Using a JIT intrinsic to set the generic context on the called method.
//

namespace ILCompiler
{
    // Contains functionality related to pseudotypes representing boxed instances of value types
    public partial class CompilerTypeSystemContext
    {
        /// <summary>
        /// For a shared (canonical) instance method on a generic valuetype, gets a method that can be used to call the
        /// method given a boxed version of the generic valuetype as 'this' pointer.
        /// </summary>
        public MethodDesc GetSpecialUnboxingThunk(MethodDesc targetMethod, ModuleDesc ownerModuleOfThunk)
        {
            Debug.Assert(targetMethod.IsSharedByGenericInstantiations);
            Debug.Assert(!targetMethod.Signature.IsStatic);
            Debug.Assert(!targetMethod.HasInstantiation);

            TypeDesc owningType = targetMethod.OwningType;
            Debug.Assert(owningType.IsValueType);

            var owningTypeDefinition = (MetadataType)owningType.GetTypeDefinition();

            // Get a reference type that has the same layout as the boxed valuetype.
            var typeKey = new BoxedValuetypeHashtableKey(owningTypeDefinition, ownerModuleOfThunk);
            BoxedValueType boxedTypeDefinition = _boxedValuetypeHashtable.GetOrCreateValue(typeKey);

            // Get a method on the reference type with the same signature as the target method (but different
            // calling convention, since 'this' will be a reference type).
            var targetMethodDefinition = targetMethod.GetTypicalMethodDefinition();
            var methodKey = new UnboxingThunkHashtableKey(targetMethodDefinition, boxedTypeDefinition);
            GenericUnboxingThunk thunkDefinition = _unboxingThunkHashtable.GetOrCreateValue(methodKey);

            // Find the thunk on the instantiated version of the reference type.
            Debug.Assert(owningType != owningTypeDefinition);
            InstantiatedType boxedType = boxedTypeDefinition.MakeInstantiatedType(owningType.Instantiation);

            MethodDesc thunk = GetMethodForInstantiatedType(thunkDefinition, boxedType);
            Debug.Assert(!thunk.HasInstantiation);

            return thunk;
        }

        public MethodDesc GetUnboxingThunk(MethodDesc targetMethod, ModuleDesc ownerModuleOfThunk)
        {
            TypeDesc owningType = targetMethod.OwningType;
            Debug.Assert(owningType.IsValueType);

            var owningTypeDefinition = (MetadataType)owningType.GetTypeDefinition();

            // Get a reference type that has the same layout as the boxed valuetype.
            var typeKey = new BoxedValuetypeHashtableKey(owningTypeDefinition, ownerModuleOfThunk);
            BoxedValueType boxedTypeDefinition = _boxedValuetypeHashtable.GetOrCreateValue(typeKey);

            // Get a method on the reference type with the same signature as the target method (but different
            // calling convention, since 'this' will be a reference type).
            var targetMethodDefinition = targetMethod.GetTypicalMethodDefinition();
            var methodKey = new UnboxingThunkHashtableKey(targetMethodDefinition, boxedTypeDefinition);
            UnboxingThunk thunkDefinition = _nonGenericUnboxingThunkHashtable.GetOrCreateValue(methodKey);

            // Find the thunk on the instantiated version of the reference type.
            MethodDesc thunk;
            if (owningType != owningTypeDefinition)
            {
                InstantiatedType boxedType = boxedTypeDefinition.MakeInstantiatedType(owningType.Instantiation);
                thunk = GetMethodForInstantiatedType(thunkDefinition, boxedType);
            }
            else
            {
                thunk = thunkDefinition;
            }
            if (thunk.HasInstantiation)
                thunk = thunk.MakeInstantiatedMethod(targetMethod.Instantiation);

            return thunk;
        }

        private struct BoxedValuetypeHashtableKey
        {
            public readonly MetadataType ValueType;
            public readonly ModuleDesc OwningModule;

            public BoxedValuetypeHashtableKey(MetadataType valueType, ModuleDesc owningModule)
            {
                ValueType = valueType;
                OwningModule = owningModule;
            }
        }

        private sealed class BoxedValuetypeHashtable : LockFreeReaderHashtable<BoxedValuetypeHashtableKey, BoxedValueType>
        {
            protected override int GetKeyHashCode(BoxedValuetypeHashtableKey key)
            {
                return key.ValueType.GetHashCode();
            }
            protected override int GetValueHashCode(BoxedValueType value)
            {
                return value.ValueTypeRepresented.GetHashCode();
            }
            protected override bool CompareKeyToValue(BoxedValuetypeHashtableKey key, BoxedValueType value)
            {
                return ReferenceEquals(key.ValueType, value.ValueTypeRepresented) &&
                    ReferenceEquals(key.OwningModule, value.Module);
            }
            protected override bool CompareValueToValue(BoxedValueType value1, BoxedValueType value2)
            {
                return ReferenceEquals(value1.ValueTypeRepresented, value2.ValueTypeRepresented) &&
                    ReferenceEquals(value1.Module, value2.Module);
            }
            protected override BoxedValueType CreateValueFromKey(BoxedValuetypeHashtableKey key)
            {
                return new BoxedValueType(key.OwningModule, key.ValueType);
            }
        }
        private BoxedValuetypeHashtable _boxedValuetypeHashtable = new BoxedValuetypeHashtable();

        private struct UnboxingThunkHashtableKey
        {
            public readonly MethodDesc TargetMethod;
            public readonly BoxedValueType OwningType;

            public UnboxingThunkHashtableKey(MethodDesc targetMethod, BoxedValueType owningType)
            {
                TargetMethod = targetMethod;
                OwningType = owningType;
            }
        }

        private sealed class UnboxingThunkHashtable : LockFreeReaderHashtable<UnboxingThunkHashtableKey, GenericUnboxingThunk>
        {
            protected override int GetKeyHashCode(UnboxingThunkHashtableKey key)
            {
                return key.TargetMethod.GetHashCode();
            }
            protected override int GetValueHashCode(GenericUnboxingThunk value)
            {
                return value.TargetMethod.GetHashCode();
            }
            protected override bool CompareKeyToValue(UnboxingThunkHashtableKey key, GenericUnboxingThunk value)
            {
                return ReferenceEquals(key.TargetMethod, value.TargetMethod) &&
                    ReferenceEquals(key.OwningType, value.OwningType);
            }
            protected override bool CompareValueToValue(GenericUnboxingThunk value1, GenericUnboxingThunk value2)
            {
                return ReferenceEquals(value1.TargetMethod, value2.TargetMethod) &&
                    ReferenceEquals(value1.OwningType, value2.OwningType);
            }
            protected override GenericUnboxingThunk CreateValueFromKey(UnboxingThunkHashtableKey key)
            {
                return new GenericUnboxingThunk(key.OwningType, key.TargetMethod);
            }
        }
        private UnboxingThunkHashtable _unboxingThunkHashtable = new UnboxingThunkHashtable();

        private sealed class NonGenericUnboxingThunkHashtable : LockFreeReaderHashtable<UnboxingThunkHashtableKey, UnboxingThunk>
        {
            protected override int GetKeyHashCode(UnboxingThunkHashtableKey key)
            {
                return key.TargetMethod.GetHashCode();
            }
            protected override int GetValueHashCode(UnboxingThunk value)
            {
                return value.TargetMethod.GetHashCode();
            }
            protected override bool CompareKeyToValue(UnboxingThunkHashtableKey key, UnboxingThunk value)
            {
                return ReferenceEquals(key.TargetMethod, value.TargetMethod) &&
                    ReferenceEquals(key.OwningType, value.OwningType);
            }
            protected override bool CompareValueToValue(UnboxingThunk value1, UnboxingThunk value2)
            {
                return ReferenceEquals(value1.TargetMethod, value2.TargetMethod) &&
                    ReferenceEquals(value1.OwningType, value2.OwningType);
            }
            protected override UnboxingThunk CreateValueFromKey(UnboxingThunkHashtableKey key)
            {
                return new UnboxingThunk(key.OwningType, key.TargetMethod);
            }
        }

        private NonGenericUnboxingThunkHashtable _nonGenericUnboxingThunkHashtable = new NonGenericUnboxingThunkHashtable();

        /// <summary>
        /// A type with an identical layout to the layout of a boxed value type.
        /// The type has a single field of the type of the valuetype it represents.
        /// </summary>
        private sealed partial class BoxedValueType : MetadataType, INonEmittableType
        {
            public MetadataType ValueTypeRepresented { get; }

            public override ModuleDesc Module { get; }

            public override ReadOnlySpan<byte> Name => "Boxed_"u8.Append(ValueTypeRepresented.Name);
            public override ReadOnlySpan<byte> Namespace => ValueTypeRepresented.Namespace;
            public override string DiagnosticName => "Boxed_" + ValueTypeRepresented.DiagnosticName;
            public override string DiagnosticNamespace => ValueTypeRepresented.DiagnosticNamespace;
            public override Instantiation Instantiation => ValueTypeRepresented.Instantiation;
            public override PInvokeStringFormat PInvokeStringFormat => PInvokeStringFormat.AutoClass;
            public override bool IsExplicitLayout => false;
            public override bool IsSequentialLayout => true;
            public override bool IsExtendedLayout => false;
            public override bool IsAutoLayout => false;
            public override bool IsBeforeFieldInit => false;
            public override MetadataType BaseType => (MetadataType)Context.GetWellKnownType(WellKnownType.Object);
            public override bool IsSealed => true;
            public override bool IsAbstract => false;
            public override MetadataType ContainingType => null;
            public override DefType[] ExplicitlyImplementedInterfaces => Array.Empty<DefType>();
            public override TypeSystemContext Context => ValueTypeRepresented.Context;

            public BoxedValueType(ModuleDesc owningModule, MetadataType valuetype)
            {
                // BoxedValueType has the same genericness as the valuetype it's wrapping.
                // Making BoxedValueType wrap the genericness (and be itself nongeneric) would
                // require a name mangling scheme to allow generating stable and unique names
                // for the wrappers.
                Debug.Assert(valuetype.IsTypeDefinition);

                Debug.Assert(valuetype.IsValueType);

                Module = owningModule;
                ValueTypeRepresented = valuetype;

                // Unboxing thunks for byref-like types don't make sense. Byref-like types cannot be boxed.
                // We still allow these to exist in the system, because it's easier than trying to prevent
                // their creation. We create them as if they existed (in lieu of e.g. pointing all of them
                // to the same __unreachable method body) so that the various places that store pointers to
                // them because they want to be able to extract the target instance method can use the same
                // mechanism they use for everything else at runtime.
            }

            public override ClassLayoutMetadata GetClassLayout() => default(ClassLayoutMetadata);
            public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false;
            public override IEnumerable<MetadataType> GetNestedTypes() => Array.Empty<MetadataType>();
            public override MetadataType GetNestedType(ReadOnlySpan<byte> name) => null;
            protected override MethodImplRecord[] ComputeVirtualMethodImplsForType() => Array.Empty<MethodImplRecord>();
            public override MethodImplRecord[] FindMethodsImplWithMatchingDeclName(ReadOnlySpan<byte> name) => Array.Empty<MethodImplRecord>();

            public override int GetHashCode() => VersionResilientHashCode.NameHashCode(Namespace, Name);

            protected override TypeFlags ComputeTypeFlags(TypeFlags mask)
            {
                TypeFlags flags = 0;

                if ((mask & TypeFlags.HasGenericVarianceComputed) != 0)
                {
                    flags |= TypeFlags.HasGenericVarianceComputed;
                }

                if ((mask & TypeFlags.CategoryMask) != 0)
                {
                    flags |= TypeFlags.Class;
                }

                flags |= TypeFlags.HasFinalizerComputed;
                flags |= TypeFlags.AttributeCacheComputed;

                return flags;
            }

            public override FieldDesc GetField(ReadOnlySpan<byte> name)
            {
                return null;
            }

            public override IEnumerable<FieldDesc> GetFields()
            {
                return Array.Empty<FieldDesc>();
            }
        }

        /// <summary>
        /// Does a method represent an unboxing stub
        /// </summary>
        public bool IsSpecialUnboxingThunk(MethodDesc method)
        {
            if (method.GetTypicalMethodDefinition().GetType() == typeof(GenericUnboxingThunk))
                return true;

            return false;
        }

        /// <summary>
        /// Convert from an unboxing stub to the actual target method
        /// </summary>
        public MethodDesc GetTargetOfSpecialUnboxingThunk(MethodDesc method)
        {
            MethodDesc typicalMethodTarget = ((GenericUnboxingThunk)method.GetTypicalMethodDefinition()).TargetMethod;

            MethodDesc methodOnInstantiatedType = typicalMethodTarget;
            if (method.OwningType.HasInstantiation)
            {
                InstantiatedType instantiatedType = GetInstantiatedType((MetadataType)typicalMethodTarget.OwningType, method.OwningType.Instantiation);
                methodOnInstantiatedType = GetMethodForInstantiatedType(typicalMethodTarget, instantiatedType);
            }

            MethodDesc instantiatedMethod = methodOnInstantiatedType;
            if (method.HasInstantiation)
            {
                instantiatedMethod = GetInstantiatedMethod(methodOnInstantiatedType, method.Instantiation);
            }

            return instantiatedMethod;
        }

        /// <summary>
        /// Represents a thunk to call shared instance method on boxed valuetypes.
        /// </summary>
        private sealed partial class GenericUnboxingThunk : ILStubMethod
        {
            private MethodDesc _targetMethod;
            private BoxedValueType _owningType;

            public GenericUnboxingThunk(BoxedValueType owningType, MethodDesc targetMethod)
            {
                Debug.Assert(targetMethod.OwningType.IsValueType);
                Debug.Assert(!targetMethod.Signature.IsStatic);

                _owningType = owningType;
                _targetMethod = targetMethod;
            }

            public override TypeSystemContext Context => _targetMethod.Context;

            public override TypeDesc OwningType => _owningType;

            public override MethodSignature Signature => _targetMethod.Signature;

            public MethodDesc TargetMethod => _targetMethod;

            public override ReadOnlySpan<byte> Name
            {
                get
                {
                    return _targetMethod.Name.Append("_Unbox"u8);
                }
            }

            public override string DiagnosticName
            {
                get
                {
                    return _targetMethod.DiagnosticName + "_Unbox";
                }
            }

            public override MethodIL EmitIL()
            {
                if (_owningType.ValueTypeRepresented.IsByRefLike)
                {
                    // If this is the fake unboxing thunk for ByRef-like types, just make a method that throws.
                    return new ILStubMethodIL(this,
                        new byte[] { (byte)ILOpcode.ldnull, (byte)ILOpcode.throw_ },
                        Array.Empty<LocalVariableDefinition>(),
                        Array.Empty<object>());
                }

                // Generate the unboxing stub. This loosely corresponds to following C#:
                // return BoxedValue.InstanceMethod(this.m_pEEType, [rest of parameters])

                ILEmitter emit = new ILEmitter();
                ILCodeStream codeStream = emit.NewCodeStream();

                FieldDesc eeTypeField = Context.GetWellKnownType(WellKnownType.Object).GetKnownField("m_pEEType"u8);

                // Load ByRef to the field with the value of the boxed valuetype
                codeStream.EmitLdArg(0);
                codeStream.Emit(ILOpcode.ldflda, emit.NewToken(Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "RawData"u8).GetField("Data"u8)));

                // Load the MethodTable of the boxed valuetype (this is the hidden generic context parameter expected
                // by the (canonical) instance method, but normally not part of the signature in IL).
                codeStream.EmitLdArg(0);
                codeStream.Emit(ILOpcode.ldfld, emit.NewToken(eeTypeField));

                codeStream.Emit(ILOpcode.call, emit.NewToken(Context.GetCoreLibEntryPoint("System.Runtime.CompilerServices"u8, "RuntimeHelpers"u8, "SetNextCallGenericContext"u8, null)));

                // Load rest of the arguments
                for (int i = 0; i < _targetMethod.Signature.Length; i++)
                {
                    codeStream.EmitLdArg(i + 1);
                }

                if (_targetMethod.IsAsyncCall())
                {
                    codeStream.Emit(ILOpcode.call, emit.NewToken(Context.GetCoreLibEntryPoint("System.Runtime.CompilerServices"u8, "AsyncHelpers"u8, "TailAwait"u8, null)));
                }

                codeStream.Emit(ILOpcode.call, emit.NewToken(_targetMethod.InstantiateAsOpen()));
                codeStream.Emit(ILOpcode.ret);

                return emit.Link(this);
            }
        }

        /// <summary>
        /// Represents a thunk to call instance method on boxed valuetypes.
        /// </summary>
        private sealed partial class UnboxingThunk : ILStubMethod
        {
            private MethodDesc _targetMethod;
            private BoxedValueType _owningType;

            public UnboxingThunk(BoxedValueType owningType, MethodDesc targetMethod)
            {
                Debug.Assert(targetMethod.OwningType.IsValueType);
                Debug.Assert(!targetMethod.Signature.IsStatic);

                _owningType = owningType;
                _targetMethod = targetMethod;
            }

            public override TypeSystemContext Context => _targetMethod.Context;

            public override TypeDesc OwningType => _owningType;

            public override MethodSignature Signature => _targetMethod.Signature;

            public MethodDesc TargetMethod => _targetMethod;

            public override ReadOnlySpan<byte> Name
            {
                get
                {
                    return _targetMethod.Name.Append("_Unbox"u8);
                }
            }

            public override string DiagnosticName
            {
                get
                {
                    return _targetMethod.DiagnosticName + "_Unbox";
                }
            }

            public override MethodIL EmitIL()
            {
                if (_owningType.ValueTypeRepresented.IsByRefLike)
                {
                    // If this is the fake unboxing thunk for ByRef-like types, just make a method that throws.
                    return new ILStubMethodIL(this,
                        new byte[] { (byte)ILOpcode.ldnull, (byte)ILOpcode.throw_ },
                        Array.Empty<LocalVariableDefinition>(),
                        Array.Empty<object>());
                }

                // TODO: mirror what was done in the commit that introduced this comment. Not doing it in that
                // commit since this can't be tested in dotnet/runtime repo main right now.
                if (_targetMethod.IsAsyncCall())
                {
                    ILEmitter e = new ILEmitter();
                    ILCodeStream c = e.NewCodeStream();

                    c.EmitCallThrowHelper(e, Context.GetCoreLibEntryPoint("System.Runtime"u8, "InternalCalls"u8, "RhpFallbackFailFast"u8, null));
                    return e.Link(this);
                }

                // Generate the unboxing stub. This loosely corresponds to following C#:
                // return BoxedValue.InstanceMethod([rest of parameters])

                ILEmitter emit = new ILEmitter();
                ILCodeStream codeStream = emit.NewCodeStream();

                // unbox to get a pointer to the value type
                codeStream.EmitLdArg(0);
                codeStream.Emit(ILOpcode.ldflda, emit.NewToken(Context.SystemModule.GetKnownType("System.Runtime.CompilerServices"u8, "RawData"u8).GetField("Data"u8)));

                // Load rest of the arguments
                for (int i = 0; i < _targetMethod.Signature.Length; i++)
                {
                    codeStream.EmitLdArg(i + 1);
                }

                TypeDesc owner = _targetMethod.OwningType;
                MethodDesc methodToInstantiate = _targetMethod;
                if (owner.HasInstantiation)
                {
                    MetadataType instantiatedOwner = (MetadataType)owner.InstantiateAsOpen();
                    methodToInstantiate = _targetMethod.Context.GetMethodForInstantiatedType(_targetMethod, (InstantiatedType)instantiatedOwner);
                }
                if (methodToInstantiate.HasInstantiation)
                {
                    TypeSystemContext context = methodToInstantiate.Context;

                    var inst = new TypeDesc[methodToInstantiate.Instantiation.Length];
                    for (int i = 0; i < inst.Length; i++)
                    {
                        inst[i] = context.GetSignatureVariable(i, true);
                    }

                    methodToInstantiate = context.GetInstantiatedMethod(methodToInstantiate, new Instantiation(inst));
                }

                codeStream.Emit(ILOpcode.call, emit.NewToken(methodToInstantiate));
                codeStream.Emit(ILOpcode.ret);

                return emit.Link(this);
            }

            public override Instantiation Instantiation => _targetMethod.Instantiation;
        }
    }
}