diff --git a/src/Common/src/TypeSystem/Common/TypeSystemHelpers.cs b/src/Common/src/TypeSystem/Common/TypeSystemHelpers.cs index 4e7a363c3ab..9f0fee83788 100644 --- a/src/Common/src/TypeSystem/Common/TypeSystemHelpers.cs +++ b/src/Common/src/TypeSystem/Common/TypeSystemHelpers.cs @@ -266,11 +266,16 @@ public static MethodDesc FindVirtualFunctionTargetMethodOnObjectType(this TypeDe } /// - /// Given Foo<T>, returns Foo<!0>. + /// Creates an open instantiation of a type. Given Foo<T>, returns Foo<!0>. + /// If the type is not generic, returns the . /// - private static InstantiatedType InstantiateAsOpen(this MetadataType type) + public static TypeDesc InstantiateAsOpen(this TypeDesc type) { - Debug.Assert(type.IsGenericDefinition); + if (!type.IsGenericDefinition) + { + Debug.Assert(!type.HasInstantiation); + return type; + } TypeSystemContext context = type.Context; @@ -280,7 +285,26 @@ private static InstantiatedType InstantiateAsOpen(this MetadataType type) inst[i] = context.GetSignatureVariable(i, false); } - return context.GetInstantiatedType(type, new Instantiation(inst)); + return context.GetInstantiatedType((MetadataType)type, new Instantiation(inst)); + } + + /// + /// Creates an open instantiation of a field. Given Foo<T>.Field, returns + /// Foo<!0>.Field. If the owning type is not generic, returns the . + /// + public static FieldDesc InstantiateAsOpen(this FieldDesc field) + { + Debug.Assert(field.GetTypicalFieldDefinition() == field); + + TypeDesc owner = field.OwningType; + + if (owner.HasInstantiation) + { + var instantiatedOwner = (InstantiatedType)owner.InstantiateAsOpen(); + return field.Context.GetFieldForInstantiatedType(field, instantiatedOwner); + } + + return field; } /// @@ -295,7 +319,7 @@ public static MethodDesc InstantiateAsOpen(this MethodDesc method) if (owner.HasInstantiation) { - MetadataType instantiatedOwner = ((MetadataType)owner).InstantiateAsOpen(); + MetadataType instantiatedOwner = (MetadataType)owner.InstantiateAsOpen(); return method.Context.GetMethodForInstantiatedType(method, (InstantiatedType)instantiatedOwner); } diff --git a/src/ILCompiler.Compiler/src/Compiler/CompilerTypeSystemContext.BoxedTypes.cs b/src/ILCompiler.Compiler/src/Compiler/CompilerTypeSystemContext.BoxedTypes.cs new file mode 100644 index 00000000000..2ab75555558 --- /dev/null +++ b/src/ILCompiler.Compiler/src/Compiler/CompilerTypeSystemContext.BoxedTypes.cs @@ -0,0 +1,471 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +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 (EEType) +// * 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 EEType 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) +// * Generating a call to a fake instance method on the valuetype that has +// the hidden (generic context) argument explicitly present in the +// signature. We need a fake method to be able to refer to the hidden parameter +// from IL. +// +// At a later stage (once codegen is done), we replace the references to the +// fake instance method with the real instance method. Their signatures after +// compilation is identical. +// + +namespace ILCompiler +{ + // Contains functionality related to pseudotypes representing boxed instances of value types + partial class CompilerTypeSystemContext + { + /// + /// 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. + /// + 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; + } + + /// + /// Returns true of is a standin method for unboxing thunk target. + /// + public bool IsSpecialUnboxingThunkTargetMethod(MethodDesc method) + { + return method.GetTypicalMethodDefinition().GetType() == typeof(ValueTypeInstanceMethodWithHiddenParameter); + } + + /// + /// Returns the real target method of an unboxing stub. + /// + public MethodDesc GetRealSpecialUnboxingThunkTargetMethod(MethodDesc method) + { + MethodDesc typicalMethod = method.GetTypicalMethodDefinition(); + MethodDesc methodDefinitionRepresented = ((ValueTypeInstanceMethodWithHiddenParameter)typicalMethod).MethodRepresented; + return GetMethodForInstantiatedType(methodDefinitionRepresented, (InstantiatedType)method.OwningType); + } + + private struct BoxedValuetypeHashtableKey + { + public readonly MetadataType ValueType; + public readonly ModuleDesc OwningModule; + + public BoxedValuetypeHashtableKey(MetadataType valueType, ModuleDesc owningModule) + { + ValueType = valueType; + OwningModule = owningModule; + } + } + + private class BoxedValuetypeHashtable : LockFreeReaderHashtable + { + 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 Object.ReferenceEquals(key.ValueType, value.ValueTypeRepresented) && + Object.ReferenceEquals(key.OwningModule, value.Module); + } + protected override bool CompareValueToValue(BoxedValueType value1, BoxedValueType value2) + { + return Object.ReferenceEquals(value1.ValueTypeRepresented, value2.ValueTypeRepresented) && + Object.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 class UnboxingThunkHashtable : LockFreeReaderHashtable + { + 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 Object.ReferenceEquals(key.TargetMethod, value.TargetMethod) && + Object.ReferenceEquals(key.OwningType, value.OwningType); + } + protected override bool CompareValueToValue(GenericUnboxingThunk value1, GenericUnboxingThunk value2) + { + return Object.ReferenceEquals(value1.TargetMethod, value2.TargetMethod) && + Object.ReferenceEquals(value1.OwningType, value2.OwningType); + } + protected override GenericUnboxingThunk CreateValueFromKey(UnboxingThunkHashtableKey key) + { + return new GenericUnboxingThunk(key.OwningType, key.TargetMethod); + } + } + private UnboxingThunkHashtable _unboxingThunkHashtable = new UnboxingThunkHashtable(); + + + /// + /// 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. + /// + private class BoxedValueType : MetadataType + { + private const string BoxedValueFieldName = "BoxedValue"; + + public FieldDesc BoxedValue { get; } + + public MetadataType ValueTypeRepresented { get; } + + public override ModuleDesc Module { get; } + + public override string Name => "Boxed_" + ValueTypeRepresented.Name; + + public override string Namespace + { + get + { + // Mangle the namespace in the hopes that it won't conflict with anything else. + + StringBuilder sb = new StringBuilder(); + + ArrayBuilder prefixes = new ArrayBuilder(); + + DefType currentType = ValueTypeRepresented; + if (currentType.ContainingType != null) + { + while (currentType.ContainingType != null) + { + prefixes.Add(currentType.Name); + currentType = currentType.ContainingType; + } + + prefixes.Add(currentType.Name); + } + + sb.Append(((IAssemblyDesc)((MetadataType)currentType).Module).GetName().Name); + sb.Append('_'); + sb.Append(currentType.Namespace); + + for (int i = prefixes.Count - 1; i >= 0; i--) + { + sb.Append(prefixes[i]); + + if (i > 0) + sb.Append('+'); + } + + return sb.ToString(); + } + } + + 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 IsBeforeFieldInit => false; + public override MetadataType MetadataBaseType => (MetadataType)Context.GetWellKnownType(WellKnownType.Object); + public override bool IsSealed => true; + public override DefType ContainingType => null; + public override DefType[] ExplicitlyImplementedInterfaces => Array.Empty(); + 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 crazy 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; + BoxedValue = new BoxedValueField(this); + } + + public override ClassLayoutMetadata GetClassLayout() => default(ClassLayoutMetadata); + public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false; + public override IEnumerable GetNestedTypes() => Array.Empty(); + public override MetadataType GetNestedType(string name) => null; + protected override MethodImplRecord[] ComputeVirtualMethodImplsForType() => Array.Empty(); + public override MethodImplRecord[] FindMethodsImplWithMatchingDeclName(string name) => Array.Empty(); + + public override int GetHashCode() + { + string ns = Namespace; + var hashCodeBuilder = new Internal.NativeFormat.TypeHashingAlgorithms.HashCodeBuilder(ns); + if (ns.Length > 0) + hashCodeBuilder.Append("."); + hashCodeBuilder.Append(Name); + return hashCodeBuilder.ToHashCode(); + } + + protected override TypeFlags ComputeTypeFlags(TypeFlags mask) + { + TypeFlags flags = 0; + + if ((mask & TypeFlags.ContainsGenericVariablesComputed) != 0) + { + flags |= TypeFlags.ContainsGenericVariablesComputed; + } + + if ((mask & TypeFlags.HasGenericVarianceComputed) != 0) + { + flags |= TypeFlags.HasGenericVarianceComputed; + } + + if ((mask & TypeFlags.CategoryMask) != 0) + { + flags |= TypeFlags.Class; + } + + return flags; + } + + public override FieldDesc GetField(string name) + { + if (name == BoxedValueFieldName) + return BoxedValue; + + return null; + } + + public override IEnumerable GetFields() + { + yield return BoxedValue; + } + + /// + /// Synthetic field on . + /// + private class BoxedValueField : FieldDesc + { + private BoxedValueType _owningType; + + public override TypeSystemContext Context => _owningType.Context; + public override TypeDesc FieldType => _owningType.ValueTypeRepresented.InstantiateAsOpen(); + public override bool HasRva => false; + public override bool IsInitOnly => false; + public override bool IsLiteral => false; + public override bool IsStatic => false; + public override bool IsThreadStatic => false; + public override DefType OwningType => _owningType; + public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false; + public override string Name => BoxedValueFieldName; + + public BoxedValueField(BoxedValueType owningType) + { + _owningType = owningType; + } + } + } + + /// + /// Represents a thunk to call shared instance method on boxed valuetypes. + /// + private class GenericUnboxingThunk : ILStubMethod + { + private MethodDesc _targetMethod; + private ValueTypeInstanceMethodWithHiddenParameter _nakedTargetMethod; + private BoxedValueType _owningType; + + public GenericUnboxingThunk(BoxedValueType owningType, MethodDesc targetMethod) + { + Debug.Assert(targetMethod.OwningType.IsValueType); + Debug.Assert(!targetMethod.Signature.IsStatic); + + _owningType = owningType; + _targetMethod = targetMethod; + _nakedTargetMethod = new ValueTypeInstanceMethodWithHiddenParameter(targetMethod); + } + + public override TypeSystemContext Context => _targetMethod.Context; + + public override TypeDesc OwningType => _owningType; + + public override MethodSignature Signature => _targetMethod.Signature; + + public MethodDesc TargetMethod => _targetMethod; + + public override string Name + { + get + { + return _targetMethod.Name + "_Unbox"; + } + } + + public override MethodIL EmitIL() + { + // 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"); + FieldDesc boxedValueField = _owningType.BoxedValue.InstantiateAsOpen(); + + // Load ByRef to the field with the value of the boxed valuetype + codeStream.EmitLdArg(0); + codeStream.Emit(ILOpcode.ldflda, emit.NewToken(boxedValueField)); + + // Load the EEType 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)); + + // Load rest of the arguments + for (int i = 0; i < _targetMethod.Signature.Length; i++) + { + codeStream.EmitLdArg(i + 1); + } + + // Call an instance method on the target valuetype that has a fake instantiation parameter + // in it's signature. This will be swapped by the actual instance method after codegen is done. + codeStream.Emit(ILOpcode.call, emit.NewToken(_nakedTargetMethod.InstantiateAsOpen())); + codeStream.Emit(ILOpcode.ret); + + return emit.Link(this); + } + } + + /// + /// Represents an instance method on a generic valuetype with an explicit instantiation parameter in the + /// signature. This is so that we can refer to the parameter from IL. References to this method will + /// be replaced by the actual instance method after codegen is done. + /// + internal class ValueTypeInstanceMethodWithHiddenParameter : MethodDesc + { + private MethodDesc _methodRepresented; + private MethodSignature _signature; + + public ValueTypeInstanceMethodWithHiddenParameter(MethodDesc methodRepresented) + { + Debug.Assert(methodRepresented.OwningType.IsValueType); + Debug.Assert(!methodRepresented.Signature.IsStatic); + + _methodRepresented = methodRepresented; + } + + public MethodDesc MethodRepresented => _methodRepresented; + + // We really don't want this method to be inlined. + public override bool IsNoInlining => true; + + public override TypeSystemContext Context => _methodRepresented.Context; + public override TypeDesc OwningType => _methodRepresented.OwningType; + + public override string Name => _methodRepresented.Name; + + public override MethodSignature Signature + { + get + { + if (_signature == null) + { + TypeDesc[] parameters = new TypeDesc[_methodRepresented.Signature.Length + 1]; + + // Shared instance methods on generic valuetypes have a hidden parameter with the generic context. + // We add it to the signature so that we can refer to it from IL. + parameters[0] = Context.GetWellKnownType(WellKnownType.Object).GetKnownField("m_pEEType").FieldType; + for (int i = 0; i < _methodRepresented.Signature.Length; i++) + parameters[i + 1] = _methodRepresented.Signature[i]; + + _signature = new MethodSignature(_methodRepresented.Signature.Flags, + _methodRepresented.Signature.GenericParameterCount, + _methodRepresented.Signature.ReturnType, + parameters); + } + + return _signature; + } + } + + public override bool HasCustomAttribute(string attributeNamespace, string attributeName) => false; + } + } +} diff --git a/src/ILCompiler.Compiler/src/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs b/src/ILCompiler.Compiler/src/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs index cb2d749621e..a768f85cd9e 100644 --- a/src/ILCompiler.Compiler/src/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs +++ b/src/ILCompiler.Compiler/src/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs @@ -34,7 +34,14 @@ protected override IMethodNode CreateMethodEntrypointNode(MethodDesc method) if (CompilationModuleGroup.ContainsMethod(method)) { - return new MethodCodeNode(method); + if (TypeSystemContext.IsSpecialUnboxingThunkTargetMethod(method)) + { + return MethodEntrypoint(TypeSystemContext.GetRealSpecialUnboxingThunkTargetMethod(method)); + } + else + { + return new MethodCodeNode(method); + } } else { @@ -44,7 +51,21 @@ protected override IMethodNode CreateMethodEntrypointNode(MethodDesc method) protected override IMethodNode CreateUnboxingStubNode(MethodDesc method) { - return new UnboxingStubNode(method); + Debug.Assert(!method.Signature.IsStatic); + + if (method.IsCanonicalMethod(CanonicalFormKind.Specific) && !method.HasInstantiation) + { + // Unboxing stubs to canonical instance methods need a special unboxing stub that unboxes + // 'this' and also provides an instantiation argument (we do a calling convention conversion). + // We don't do this for generic instance methods though because they don't use the EEType + // for the generic context anyway. + return new MethodCodeNode(TypeSystemContext.GetSpecialUnboxingThunk(method, CompilationModuleGroup.GeneratedAssembly)); + } + else + { + // Otherwise we just unbox 'this' and don't touch anything else. + return new UnboxingStubNode(method); + } } protected override ISymbolNode CreateReadyToRunHelperNode(Tuple helperCall) diff --git a/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj b/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj index fdec5550f61..ad2df96c04d 100644 --- a/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj +++ b/src/ILCompiler.Compiler/src/ILCompiler.Compiler.csproj @@ -90,6 +90,7 @@ JitInterface\JitConfigProvider.cs + diff --git a/src/JitInterface/src/CorInfoImpl.cs b/src/JitInterface/src/CorInfoImpl.cs index 72fb3a0d26b..5e2a531465a 100644 --- a/src/JitInterface/src/CorInfoImpl.cs +++ b/src/JitInterface/src/CorInfoImpl.cs @@ -499,7 +499,7 @@ private void Get_CORINFO_SIG_INFO(MethodDesc method, out CORINFO_SIG_INFO sig, b Get_CORINFO_SIG_INFO(method.Signature, out sig); // Does the method have a hidden parameter? - if (method.RequiresInstArg() && !isFatFunctionPointer) + if (method.RequiresInstArg() && !isFatFunctionPointer && !_compilation.TypeSystemContext.IsSpecialUnboxingThunkTargetMethod(method)) { sig.callConv |= CorInfoCallConv.CORINFO_CALLCONV_PARAMTYPE; } diff --git a/tests/src/Simple/Generics/Generics.cs b/tests/src/Simple/Generics/Generics.cs index 603d5b792e3..55d6fb6a333 100644 --- a/tests/src/Simple/Generics/Generics.cs +++ b/tests/src/Simple/Generics/Generics.cs @@ -19,6 +19,7 @@ static int Main() TestDelegateInterfaceMethod.Run(); TestThreadStaticFieldAccess.Run(); TestConstrainedMethodCalls.Run(); + TestInstantiatingUnboxingStubs.Run(); TestNameManglingCollisionRegression.Run(); TestUnusedGVMsDoNotCrashCompiler.Run(); @@ -420,6 +421,52 @@ public static void Run() } } + class TestInstantiatingUnboxingStubs + { + static volatile IFoo s_foo; + + interface IFoo + { + bool IsInst(object o); + + void Set(int value); + } + + struct Foo : IFoo + { + public int Value; + + public bool IsInst(object o) + { + return o is T; + } + + public void Set(int value) + { + Value = value; + } + } + + public static void Run() + { + s_foo = new Foo(); + + // Make sure the instantiation argument is properly passed + if (!s_foo.IsInst("ab")) + throw new Exception(); + + if (s_foo.IsInst(new object())) + throw new Exception(); + + // Make sure the byref to 'this' is properly passed + s_foo.Set(42); + + var foo = (Foo)s_foo; + if (foo.Value != 42) + throw new Exception(); + } + } + // // Regression test for issue https://github.com/dotnet/corert/issues/1964 //