diff --git a/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs b/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs index c0a98386343a21..d966c1d672032d 100644 --- a/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs +++ b/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs @@ -107,6 +107,14 @@ public Map(TypeDesc typeMapGroup) public TypeDesc TypeMapGroup { get; } + /// + /// Indicates whether any TypeMapAssemblyTarget attributes were processed for this group, + /// regardless of whether the target assembly was successfully resolved. When true and + /// is empty, it means all target attributes failed to resolve + /// and the runtime should fall back to attribute processing. + /// + public bool HasAssemblyTargetAttributes { get; set; } + public void AddAssociatedTypeMapEntry(TypeDesc type, TypeDesc associatedType) { if (!_associatedTypeMap.TryAdd(type, associatedType)) @@ -205,6 +213,7 @@ public void MergePendingMap(ModuleDesc stubModule, Map pendingMap) } _targetModules.AddRange(pendingMap._targetModules); + HasAssemblyTargetAttributes |= pendingMap.HasAssemblyTargetAttributes; } public void AddTargetModule(ModuleDesc targetModule) @@ -383,6 +392,11 @@ public static TypeMapMetadata CreateFromAssembly(EcmaAssembly assembly, ModuleDe typeMapStates[typeMapGroup] = value; } + if (attrKind is TypeMapAttributeKind.TypeMapAssemblyTarget) + { + value.HasAssemblyTargetAttributes = true; + } + if (attrKind is TypeMapAttributeKind.TypeMapAssemblyTarget or TypeMapAttributeKind.TypeMap) { value.SetExternalTypeMapException(throwHelperEmitModule, ex); @@ -404,6 +418,8 @@ public static TypeMapMetadata CreateFromAssembly(EcmaAssembly assembly, ModuleDe void ProcessTypeMapAssemblyTargetAttribute(CustomAttributeValue attrValue, Map typeMapState) { + typeMapState.HasAssemblyTargetAttributes = true; + if (attrValue.FixedArguments is not [{ Value: string assemblyName }]) { ThrowHelper.ThrowBadImageFormatException(); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs index a3bd9dddb54c54..0806d702d7929c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs @@ -32,6 +32,14 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact DependencyList dependencies = []; foreach (var map in _assemblyTypeMaps.Maps) { + // Skip groups where assembly target attributes were present but all failed to resolve + // (e.g. when the target assembly name doesn't exist). The runtime will fall back to + // attribute processing for these groups. Groups with no assembly target attributes + // at all should still emit an entry so the runtime knows they are precached and + // avoids unnecessary fallback to attribute scanning. + if (map.Value.TargetModules.Count == 0 && map.Value.HasAssemblyTargetAttributes) + continue; + var groupType = map.Key; dependencies.Add(new DependencyListEntry(_importReferenceProvider.GetImportToType(groupType), "Type Map Assembly Target")); foreach (var targetModule in map.Value.TargetModules) @@ -59,6 +67,10 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) foreach (var map in _assemblyTypeMaps.Maps) { + // Skip groups where assembly target attributes were present but all failed to resolve. + if (map.Value.TargetModules.Count == 0 && map.Value.HasAssemblyTargetAttributes) + continue; + var groupType = map.Key; Vertex groupTypeVertex = _importReferenceProvider.EncodeReferenceToType(writer, groupType); VertexSequence modules = new(); diff --git a/src/coreclr/vm/assemblynative.cpp b/src/coreclr/vm/assemblynative.cpp index ce938e9549ff38..a4d71202470ed7 100644 --- a/src/coreclr/vm/assemblynative.cpp +++ b/src/coreclr/vm/assemblynative.cpp @@ -1773,16 +1773,19 @@ extern "C" void QCALLTYPE TypeMapLazyDictionary_ProcessAttributes( (newProxyTypeEntry != nullptr && !hasPrecachedProxy) || !hasPrecachedTargets) { - // Only fall back to attribute parsing for the assembly targets if they were - // not found in the pre-cached R2R section. - if (!hasPrecachedTargets) - { - ProcessTypeMapAttribute( - TypeMapAssemblyTargetAttributeName, - assemblies, - groupTypeMT, - currAssembly); - } + // Fall back to attribute parsing for the assembly targets if they were + // not found in the pre-cached R2R section, or if the external/proxy type + // maps were not pre-cached. When CrossGen2 fails to resolve an assembly + // target, it emits an assembly targets entry with count=0 but marks the + // external/proxy maps as invalid (state=0). Re-processing the assembly + // target attributes in that case ensures the runtime correctly loads (and + // throws for) unresolvable assemblies. The AssemblyTargetProcessor already + // deduplicates, so re-processing is safe. + ProcessTypeMapAttribute( + TypeMapAssemblyTargetAttributeName, + assemblies, + groupTypeMT, + currAssembly); // We will only process the specific type maps if we have a callback to process // the entry and the precached map was not calculated for this module. diff --git a/src/tests/Interop/TypeMap/TypeMapApp.cs b/src/tests/Interop/TypeMap/TypeMapApp.cs index 02259e4738eb54..9c07412d5e3282 100644 --- a/src/tests/Interop/TypeMap/TypeMapApp.cs +++ b/src/tests/Interop/TypeMap/TypeMapApp.cs @@ -265,6 +265,54 @@ public static void Validate_MissingAssemblyTarget() Assert.Throws(() => TypeMapping.GetOrCreateProxyTypeMapping()); } + [Fact] + public static void Validate_MissingAssemblyTarget_DoesNotAffectGroupsWithoutTargets() + { + // Validates that groups without TypeMapAssemblyTarget attributes (like TypicalUseCase) + // still work correctly alongside groups with failing targets (like UnknownAssemblyReference). + // In R2R, groups without assembly targets must have their precached entry emitted so the + // runtime doesn't unnecessarily fall back to attribute scanning. + Console.WriteLine(nameof(Validate_MissingAssemblyTarget_DoesNotAffectGroupsWithoutTargets)); + + Assert.Throws(() => TypeMapping.GetOrCreateExternalTypeMapping()); + + IReadOnlyDictionary externalMap = TypeMapping.GetOrCreateExternalTypeMapping(); + Assert.Equal(typeof(C1), externalMap["1"]); + Assert.Equal(typeof(S1), externalMap["2"]); + + IReadOnlyDictionary proxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); + Assert.Equal(typeof(C1), proxyMap[new C1().GetType()]); + Assert.Equal(typeof(S1), proxyMap[((object)default(S1)).GetType()]); + + // When running in R2R mode, verify the R2R image contains TypeMap sections. + // This confirms CrossGen2 correctly emitted entries for groups without + // TypeMapAssemblyTarget attributes alongside groups with failed targets. + string assemblyLocation = typeof(TypeMap).Assembly.Location; + if (!string.IsNullOrEmpty(assemblyLocation)) + { + string r2rDumpFile = assemblyLocation + ".r2rdump"; + if (File.Exists(r2rDumpFile)) + { + string[] lines = File.ReadAllLines(r2rDumpFile); + bool hasExternalTypeMaps = false; + bool hasProxyTypeMaps = false; + bool hasTypeMapAssemblyTargets = false; + foreach (string line in lines) + { + if (line.Contains("ExternalTypeMaps", StringComparison.Ordinal)) + hasExternalTypeMaps = true; + if (line.Contains("ProxyTypeMaps", StringComparison.Ordinal)) + hasProxyTypeMaps = true; + if (line.Contains("TypeMapAssemblyTargets", StringComparison.Ordinal)) + hasTypeMapAssemblyTargets = true; + } + Assert.True(hasExternalTypeMaps, "R2R image should contain ExternalTypeMaps section"); + Assert.True(hasProxyTypeMaps, "R2R image should contain ProxyTypeMaps section"); + Assert.True(hasTypeMapAssemblyTargets, "R2R image should contain TypeMapAssemblyTargets section"); + } + } + } + [Fact] public static void Validate_EmptyOrInvalidMappings() {