diff --git a/docs/design/features/typemap.md b/docs/design/features/typemap.md index 9e1e7572491625..cf7a918fb740a7 100644 --- a/docs/design/features/typemap.md +++ b/docs/design/features/typemap.md @@ -200,6 +200,8 @@ An entry in an External Type Map is included when the "trim target" type is refe Many of these instructions can be passed a generic parameter. In that case, the trimming tool should consider type arguments of instantiations of that type as having met one of these rules and include any entries with those types as "trim target" types. +For pointer, byref, and array types as trim targets, the entries are preserved if the element type meets the requirements above to keep the entry. A trimming tool is free to remove entries if it can prove that the pointer, byref, or array type cannot be represented at runtime. + ### Proxy Type Map An entry in the Proxy Type Map is included when the "source type" is referenced in one of the following ways: @@ -220,3 +222,5 @@ If the type is an interface type and the user could possibly see a `RuntimeTypeH - The owning type of the method argument to `callvirt`, or `ldvirtftn`. Finally, if the trimming tool determines that it is impossible to retrieve a `System.Type` instance the represents the "source type" at runtime, then the entry may be omitted from the Proxy Type Map as its existence is unobservable. + +For pointer, byref, and array types as source types, the entries are preserved if the element type meets the requirements above to keep the entry. A trimming tool is free to remove entries if it can prove that the pointer, byref, or array type cannot be represented at runtime. diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs index 9fa357c3329b8c..0a15fdc18cabbb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs @@ -55,6 +55,32 @@ public override IEnumerable GetConditionalStaticDep context.NativeLayout.TemplateTypeLayout(canonTrimmingType), "External type map trim target that could be loaded at runtime"); } + else if (effectiveTrimTargetType is ArrayType arrayType) + { + // Some arrays don't have array templates (e.g. multidimensional arrays, arrays of pointers). + // If the element type is template-loadable, the runtime can still construct the array type. + TypeDesc effectiveElementType = GetEffectiveTrimTargetType(arrayType.ElementType); + TypeDesc canonElementType = effectiveElementType.ConvertToCanonForm(CanonicalFormKind.Specific); + if (canonElementType != effectiveElementType && GenericTypesTemplateMap.IsEligibleToHaveATemplate(canonElementType)) + { + yield return new CombinedDependencyListEntry( + context.NecessaryTypeSymbol(effectiveTrimTargetType), + context.NativeLayout.TemplateTypeLayout(canonElementType), + "External type map array trim target with template-loadable element type"); + } + + // Array types that aren't eligible for templates (MdArrays, pointer/fnptr-element SzArrays) + // can be constructed at runtime from just the element type's MethodTable using hardcoded + // templates (typeof(object[,]) for MdArrays, typeof(char*[]) for pointer arrays). + // If the element type is reachable, consider the array type reachable as well. + if (!GenericTypesTemplateMap.IsArrayTypeEligibleForTemplate(arrayType)) + { + yield return new CombinedDependencyListEntry( + context.NecessaryTypeSymbol(effectiveTrimTargetType), + context.NecessaryTypeSymbol(effectiveElementType), + "Array without template can be constructed at runtime from element type"); + } + } } } } @@ -100,12 +126,12 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer } } - // Strip parameterized type wrappers (arrays, pointers, byrefs) to get the effective - // type for trimming purposes. If the trim target is Foo[], the TypeMap entry should be - // included when Foo is reachable, matching ILLink's TypeMapHandler stripping behavior. + // Strip non-array parameterized wrappers (pointers, byrefs) to get the effective + // trimming target type. Arrays are preserved so trim dependencies can be conditioned + // on array existence rather than just element type reachability. private static TypeDesc GetEffectiveTrimTargetType(TypeDesc trimmingTargetType) { - while (trimmingTargetType is ParameterizedType parameterized) + while (trimmingTargetType is ParameterizedType parameterized && !trimmingTargetType.IsArray) trimmingTargetType = parameterized.ParameterType; return trimmingTargetType; } diff --git a/src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs b/src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs index b8cfacbf4f4bfe..808236789e05d3 100644 --- a/src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs +++ b/src/tests/nativeaot/SmokeTests/TrimmingBehaviors/DeadCodeElimination.cs @@ -23,6 +23,8 @@ [assembly: TypeMap("E", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target5), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget5[]))] [assembly: TypeMap("F", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target6), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget6[]))] [assembly: TypeMap("G", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target7), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget7[]))] +[assembly: TypeMap("H", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target8), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget8[]))] +[assembly: TypeMap("I", typeof(DeadCodeElimination.TestInteropMapArrayTrimming.Target9), typeof(DeadCodeElimination.TestInteropMapArrayTrimming.TrimTarget9[,]))] class DeadCodeElimination { @@ -1387,6 +1389,8 @@ public struct TrimTarget4; public struct TrimTarget5; public struct TrimTarget6; public struct TrimTarget7; + public struct TrimTarget8; + public class TrimTarget9; public class Target1; public class Target2; public class Target3; @@ -1394,6 +1398,8 @@ public class Target4; public class Target5; public class Target6; public class Target7; + public class Target8; + public class Target9; public class Atom; public static unsafe object[] MakeGenerics() @@ -1414,18 +1420,36 @@ public static unsafe object[] MakeGenerics() [MethodImpl(MethodImplOptions.NoInlining)] static object GetUnknown() => null; + [MethodImpl(MethodImplOptions.NoInlining)] + public static object MakeSharedGeneric() + => new T[1,1]; + + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool TestSharedGeneric() + => MakeSharedGeneric() is T[,]; + + [MethodImpl(MethodImplOptions.NoInlining)] + static Type GetTrimTarget9() => typeof(TrimTarget9); + public static void Run() { if (GetUnknown() is TrimTarget7[]) { Console.WriteLine("Unexpected"); } + if (GetUnknown() is TrimTarget8) + { + Console.WriteLine("Unexpected"); + } typeof(TestInteropMapArrayTrimming).GetMethod(nameof(MakeGenerics)).MakeGenericMethod([GetAtom()]).Invoke(null, []); + typeof(TestInteropMapArrayTrimming).GetMethod(nameof(TestSharedGeneric)).MakeGenericMethod([GetTrimTarget9()]).Invoke(null, []); + var map = TypeMapping.GetOrCreateExternalTypeMapping(); // A, B, C, F, G: trim target element type is reachable — entries must be present + // I: trim target can be constructed at runtime using type loader if (!map.TryGetValue("A", out Type typeA) || typeA.Name != nameof(Target1)) throw new Exception("Expected entry A"); ThrowIfUsableMethodTable(typeA); @@ -1446,11 +1470,17 @@ public static void Run() throw new Exception("Expected entry G"); ThrowIfUsableMethodTable(typeG); - // D, E: element type is unreachable — entries must be absent + if (!map.TryGetValue("I", out Type typeI) || typeI.Name != nameof(Target9)) + throw new Exception("Expected entry I"); + ThrowIfUsableMethodTable(typeI); + + // D, E: trim target element type is unreachable. H: element is reachable, but TrimTarget8[] is unreachable. if (map.TryGetValue("D", out _)) throw new Exception("Unexpected entry D"); if (map.TryGetValue("E", out _)) throw new Exception("Unexpected entry E"); + if (map.TryGetValue("H", out _)) + throw new Exception("Unexpected entry H"); } } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs index 6027b7c1274e9f..ce887a9d5e8458 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs @@ -211,7 +211,7 @@ static void ConstrainedStaticCall(T t) where T : IStaticInterface Console.WriteLine(new ArrayElement[1]); - Console.WriteLine(new ArrayTypeTrimTargetClass()); + Console.WriteLine(new ArrayTypeTrimTargetClass[1] { new ArrayTypeTrimTargetClass() }); Console.WriteLine(new ConstructedNoTypeCheckNoBoxStruct(42).Value);