From 04300d9ce2eb30a0c45aea968aa22d81848d2243 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Tue, 18 Aug 2026 16:54:16 -0700 Subject: [PATCH 1/6] Add a new test to cover the situation where a GVM moved to a base type after C# build time but before R2R build time --- src/tests/readytorun/tests/main.cs | 42 ++++++ src/tests/readytorun/tests/mainv3.csproj | 180 +++++++++++++++++++++++ src/tests/readytorun/tests/test.cs | 59 +++++--- 3 files changed, 264 insertions(+), 17 deletions(-) create mode 100644 src/tests/readytorun/tests/mainv3.csproj diff --git a/src/tests/readytorun/tests/main.cs b/src/tests/readytorun/tests/main.cs index 75a4adf951a92d..f0a98c93b8e1f1 100644 --- a/src/tests/readytorun/tests/main.cs +++ b/src/tests/readytorun/tests/main.cs @@ -229,6 +229,48 @@ static void TestMovedGenericVirtualMethod() } Assert.AreEqual("NullReferenceException", "thrown"); + + TestMovedGenericVirtualMethodSharedGenericCaller(); + } + + [MethodImplAttribute(MethodImplOptions.NoInlining)] + static void TestMovedGenericVirtualMethodSharedGeneric() + { + var o = new MyChildGeneric(); + + Assert.AreEqual(o.MovedToBaseClass(), typeof(List).ToString()); + Assert.AreEqual(o.ChangedToVirtual(), typeof(List).ToString()); + + // Test that changing a virtual to a non-virtual doesn't cause a crash. (Behavior is somewhat undefined, as this change is explicitly defined as a breaking change.) + Assert.AreEqual(GetChangedToNonVirtualDelegate(o)(), typeof(List).ToString()); + + o = null; + + try + { + o.MovedToBaseClass(); + } + catch (NullReferenceException) + { + try + { + o.ChangedToVirtual(); + } + catch (NullReferenceException) + { + return; + } + } + + Assert.AreEqual("NullReferenceException", "thrown"); + + var o2 = new MyChildClass(); + Assert.AreEqual(o2.MovedToBaseClassGeneric(), "MovedToBaseClassGeneric"); + } + + static void TestMovedGenericVirtualMethodSharedGenericCaller() + { + TestMovedGenericVirtualMethodSharedGeneric(); } [MethodImplAttribute(MethodImplOptions.NoInlining)] diff --git a/src/tests/readytorun/tests/mainv3.csproj b/src/tests/readytorun/tests/mainv3.csproj new file mode 100644 index 00000000000000..478a93d935de7c --- /dev/null +++ b/src/tests/readytorun/tests/mainv3.csproj @@ -0,0 +1,180 @@ + + + + + true + false + false + + true + + true + true + + + + + + + + + + + + + + + + + + + + + test.dll + $(OutputPath)/../testv1/test/ + $(OutputPath)/testv1 + $(OutputPath)/../testv2/test/ + $(OutputPath)/testv2 + + + + + + + diff --git a/src/tests/readytorun/tests/test.cs b/src/tests/readytorun/tests/test.cs index b6b74a0dff2cff..6f1e2159cb5a19 100644 --- a/src/tests/readytorun/tests/test.cs +++ b/src/tests/readytorun/tests/test.cs @@ -156,13 +156,6 @@ static public void TestInterop() NativeMethod(); } -#if V2 - public string MovedToBaseClass() - { - return "MovedToBaseClass"; - } -#endif - #if V2 public virtual string ChangedToVirtual() { @@ -195,7 +188,26 @@ public static void ThrowIOE() } } -public class MyChildClass : MyClass +#if V2 +public class MyIntermediateClass : MyClass +{ + public string MovedToBaseClass() + { + return "MovedToBaseClass"; + } + public string MovedToBaseClassGeneric() + { + return "MovedToBaseClassGeneric"; + } +} +#endif + +public class MyChildClass +#if V2 + : MyIntermediateClass +#else + : MyClass +#endif { public MyChildClass() { @@ -206,6 +218,10 @@ public string MovedToBaseClass() { return "MovedToBaseClass"; } + public string MovedToBaseClassGeneric() + { + return "MovedToBaseClassGeneric"; + } #endif #if V2 @@ -271,14 +287,6 @@ public virtual string GenericVirtualMethod() return typeof(T).ToString() + typeof(U).ToString() + typeof(V).ToString() + typeof(W).ToString(); } -#if V2 - public string MovedToBaseClass() - { - typeof(Dictionary).ToString(); - return typeof(List).ToString(); - } -#endif - #if V2 public virtual string ChangedToVirtual() { @@ -310,7 +318,24 @@ public string NonVirtualMethod() } } -public class MyChildGeneric : MyGeneric +#if V2 +public class MyIntermediateGeneric : MyGeneric +{ + public string MovedToBaseClass() + { + typeof(Dictionary).ToString(); + return typeof(List).ToString(); + } +} +#endif + + +public class MyChildGeneric +#if V2 + : MyIntermediateGeneric +#else + : MyGeneric +#endif { public MyChildGeneric() { From c289b2204c86d35695b6cc238b6a087406d80f24 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Wed, 19 Aug 2026 10:42:40 -0700 Subject: [PATCH 2/6] Add R2R fixup for referencing a type via a method it declares Crossgen2 encodes a type into a signature by finding a token for its typedef in some module visible to the compilation. That is not always possible. If a method is moved from a derived class to a base class between the C# compilation and the R2R compilation, and the base class is not visible to the compiling module, the callsite still has a memberref naming the derived class, but the declaring type has no token anywhere. Crossgen2 threw NotImplementedException out of ModuleTokenResolver.GetModuleTokenForType. Fix this by referencing such a type indirectly, through a method that it declares. READYTORUN_FIXUP_DeclaringTypeHandle is repurposed to be followed by a method signature and to resolve to the type declaring that method. The method is still named by its original memberref/methodspec, which is always encodable, so the unencodable declaring type never has to appear in the signature. The runtime resolves the member and returns its declaring type, using GetMethodTableMatchingParentClass to recover the exact instantiated ancestor when the member resolves to a canonical MethodDesc. Crossgen2 detects the situation with CanEncodeTypeInSignature, which mirrors the tokenization requirements of SignatureBuilder. EmitTypeSignature, and downgrades ReadyToRunHelperId.TypeHandle to ReadyToRunHelperId.DeclaringTypeHandle. Both the shared generic dictionary lookup path and the ordinary import cell path need this. The fixup previously had a different encoding - a pair of type signatures - that was only ever emitted by crossgen1. Crossgen2 has never emitted it. MINIMUM_READYTORUN_MAJOR_VERSION is 26 and crossgen1 was removed in R2R 5.4, so the runtime cannot encounter the old form. The R2R dumper can still be pointed at such images, so its parsing is gated on the R2R version. The old path there also parsed only the first of the two type signatures, which desynchronized the rest of the signature; it now parses both. Bump the R2R minor version to 26.2. mainv3 does not enable cross module inlining, so exclude ILInliningVersioningTest from it rather than have it assert that cross module inlined methods are present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cfb63f2-70d5-4290-b546-b37cbe339696 --- docs/design/coreclr/botr/readytorun-format.md | 2 +- src/coreclr/inc/readytorun.h | 5 +- .../nativeaot/Runtime/inc/ModuleHeaders.h | 2 +- .../Common/Internal/Runtime/ModuleHeaders.cs | 2 +- .../Internal/Runtime/ReadyToRunConstants.cs | 5 +- .../ReadyToRun/GenericLookupSignature.cs | 5 ++ .../ReadyToRunSymbolNodeFactory.cs | 20 +++++ .../JitInterface/CorInfoImpl.ReadyToRun.cs | 82 ++++++++++++++++++- .../ReadyToRunReader.cs | 14 ++++ .../ReadyToRunSignature.cs | 13 ++- src/coreclr/vm/genericdict.cpp | 33 +++++++- src/coreclr/vm/genericdict.h | 3 + src/coreclr/vm/jitinterface.cpp | 27 ++++++ src/tests/readytorun/tests/main.cs | 4 + src/tests/readytorun/tests/mainv3.csproj | 16 ++-- 15 files changed, 214 insertions(+), 19 deletions(-) diff --git a/docs/design/coreclr/botr/readytorun-format.md b/docs/design/coreclr/botr/readytorun-format.md index 6e4184afa65343..fb944655f044c3 100644 --- a/docs/design/coreclr/botr/readytorun-format.md +++ b/docs/design/coreclr/botr/readytorun-format.md @@ -276,7 +276,7 @@ fixup kind, the rest of the signature varies based on the fixup kind. | READYTORUN_FIXUP_Check_TypeLayout | 0x2A | Verification of type layout, followed by typespec and expected type layout descriptor | READYTORUN_FIXUP_Check_FieldOffset | 0x2B | Verification of field offset, followed by field signature and expected field layout descriptor | READYTORUN_FIXUP_DelegateCtor | 0x2C | Delegate constructor, followed by method signature -| READYTORUN_FIXUP_DeclaringTypeHandle | 0x2D | Dictionary lookup for method declaring type. Followed by the type signature. +| READYTORUN_FIXUP_DeclaringTypeHandle | 0x2D | Type which declares the method described by the signature. Followed by the method signature. | READYTORUN_FIXUP_IndirectPInvokeTarget | 0x2E | Target (indirect) of an inlined PInvoke. Followed by method signature. | READYTORUN_FIXUP_PInvokeTarget | 0x2F | Target of an inlined PInvoke. Followed by method signature. | READYTORUN_FIXUP_Check_InstructionSetSupport | 0x30 | Specify the instruction sets that must be supported/unsupported to use the R2R code associated with the fixup. diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index 993aec8e2f286a..b0d895f2174c67 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -20,7 +20,7 @@ // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. #define READYTORUN_MAJOR_VERSION 26 -#define READYTORUN_MINOR_VERSION 0x0001 +#define READYTORUN_MINOR_VERSION 0x0002 #define MINIMUM_READYTORUN_MAJOR_VERSION 26 @@ -67,6 +67,7 @@ // R2R Version 25 renames runtime async infrastructure members, makes thunk-used members NonVersionable, and frees up a flag in CorInfoContinuationFlags // R2R Version 26 changes ARM64 NativeVarInfo register encoding to include V0-V31 // R2R Version 26.1 adds READYTORUN_FIXUP_StoreMultiCallableAddrOfCode for storing a method's MultiCallableAddrOfCode into a location in the R2R image (used on WebAssembly) +// R2R Version 26.2 redefines READYTORUN_FIXUP_DeclaringTypeHandle to be encoded as a method signature instead of a pair of type signatures struct READYTORUN_CORE_HEADER { @@ -303,7 +304,7 @@ enum ReadyToRunFixupKind READYTORUN_FIXUP_Check_FieldOffset = 0x2B, READYTORUN_FIXUP_DelegateCtor = 0x2C, /* optimized delegate ctor */ - READYTORUN_FIXUP_DeclaringTypeHandle = 0x2D, + READYTORUN_FIXUP_DeclaringTypeHandle = 0x2D, /* Type which declares the method described by the (method) signature */ READYTORUN_FIXUP_IndirectPInvokeTarget = 0x2E, /* Target (indirect) of an inlined pinvoke */ READYTORUN_FIXUP_PInvokeTarget = 0x2F, /* Target of an inlined pinvoke */ diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index edcc2d76e58aa4..22185114bb6dd5 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -12,7 +12,7 @@ struct ReadyToRunHeaderConstants static const uint32_t Signature = 0x00525452; // 'RTR' static const uint32_t CurrentMajorVersion = 26; - static const uint32_t CurrentMinorVersion = 1; + static const uint32_t CurrentMinorVersion = 2; }; struct ReadyToRunHeader diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index 489b16ff50e517..e4e1d24529a336 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -16,7 +16,7 @@ internal struct ReadyToRunHeaderConstants public const uint Signature = 0x00525452; // 'RTR' public const ushort CurrentMajorVersion = 26; - public const ushort CurrentMinorVersion = 1; + public const ushort CurrentMinorVersion = 2; } #if READYTORUN #pragma warning disable 0169 diff --git a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs index 31fdfa6218e5bd..dac7b8fe8bd2c6 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs @@ -124,6 +124,9 @@ public enum DictionaryEntryKind DispatchStubAddrSlot = 5, FieldDescSlot = 6, DeclaringTypeHandleSlot = 7, + // Only used by ReadyToRun signatures (ReadyToRunFixupKind.DeclaringTypeHandle). The signature encodes a + // method and the slot is populated with the type which declares that method. + DeclaringTypeHandleFromMethodSlot = 8, } public enum ReadyToRunFixupKind @@ -174,7 +177,7 @@ public enum ReadyToRunFixupKind Check_FieldOffset = 0x2B, DelegateCtor = 0x2C, // optimized delegate ctor - DeclaringTypeHandle = 0x2D, + DeclaringTypeHandle = 0x2D, // Type which declares the method described by the (method) signature IndirectPInvokeTarget = 0x2E, // Target (indirect) of an inlined pinvoke PInvokeTarget = 0x2F, // Target of an inlined pinvoke diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs index 94623a51708466..0e5f3358eccff7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs @@ -145,6 +145,11 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact if (_typeArgument != null) factory.AddVirtualMethodDiscoveryDependencies(ref dependencies, _typeArgument); } + else if (_fixupKind == ReadyToRunFixupKind.DeclaringTypeHandle) + { + // This fixup is a type handle lookup as well - the type is the one declaring the method. + factory.AddVirtualMethodDiscoveryDependencies(ref dependencies, _methodArgument.Method.OwningType); + } return dependencies; } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunSymbolNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunSymbolNodeFactory.cs index 379becc9189fbf..b68f4a28ef34bc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunSymbolNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunSymbolNodeFactory.cs @@ -276,6 +276,9 @@ private Import CreateReadyToRunHelper(ReadyToRunHelperKey key) case ReadyToRunHelperId.TypeHandle: return CreateTypeHandleHelper((TypeDesc)key.Target); + case ReadyToRunHelperId.DeclaringTypeHandle: + return CreateDeclaringTypeHandleHelper((MethodWithToken)key.Target); + case ReadyToRunHelperId.MethodHandle: return CreateMethodHandleHelper((MethodWithToken)key.Target); @@ -389,6 +392,16 @@ private Import CreateTypeHandleHelper(TypeDesc type) _codegenNodeFactory.TypeSignature(ReadyToRunFixupKind.TypeHandle, type)); } + private Import CreateDeclaringTypeHandleHelper(MethodWithToken method) + { + return new PrecodeHelperImport( + _codegenNodeFactory, + _codegenNodeFactory.MethodSignature( + ReadyToRunFixupKind.DeclaringTypeHandle, + method, + isInstantiatingStub: false)); + } + private Import CreateMethodHandleHelper(MethodWithToken method) { bool useInstantiatingStub = method.Method.GetCanonMethodTarget(CanonicalFormKind.Specific) != method.Method; @@ -619,6 +632,13 @@ public Import GenericLookupHelper( helperArgument, methodContext); + case ReadyToRunHelperId.DeclaringTypeHandle: + return GenericLookupMethodHelper( + runtimeLookupKind, + ReadyToRunFixupKind.DeclaringTypeHandle, + (MethodWithToken)helperArgument, + methodContext); + case ReadyToRunHelperId.MethodHandle: return GenericLookupMethodHelper( runtimeLookupKind, diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 4ec656fa67a58f..c41c72a4c7b581 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2680,6 +2680,57 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO } } + /// + /// Determine whether a type can be encoded as a type signature in the compiled image. Encoding a named type + /// requires a metadata token for it which is resolvable from within the version bubble being compiled. + /// This must be kept in sync with . + /// + private bool CanEncodeTypeInSignature(TypeDesc type) + { + switch (type) + { + case RuntimeDeterminedType: + return true; + + case ParameterizedType parameterizedType: + return CanEncodeTypeInSignature(parameterizedType.ParameterType); + + case FunctionPointerType functionPointerType: + MethodSignature signature = functionPointerType.Signature; + if (!CanEncodeTypeInSignature(signature.ReturnType)) + return false; + for (int argIndex = 0; argIndex < signature.Length; argIndex++) + { + if (!CanEncodeTypeInSignature(signature[argIndex])) + return false; + } + return true; + } + + if (type.HasInstantiation && !type.IsGenericDefinition) + { + foreach (TypeDesc instantiationArgument in type.Instantiation) + { + if (!CanEncodeTypeInSignature(instantiationArgument)) + return false; + } + } + + TypeDesc typeDefinition = type.GetTypeDefinition(); + if (typeDefinition.IsPrimitive || typeDefinition.IsVoid || typeDefinition.IsString || typeDefinition.IsObject + || typeDefinition.IsWellKnownType(WellKnownType.TypedReference) + || typeDefinition.IsCanonicalDefinitionType(CanonicalFormKind.Specific)) + { + return true; + } + + if (typeDefinition is not EcmaType ecmaTypeDefinition) + return true; + + return !_compilation.NodeFactory.Resolver.GetModuleTokenForType( + ecmaTypeDefinition, allowDynamicallyCreatedReference: true, throwIfNotFound: false).IsNull; + } + private void ComputeRuntimeLookupForSharedGenericToken( DictionaryEntryKind entryKind, ref CORINFO_RESOLVED_TOKEN pResolvedToken, @@ -2778,6 +2829,15 @@ private void ComputeRuntimeLookupForSharedGenericToken( Debug.Assert(MethodBeingCompiled is not AsyncResumptionStub); _compilation.NodeFactory.DetectGenericCycles(MethodBeingCompiled, sharedMethod); helperArg = new MethodWithToken(methodDesc, HandleToModuleToken(ref pResolvedToken, out bool strippedInstantiation), constrainedType, unboxing: false, genericContextObject: sharedMethod, forceOwningTypeFromMethodDesc: strippedInstantiation); + + if (helperId == ReadyToRunHelperId.TypeHandle && !CanEncodeTypeInSignature(methodDesc.OwningType)) + { + // The type handle being looked up is the type which declares the method described by the token. That + // type isn't necessarily referenceable from the modules being compiled - the method may have moved to + // a base type which is invisible to the version bubble of the token. Encode the method instead and let + // the runtime compute the type which declares it. + helperId = ReadyToRunHelperId.DeclaringTypeHandle; + } } else if (helperArg is FieldDesc fieldDesc) { @@ -2941,9 +3001,25 @@ private void embedGenericHandle(ref CORINFO_RESOLVED_TOKEN pResolvedToken, bool switch (pResult.handleType) { case CorInfoGenericHandleType.CORINFO_HANDLETYPE_CLASS: - symbolNode = _compilation.SymbolNodeFactory.CreateReadyToRunHelper( - ReadyToRunHelperId.TypeHandle, - HandleToObject(pResolvedToken.hClass)); + { + TypeDesc typeHandleType = HandleToObject(pResolvedToken.hClass); + if (fEmbedParent && pResolvedToken.hMethod != null && !CanEncodeTypeInSignature(typeHandleType)) + { + // The type handle being embedded is the type which declares the method described by the + // token. That type isn't necessarily referenceable from the modules being compiled - the + // method may have moved to a base type which is invisible to the version bubble of the + // token. Encode the method instead and let the runtime compute the declaring type. + symbolNode = _compilation.SymbolNodeFactory.CreateReadyToRunHelper( + ReadyToRunHelperId.DeclaringTypeHandle, + ComputeMethodWithToken(HandleToObject(pResolvedToken.hMethod), ref pResolvedToken, constrainedType: null, unboxing: false)); + } + else + { + symbolNode = _compilation.SymbolNodeFactory.CreateReadyToRunHelper( + ReadyToRunHelperId.TypeHandle, + typeHandleType); + } + } break; case CorInfoGenericHandleType.CORINFO_HANDLETYPE_METHOD: diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index 9ade1797a4088f..58757cb3753554 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -242,6 +242,20 @@ public bool ComponentAssemblyIndicesStartAtTwo public int ComponentAssemblyIndexOffset => (ComponentAssemblyIndicesStartAtTwo ? 2 : 1); + /// + /// Starting with R2R version 26.2, READYTORUN_FIXUP_DeclaringTypeHandle is followed by a method + /// signature and resolves to the type declaring that method. In earlier versions (emitted only by + /// crossgen1) it was followed by the declaring type signature and then the type from the token. + /// + public bool DeclaringTypeHandleFixupUsesMethodSignature + { + get + { + EnsureHeader(); + return _readyToRunHeader.MajorVersion > 26 || (_readyToRunHeader.MajorVersion == 26 && _readyToRunHeader.MinorVersion >= 2); + } + } + /// /// The preferred address of the first byte of image when loaded into memory; /// must be a multiple of 64K. diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs index 3965936cb063c7..cd91b099cbe0ca 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs @@ -1456,7 +1456,18 @@ private ReadyToRunSignature ParseSignature(ReadyToRunFixupKind fixupType, String break; case ReadyToRunFixupKind.DeclaringTypeHandle: - ParseType(builder); + if (_contextReader.DeclaringTypeHandleFixupUsesMethodSignature) + { + ParseMethod(builder); + } + else + { + // Images predating R2R version 26.2 encode the declaring type followed by the type + // named by the token. + ParseType(builder); + builder.Append(" of "); + ParseType(builder); + } builder.Append(" (DECLARING_TYPE_HANDLE)"); break; diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index 78df58b1c57ce8..38299d9212ff9e 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -728,7 +728,7 @@ Dictionary::PopulateEntry( switch (signatureKind) { - case READYTORUN_FIXUP_DeclaringTypeHandle: kind = DeclaringTypeHandleSlot; break; + case READYTORUN_FIXUP_DeclaringTypeHandle: kind = DeclaringTypeHandleFromMethodSlot; break; case READYTORUN_FIXUP_TypeHandle: kind = TypeHandleSlot; break; case READYTORUN_FIXUP_FieldHandle: kind = FieldDescSlot; break; case READYTORUN_FIXUP_MethodHandle: kind = MethodDescSlot; break; @@ -864,6 +864,7 @@ Dictionary::PopulateEntry( case MethodDescSlot: case DispatchStubAddrSlot: case MethodEntrySlot: + case DeclaringTypeHandleFromMethodSlot: { TypeHandle ownerType; MethodTable * pOwnerMT = NULL; @@ -878,6 +879,9 @@ Dictionary::PopulateEntry( BOOL fRequiresDispatchStub = 0; BOOL isAsyncVariant = 0; + // 'kind' can be reassigned below when the signature carries a constrained token, so capture this up front. + BOOL fDeclaringTypeHandleFromMethod = (kind == DeclaringTypeHandleFromMethodSlot); + if (isReadyToRunModule) { IfFailThrow(ptr.GetData(&methodFlags)); @@ -926,7 +930,10 @@ Dictionary::PopulateEntry( { FieldDesc * pFDDummy = NULL; - MemberLoader::GetDescFromMemberRef(pZapSigContext->pInfoModule, TokenFromRid(rid, mdtMemberRef), &pMethod, &pFDDummy, NULL, FALSE, &ownerType); + // For DeclaringTypeHandleFromMethodSlot the type referenced by the token is needed to compute + // the exact instantiation of the type which declares the method, so ask for the actual type. + MemberLoader::GetDescFromMemberRef(pZapSigContext->pInfoModule, TokenFromRid(rid, mdtMemberRef), &pMethod, &pFDDummy, NULL, FALSE, &ownerType, + fDeclaringTypeHandleFromMethod /* actualTypeRequired */); _ASSERTE(pMethod != NULL && pFDDummy == NULL); } else @@ -1025,6 +1032,28 @@ Dictionary::PopulateEntry( } } + if (fDeclaringTypeHandleFromMethod) + { + _ASSERTE(isReadyToRunModule); + + // The signature describes a method; the value of the slot is the type which declares that method. + // The method may be declared on a base type of the type referenced by the token, and the MethodDesc + // found for it may belong to a canonical instantiation of that base type, so walk the parent chain of + // the (exact) type from the token to recover the exact declaring type. + MethodTable * pDeclaringMT = pMethod->GetMethodTable(); + if (pOwnerMT != NULL) + { + MethodTable * pExactDeclaringMT = pOwnerMT->GetMethodTableMatchingParentClass(pDeclaringMT); + if (pExactDeclaringMT != NULL) + pDeclaringMT = pExactDeclaringMT; + } + + pDeclaringMT->EnsureInstanceActive(); + + result = (CORINFO_GENERIC_HANDLE)TypeHandle(pDeclaringMT).AsPtr(); + break; + } + if (fRequiresDispatchStub) { LoaderAllocator * pDictLoaderAllocator = (pMT != NULL) ? pMT->GetLoaderAllocator() : pMD->GetLoaderAllocator(); diff --git a/src/coreclr/vm/genericdict.h b/src/coreclr/vm/genericdict.h index be2a0af5569029..7953973e379173 100644 --- a/src/coreclr/vm/genericdict.h +++ b/src/coreclr/vm/genericdict.h @@ -58,6 +58,9 @@ enum DictionaryEntryKind DispatchStubAddrSlot = 5, FieldDescSlot = 6, DeclaringTypeHandleSlot = 7, + // Only used by ReadyToRun signatures (READYTORUN_FIXUP_DeclaringTypeHandle). The signature encodes a + // method and the slot is populated with the type which declares that method. + DeclaringTypeHandleFromMethodSlot = 8, }; enum DictionaryEntrySignatureSource : BYTE diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 8fc64fc082d3b1..7f6c6f72e4eb83 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14407,6 +14407,33 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, } break; + case READYTORUN_FIXUP_DeclaringTypeHandle: + { + // The signature describes a method; the value of the fixup is the type which declares that method. + // The method may be declared on a base type of the type referenced by the token, and the MethodDesc + // found for it may belong to a canonical instantiation of that base type, so walk the parent chain of + // the (exact) type from the token to recover the exact declaring type. + TypeHandle thOwner; + MethodDesc * pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, &thOwner); + + MethodTable * pDeclaringMT = pMethod->GetMethodTable(); + if (!thOwner.IsNull() && !thOwner.IsTypeDesc()) + { + MethodTable * pExactDeclaringMT = thOwner.AsMethodTable()->GetMethodTableMatchingParentClass(pDeclaringMT); + if (pExactDeclaringMT != NULL) + pDeclaringMT = pExactDeclaringMT; + } + + if (currentModule->IsReadyToRun()) + { + // We do not emit activation fixups for version resilient references. Activate the target explicitly. + pDeclaringMT->EnsureInstanceActive(); + } + + result = (size_t)TypeHandle(pDeclaringMT).AsPtr(); + } + break; + case READYTORUN_FIXUP_FieldHandle: result = (size_t) ZapSig::DecodeField(currentModule, pInfoModule, pBlob); break; diff --git a/src/tests/readytorun/tests/main.cs b/src/tests/readytorun/tests/main.cs index f0a98c93b8e1f1..1819b5458c73e6 100644 --- a/src/tests/readytorun/tests/main.cs +++ b/src/tests/readytorun/tests/main.cs @@ -621,7 +621,11 @@ static void RunAllTests() Console.WriteLine("TestDefaultVsExactStaticVirtualMethodImplementation"); TestDefaultVsExactStaticVirtualMethodImplementation(); +#if !NO_CROSS_MODULE_INLINING + // ILInliningVersioningTest validates the set of methods that were inlined across modules using the map file + // produced by crossgen2, so it is only meaningful when the test binary is compiled with --opt-cross-module. ILInliningVersioningTest.RunAllTests(typeof(Program).Assembly); +#endif } public static int Main() diff --git a/src/tests/readytorun/tests/mainv3.csproj b/src/tests/readytorun/tests/mainv3.csproj index 478a93d935de7c..1108a69969a97b 100644 --- a/src/tests/readytorun/tests/mainv3.csproj +++ b/src/tests/readytorun/tests/mainv3.csproj @@ -1,5 +1,5 @@ - + true @@ -7,6 +7,8 @@ false true + + $(DefineConstants);NO_CROSS_MODULE_INLINING true true @@ -107,13 +109,13 @@ then exit 1 fi -if [ ! -f IL_DLLS/mainv2.dll ] +if [ ! -f IL_DLLS/mainv3.dll ] then - cp mainv2.dll IL_DLLS/mainv2.dll + cp mainv3.dll IL_DLLS/mainv3.dll fi -if [ ! -f IL_DLLS/mainv2.dll ] +if [ ! -f IL_DLLS/mainv3.dll ] then - echo Failed to copy mainv2.dll to IL_DLLS + echo Failed to copy mainv3.dll to IL_DLLS exit 1 fi @@ -154,9 +156,9 @@ then echo Crossgen failed with exitcode: $__cgExitCode exit 1 fi -if [ ! -f mainv2.map ] +if [ ! -f mainv3.map ] then - echo Failed to build mainv2.dll + echo Failed to build mainv3.dll exit 1 fi From dacf12551d111ee30b67a91481c31540f2434703 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Fri, 21 Aug 2026 12:16:16 -0700 Subject: [PATCH 3/6] Remove unnecessary processing --- .../DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs index 0e5f3358eccff7..94623a51708466 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GenericLookupSignature.cs @@ -145,11 +145,6 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact if (_typeArgument != null) factory.AddVirtualMethodDiscoveryDependencies(ref dependencies, _typeArgument); } - else if (_fixupKind == ReadyToRunFixupKind.DeclaringTypeHandle) - { - // This fixup is a type handle lookup as well - the type is the one declaring the method. - factory.AddVirtualMethodDiscoveryDependencies(ref dependencies, _methodArgument.Method.OwningType); - } return dependencies; } From f5bafe9e604094b46b926d7795b3d375964c5468 Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 24 Aug 2026 10:37:05 -0700 Subject: [PATCH 4/6] Improve asserts and testing --- src/coreclr/vm/genericdict.cpp | 2 ++ src/coreclr/vm/jitinterface.cpp | 2 ++ src/tests/readytorun/tests/main.cs | 4 ++++ src/tests/readytorun/tests/test.cs | 8 ++++++++ 4 files changed, 16 insertions(+) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index 38299d9212ff9e..01c863eefbc9ab 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -1050,6 +1050,8 @@ Dictionary::PopulateEntry( pDeclaringMT->EnsureInstanceActive(); + _ASSERT(!pDeclaringMT->IsSharedByGenericInstantiations()); + result = (CORINFO_GENERIC_HANDLE)TypeHandle(pDeclaringMT).AsPtr(); break; } diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 7f6c6f72e4eb83..bd5737b7396c66 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14430,6 +14430,8 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, pDeclaringMT->EnsureInstanceActive(); } + _ASSERTE(!pDeclaringMT->IsSharedByGenericInstantiations()); + result = (size_t)TypeHandle(pDeclaringMT).AsPtr(); } break; diff --git a/src/tests/readytorun/tests/main.cs b/src/tests/readytorun/tests/main.cs index 1819b5458c73e6..0f8ef26c95cf7e 100644 --- a/src/tests/readytorun/tests/main.cs +++ b/src/tests/readytorun/tests/main.cs @@ -205,7 +205,9 @@ static void TestMovedGenericVirtualMethod() var o = new MyChildGeneric(); Assert.AreEqual(o.MovedToBaseClass(), typeof(List).ToString()); + Assert.AreEqual(new Func(o.MovedToBaseClass)(), typeof(List).ToString()); Assert.AreEqual(o.ChangedToVirtual(), typeof(List).ToString()); + Assert.AreEqual(new Func(o.MovedToBaseClass)(), "MyIntermediateGeneric.MovedToBaseClass"); // Test that changing a virtual to a non-virtual doesn't cause a crash. (Behavior is somewhat undefined, as this change is explicitly defined as a breaking change.) Assert.AreEqual(GetChangedToNonVirtualDelegate(o)(), typeof(List).ToString()); @@ -239,7 +241,9 @@ static void TestMovedGenericVirtualMethodSharedGeneric() var o = new MyChildGeneric(); Assert.AreEqual(o.MovedToBaseClass(), typeof(List).ToString()); + Assert.AreEqual(new Func(o.MovedToBaseClass)(), typeof(List).ToString()); Assert.AreEqual(o.ChangedToVirtual(), typeof(List).ToString()); + Assert.AreEqual(new Func(o.MovedToBaseClass)(), "MyIntermediateGeneric.MovedToBaseClass"); // Test that changing a virtual to a non-virtual doesn't cause a crash. (Behavior is somewhat undefined, as this change is explicitly defined as a breaking change.) Assert.AreEqual(GetChangedToNonVirtualDelegate(o)(), typeof(List).ToString()); diff --git a/src/tests/readytorun/tests/test.cs b/src/tests/readytorun/tests/test.cs index 6f1e2159cb5a19..d53dbcbb68e388 100644 --- a/src/tests/readytorun/tests/test.cs +++ b/src/tests/readytorun/tests/test.cs @@ -326,6 +326,10 @@ public string MovedToBaseClass() typeof(Dictionary).ToString(); return typeof(List).ToString(); } + public string MovedToBaseClass() + { + return "MyIntermediateGeneric.MovedToBaseClass"; + } } #endif @@ -346,6 +350,10 @@ public string MovedToBaseClass() { return typeof(List).ToString(); } + public string MovedToBaseClass() + { + return "MyIntermediateGeneric.MovedToBaseClass"; + } #endif #if V2 From b217a64608c6cf5268e757b3f974e09880f71c5d Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 24 Aug 2026 11:59:57 -0700 Subject: [PATCH 5/6] Handle unencodable declaring type lookups in crossgen2 Use method-based declaring type handle lookups when an exact generic owner cannot be represented directly in a ReadyToRun signature. Cover standalone and dictionary resolution for both MemberRef and MethodSpec signatures, including methods moved through a transformed generic parent hierarchy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 571c2544-a08b-41b0-a15d-0f70068f122f --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 33 ++++++++++- src/tests/readytorun/tests/main.cs | 59 +++++++++++++++++++ src/tests/readytorun/tests/test.cs | 35 +++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index c41c72a4c7b581..d8f64716295f98 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -2672,9 +2672,20 @@ private void getCallInfo(ref CORINFO_RESOLVED_TOKEN pResolvedToken, CORINFO_RESO } else { - pResult->instParamLookup = CreateConstLookupToSymbol(_compilation.SymbolNodeFactory.CreateReadyToRunHelper( - ReadyToRunHelperId.TypeDictionary, - exactType)); + ISymbolNode instParamLookup; + if (targetMethod.OwningType == exactType && !CanEncodeTypeInSignature(exactType)) + { + instParamLookup = _compilation.SymbolNodeFactory.CreateReadyToRunHelper( + ReadyToRunHelperId.DeclaringTypeHandle, + ComputeMethodWithToken(targetMethod, ref resolvedToken, constrainedType, unboxing: false)); + } + else + { + instParamLookup = _compilation.SymbolNodeFactory.CreateReadyToRunHelper( + ReadyToRunHelperId.TypeDictionary, + exactType); + } + pResult->instParamLookup = CreateConstLookupToSymbol(instParamLookup); } } } @@ -2845,6 +2856,22 @@ private void ComputeRuntimeLookupForSharedGenericToken( Debug.Assert(!strippedInstantiation); helperArg = new FieldWithToken(fieldDesc, fieldToken, forceOwningTypeNotDerivedFromToken: strippedInstantiation); } + else if (helperId == ReadyToRunHelperId.TypeHandle && + helperArg is TypeDesc typeDesc && + pResolvedToken.hMethod != null && + HandleToObject(pResolvedToken.hMethod) is MethodDesc declaringMethod && + declaringMethod.OwningType == typeDesc && + !CanEncodeTypeInSignature(typeDesc)) + { + // The type handle being looked up is the type which declares the method described by the token. Encode + // the method instead when the declaring type isn't referenceable from the modules being compiled. + helperId = ReadyToRunHelperId.DeclaringTypeHandle; + helperArg = ComputeMethodWithToken( + declaringMethod, + ref pResolvedToken, + constrainedType: null, + unboxing: false); + } var methodContext = new GenericContext(callerHandle); ISymbolNode helper = _compilation.SymbolNodeFactory.GenericLookupHelper( diff --git a/src/tests/readytorun/tests/main.cs b/src/tests/readytorun/tests/main.cs index 0f8ef26c95cf7e..55703b65ffffff 100644 --- a/src/tests/readytorun/tests/main.cs +++ b/src/tests/readytorun/tests/main.cs @@ -240,6 +240,8 @@ static void TestMovedGenericVirtualMethodSharedGeneric() { var o = new MyChildGeneric(); + // The generic method moved from MyChildGeneric to MyIntermediateGeneric. This call requires a + // generic-dictionary DeclaringTypeHandle lookup whose method is encoded as a MethodSpec. Assert.AreEqual(o.MovedToBaseClass(), typeof(List).ToString()); Assert.AreEqual(new Func(o.MovedToBaseClass)(), typeof(List).ToString()); Assert.AreEqual(o.ChangedToVirtual(), typeof(List).ToString()); @@ -269,6 +271,8 @@ static void TestMovedGenericVirtualMethodSharedGeneric() Assert.AreEqual("NullReferenceException", "thrown"); var o2 = new MyChildClass(); + // The generic method moved from MyChildClass to MyIntermediateClass. Because the owning type is not generic, + // this requires a standalone DeclaringTypeHandle lookup whose method is encoded as a MethodSpec. Assert.AreEqual(o2.MovedToBaseClassGeneric(), "MovedToBaseClassGeneric"); } @@ -277,6 +281,59 @@ static void TestMovedGenericVirtualMethodSharedGenericCaller() TestMovedGenericVirtualMethodSharedGeneric(); } + static void TestDeclaringTypeHandle() + { + TestDirectDeclaringTypeHandle(); + TestDeclaringTypeHandleSharedGeneric(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TestDirectDeclaringTypeHandle() + { + var instance = new DeclaringTypeHandleChild(); + // The closed generic owning type makes this a standalone DeclaringTypeHandle lookup. The generic method is + // encoded as a MethodSpec, and the delegate checks that the exact transformed declaring type was recovered. + Assert.AreEqual(instance.MovedToBaseClass(), typeof(V)); + + var movedMethod = new Func(instance.MovedToBaseClass); + Assert.AreEqual(movedMethod(), typeof(V)); + AssertDeclaringType(movedMethod.Method.DeclaringType, typeof(int), typeof(string[])); + + // A non-generic method is encoded as a MemberRef. The static call needs the declaring type as its hidden + // instantiation argument, so this covers a standalone DeclaringTypeHandle lookup using a MemberRef. + Type[] staticMethodTypes = DeclaringTypeHandleChild.StaticMovedToBaseClass(); + Assert.AreEqual(staticMethodTypes[0], typeof(int)); + Assert.AreEqual(staticMethodTypes[1], typeof(string[])); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TestDeclaringTypeHandleSharedGeneric() + { + var instance = new DeclaringTypeHandleChild(); + // Runtime-determined type and method arguments require a generic-dictionary DeclaringTypeHandle lookup whose + // method is encoded as a MethodSpec. + Assert.AreEqual(instance.MovedToBaseClass(), typeof(V)); + + var movedMethod = new Func(instance.MovedToBaseClass); + Assert.AreEqual(movedMethod(), typeof(V)); + AssertDeclaringType(movedMethod.Method.DeclaringType, typeof(U), typeof(T[])); + + // The static non-generic method requires the same dictionary lookup for its hidden instantiation argument, + // but its method is encoded directly as a MemberRef. + Type[] staticMethodTypes = DeclaringTypeHandleChild.StaticMovedToBaseClass(); + Assert.AreEqual(staticMethodTypes[0], typeof(U)); + Assert.AreEqual(staticMethodTypes[1], typeof(T[])); + } + + static void AssertDeclaringType(Type declaringType, Type firstTypeArgument, Type secondTypeArgument) + { + Assert.AreEqual(declaringType.Name, "DeclaringTypeHandleIntermediate`2"); + + Type[] typeArguments = declaringType.GetGenericArguments(); + Assert.AreEqual(typeArguments[0], firstTypeArgument); + Assert.AreEqual(typeArguments[1], secondTypeArgument); + } + [MethodImplAttribute(MethodImplOptions.NoInlining)] static void TestGenericNonVirtualMethod() { @@ -564,6 +621,8 @@ static void RunAllTests() TestGenericVirtualMethod(); Console.WriteLine("TestMovedGenericVirtualMethod"); TestMovedGenericVirtualMethod(); + Console.WriteLine("TestDeclaringTypeHandle"); + TestDeclaringTypeHandle(); Console.WriteLine("TestGenericNonVirtualMethod"); TestGenericNonVirtualMethod(); diff --git a/src/tests/readytorun/tests/test.cs b/src/tests/readytorun/tests/test.cs index d53dbcbb68e388..d1ce9093dd0906 100644 --- a/src/tests/readytorun/tests/test.cs +++ b/src/tests/readytorun/tests/test.cs @@ -365,6 +365,41 @@ public override string ChangedToVirtual() #endif } +#if V2 +// V1 declares these methods on DeclaringTypeHandleChild. V2 moves them to an intermediate base and transforms +// the exact declaring type from to so the runtime hierarchy walk must recover more than the TypeDef. +public class DeclaringTypeHandleIntermediate +{ + public Type MovedToBaseClass() + { + return typeof(TMethod); + } + + public static Type[] StaticMovedToBaseClass() + { + return new Type[] { typeof(TFirst), typeof(TSecond) }; + } +} +#endif + +public class DeclaringTypeHandleChild +#if V2 + : DeclaringTypeHandleIntermediate +#endif +{ +#if !V2 + public Type MovedToBaseClass() + { + return typeof(TMethod); + } + + public static Type[] StaticMovedToBaseClass() + { + return new Type[] { typeof(U), typeof(T[]) }; + } +#endif +} + [StructLayout(LayoutKind.Sequential)] public class MyClassWithLayout { From 55a8ea6f4c845d881a2fe03e4946e38108230ccd Mon Sep 17 00:00:00 2001 From: David Wrighton Date: Mon, 24 Aug 2026 12:48:19 -0700 Subject: [PATCH 6/6] Address ReadyToRun declaring type review findings Bump the ReadyToRun format to version 27 because the declaring type fixup encoding is not forward-compatible with version 26 runtimes. Reuse MethodDesc exact-owner resolution, reject invalid hierarchy matches, keep array handling exact, and limit the new runtime coverage to the mainv3 configuration that exercises the intended fixups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 571c2544-a08b-41b0-a15d-0f70068f122f --- src/coreclr/inc/readytorun.h | 6 +++--- .../nativeaot/Runtime/inc/ModuleHeaders.h | 4 ++-- .../Common/Internal/Runtime/ModuleHeaders.cs | 4 ++-- .../ReadyToRunReader.cs | 4 ++-- .../ReadyToRunSignature.cs | 2 +- src/coreclr/vm/genericdict.cpp | 19 ++++++++++--------- src/coreclr/vm/jitinterface.cpp | 15 ++++++++++----- src/tests/readytorun/tests/main.cs | 19 +++++++++++-------- src/tests/readytorun/tests/mainv3.csproj | 4 ++-- 9 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index b0d895f2174c67..c81c19a6017005 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -19,8 +19,8 @@ // src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. -#define READYTORUN_MAJOR_VERSION 26 -#define READYTORUN_MINOR_VERSION 0x0002 +#define READYTORUN_MAJOR_VERSION 27 +#define READYTORUN_MINOR_VERSION 0x0000 #define MINIMUM_READYTORUN_MAJOR_VERSION 26 @@ -67,7 +67,7 @@ // R2R Version 25 renames runtime async infrastructure members, makes thunk-used members NonVersionable, and frees up a flag in CorInfoContinuationFlags // R2R Version 26 changes ARM64 NativeVarInfo register encoding to include V0-V31 // R2R Version 26.1 adds READYTORUN_FIXUP_StoreMultiCallableAddrOfCode for storing a method's MultiCallableAddrOfCode into a location in the R2R image (used on WebAssembly) -// R2R Version 26.2 redefines READYTORUN_FIXUP_DeclaringTypeHandle to be encoded as a method signature instead of a pair of type signatures +// R2R Version 27 redefines READYTORUN_FIXUP_DeclaringTypeHandle to be encoded as a method signature instead of a pair of type signatures struct READYTORUN_CORE_HEADER { diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index 22185114bb6dd5..33f47c9cb339a1 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -11,8 +11,8 @@ struct ReadyToRunHeaderConstants { static const uint32_t Signature = 0x00525452; // 'RTR' - static const uint32_t CurrentMajorVersion = 26; - static const uint32_t CurrentMinorVersion = 2; + static const uint32_t CurrentMajorVersion = 27; + static const uint32_t CurrentMinorVersion = 0; }; struct ReadyToRunHeader diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index e4e1d24529a336..2599ae355ebf5d 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -15,8 +15,8 @@ internal struct ReadyToRunHeaderConstants { public const uint Signature = 0x00525452; // 'RTR' - public const ushort CurrentMajorVersion = 26; - public const ushort CurrentMinorVersion = 2; + public const ushort CurrentMajorVersion = 27; + public const ushort CurrentMinorVersion = 0; } #if READYTORUN #pragma warning disable 0169 diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs index 58757cb3753554..7a57773a932dcb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunReader.cs @@ -243,7 +243,7 @@ public bool ComponentAssemblyIndicesStartAtTwo public int ComponentAssemblyIndexOffset => (ComponentAssemblyIndicesStartAtTwo ? 2 : 1); /// - /// Starting with R2R version 26.2, READYTORUN_FIXUP_DeclaringTypeHandle is followed by a method + /// Starting with R2R version 27, READYTORUN_FIXUP_DeclaringTypeHandle is followed by a method /// signature and resolves to the type declaring that method. In earlier versions (emitted only by /// crossgen1) it was followed by the declaring type signature and then the type from the token. /// @@ -252,7 +252,7 @@ public bool DeclaringTypeHandleFixupUsesMethodSignature get { EnsureHeader(); - return _readyToRunHeader.MajorVersion > 26 || (_readyToRunHeader.MajorVersion == 26 && _readyToRunHeader.MinorVersion >= 2); + return _readyToRunHeader.MajorVersion >= 27; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs index cd91b099cbe0ca..1323adec086780 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs @@ -1462,7 +1462,7 @@ private ReadyToRunSignature ParseSignature(ReadyToRunFixupKind fixupType, String } else { - // Images predating R2R version 26.2 encode the declaring type followed by the type + // Images predating R2R version 27 encode the declaring type followed by the type // named by the token. ParseType(builder); builder.Append(" of "); diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index a3db0a00321923..3fbec48448ff94 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -928,10 +928,7 @@ Dictionary::PopulateEntry( { FieldDesc * pFDDummy = NULL; - // For DeclaringTypeHandleFromMethodSlot the type referenced by the token is needed to compute - // the exact instantiation of the type which declares the method, so ask for the actual type. - MemberLoader::GetDescFromMemberRef(pZapSigContext->pInfoModule, TokenFromRid(rid, mdtMemberRef), &pMethod, &pFDDummy, NULL, FALSE, &ownerType, - fDeclaringTypeHandleFromMethod /* actualTypeRequired */); + MemberLoader::GetDescFromMemberRef(pZapSigContext->pInfoModule, TokenFromRid(rid, mdtMemberRef), &pMethod, &pFDDummy, NULL, FALSE, &ownerType); _ASSERTE(pMethod != NULL && pFDDummy == NULL); } else @@ -1038,12 +1035,16 @@ Dictionary::PopulateEntry( // The method may be declared on a base type of the type referenced by the token, and the MethodDesc // found for it may belong to a canonical instantiation of that base type, so walk the parent chain of // the (exact) type from the token to recover the exact declaring type. - MethodTable * pDeclaringMT = pMethod->GetMethodTable(); - if (pOwnerMT != NULL) + MethodTable * pDeclaringMT; + if (pMethod->IsArray()) { - MethodTable * pExactDeclaringMT = pOwnerMT->GetMethodTableMatchingParentClass(pDeclaringMT); - if (pExactDeclaringMT != NULL) - pDeclaringMT = pExactDeclaringMT; + pDeclaringMT = pOwnerMT; + } + else + { + pDeclaringMT = pMethod->GetExactDeclaringType(pOwnerMT); + if (pDeclaringMT == NULL) + COMPlusThrowHR(COR_E_TYPELOAD); } pDeclaringMT->EnsureInstanceActive(); diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index c71703b3f277b3..2d1d62a0cef688 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -14415,12 +14415,17 @@ BOOL LoadDynamicInfoEntry(Module *currentModule, TypeHandle thOwner; MethodDesc * pMethod = ZapSig::DecodeMethod(currentModule, pInfoModule, pBlob, &thOwner); - MethodTable * pDeclaringMT = pMethod->GetMethodTable(); - if (!thOwner.IsNull() && !thOwner.IsTypeDesc()) + MethodTable * pOwnerMT = thOwner.GetMethodTable(); + MethodTable * pDeclaringMT; + if (pMethod->IsArray()) { - MethodTable * pExactDeclaringMT = thOwner.AsMethodTable()->GetMethodTableMatchingParentClass(pDeclaringMT); - if (pExactDeclaringMT != NULL) - pDeclaringMT = pExactDeclaringMT; + pDeclaringMT = pOwnerMT; + } + else + { + pDeclaringMT = pMethod->GetExactDeclaringType(pOwnerMT); + if (pDeclaringMT == NULL) + COMPlusThrowHR(COR_E_TYPELOAD); } if (currentModule->IsReadyToRun()) diff --git a/src/tests/readytorun/tests/main.cs b/src/tests/readytorun/tests/main.cs index 55703b65ffffff..7c02704e4dc7f5 100644 --- a/src/tests/readytorun/tests/main.cs +++ b/src/tests/readytorun/tests/main.cs @@ -281,6 +281,7 @@ static void TestMovedGenericVirtualMethodSharedGenericCaller() TestMovedGenericVirtualMethodSharedGeneric(); } +#if TEST_DECLARING_TYPE_HANDLE static void TestDeclaringTypeHandle() { TestDirectDeclaringTypeHandle(); @@ -297,7 +298,7 @@ static void TestDirectDeclaringTypeHandle() var movedMethod = new Func(instance.MovedToBaseClass); Assert.AreEqual(movedMethod(), typeof(V)); - AssertDeclaringType(movedMethod.Method.DeclaringType, typeof(int), typeof(string[])); + Assert.AreEqual(movedMethod.Method.DeclaringType, GetExpectedDeclaringType(instance.GetType())); // A non-generic method is encoded as a MemberRef. The static call needs the declaring type as its hidden // instantiation argument, so this covers a standalone DeclaringTypeHandle lookup using a MemberRef. @@ -316,7 +317,7 @@ static void TestDeclaringTypeHandleSharedGeneric() var movedMethod = new Func(instance.MovedToBaseClass); Assert.AreEqual(movedMethod(), typeof(V)); - AssertDeclaringType(movedMethod.Method.DeclaringType, typeof(U), typeof(T[])); + Assert.AreEqual(movedMethod.Method.DeclaringType, GetExpectedDeclaringType(instance.GetType())); // The static non-generic method requires the same dictionary lookup for its hidden instantiation argument, // but its method is encoded directly as a MemberRef. @@ -325,14 +326,12 @@ static void TestDeclaringTypeHandleSharedGeneric() Assert.AreEqual(staticMethodTypes[1], typeof(T[])); } - static void AssertDeclaringType(Type declaringType, Type firstTypeArgument, Type secondTypeArgument) + static Type GetExpectedDeclaringType(Type instanceType) { - Assert.AreEqual(declaringType.Name, "DeclaringTypeHandleIntermediate`2"); - - Type[] typeArguments = declaringType.GetGenericArguments(); - Assert.AreEqual(typeArguments[0], firstTypeArgument); - Assert.AreEqual(typeArguments[1], secondTypeArgument); + Type baseType = instanceType.BaseType; + return baseType == typeof(object) ? instanceType : baseType; } +#endif [MethodImplAttribute(MethodImplOptions.NoInlining)] static void TestGenericNonVirtualMethod() @@ -621,8 +620,12 @@ static void RunAllTests() TestGenericVirtualMethod(); Console.WriteLine("TestMovedGenericVirtualMethod"); TestMovedGenericVirtualMethod(); +#if TEST_DECLARING_TYPE_HANDLE + // mainv1 has no moved method, while mainv2 can inline the V1 implementation. Only mainv3 compiles the V1 + // callsite against V2 without cross-module inlining, which exercises these runtime declaring-type fixups. Console.WriteLine("TestDeclaringTypeHandle"); TestDeclaringTypeHandle(); +#endif Console.WriteLine("TestGenericNonVirtualMethod"); TestGenericNonVirtualMethod(); diff --git a/src/tests/readytorun/tests/mainv3.csproj b/src/tests/readytorun/tests/mainv3.csproj index 1108a69969a97b..487aecda28da3a 100644 --- a/src/tests/readytorun/tests/mainv3.csproj +++ b/src/tests/readytorun/tests/mainv3.csproj @@ -8,8 +8,8 @@ true - $(DefineConstants);NO_CROSS_MODULE_INLINING - + $(DefineConstants);NO_CROSS_MODULE_INLINING;TEST_DECLARING_TYPE_HANDLE + true true