From d5259df005e6a3b701ec6963a07c1c3f5d3dfdfa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 00:01:45 +0200 Subject: [PATCH 1/4] [TrimmableTypeMap] Compile array map anchors Include ArrayMapAnchors.cs in Mono.Android so generated per-rank array TypeMap entries can resolve their shared Microsoft.Android.Runtime.__ArrayMapRankN anchor types at NativeAOT compile time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Mono.Android.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 89bf52ac3b1..e4a833e83ea 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -361,6 +361,7 @@ + From 79e5a2e9b8f280d5ed5390eae15492ba8041d638 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:05:55 +0200 Subject: [PATCH 2/4] [NativeAOT] Inline array map anchors Keep the shared array typemap anchor types in an existing compiled Mono.Android runtime source file so the project does not need a separate Compile item for ArrayMapAnchors.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ArrayMapAnchors.cs | 19 ------------------- .../TrimmableTypeMap.cs | 15 +++++++++++++++ src/Mono.Android/Mono.Android.csproj | 1 - 3 files changed, 15 insertions(+), 20 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs b/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs deleted file mode 100644 index 51b1ddb9cd5..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable enable - -namespace Microsoft.Android.Runtime; - -// Shared TypeMap group anchors for per-rank array entries. Per-assembly typemap DLLs -// reference these so all rank-N entries across all assemblies merge into a single -// dictionary at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>(). -// -// To support a higher MaxArrayRank, add additional types here and bump -// TrimmableTypeMapGenerator.MaxSupportedArrayRank. - -internal sealed class __ArrayMapRank1 { } -internal sealed class __ArrayMapRank2 { } -internal sealed class __ArrayMapRank3 { } -internal sealed class __ArrayMapRank4 { } -internal sealed class __ArrayMapRank5 { } -internal sealed class __ArrayMapRank6 { } -internal sealed class __ArrayMapRank7 { } -internal sealed class __ArrayMapRank8 { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index d4dd2b77919..ab994245c7c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -469,3 +469,18 @@ public MissingJavaPeerProxy () : base ("", typeof (Java.Lang.Object), n } } + +// Shared TypeMap group anchors for per-rank array entries. Per-assembly typemap DLLs +// reference these so all rank-N entries across all assemblies merge into a single +// dictionary at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>(). +// +// To support a higher MaxArrayRank, add additional types here and bump +// TrimmableTypeMapGenerator.MaxSupportedArrayRank. +internal sealed class __ArrayMapRank1 { } +internal sealed class __ArrayMapRank2 { } +internal sealed class __ArrayMapRank3 { } +internal sealed class __ArrayMapRank4 { } +internal sealed class __ArrayMapRank5 { } +internal sealed class __ArrayMapRank6 { } +internal sealed class __ArrayMapRank7 { } +internal sealed class __ArrayMapRank8 { } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index e4a833e83ea..89bf52ac3b1 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -361,7 +361,6 @@ - From b3c5c3a6792ea2e41fdde2ad8b82566a83c069b6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 09:32:47 +0200 Subject: [PATCH 3/4] [TrimmableTypeMap] Keep array rank anchors private Rename the typemap abstraction to ITypeMap and move array lookup behind the unified runtime wrapper. Generated typemap assemblies now define private __ArrayMapRankN anchors, and the root loader builds per-assembly/per-rank array maps instead of referencing shared Mono.Android anchors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 89 +++++++++++++------ .../Generator/TypeMapAssemblyEmitter.cs | 20 +++-- .../TrimmableTypeMapGenerator.cs | 12 --- .../AggregateTypeMap.cs | 14 ++- .../{ITypeMapWithAliasing.cs => ITypeMap.cs} | 13 ++- .../SingleUniverseTypeMap.cs | 34 ++++++- .../TrimmableTypeMap.cs | 71 +++++++-------- src/Mono.Android/Mono.Android.csproj | 2 +- .../RootTypeMapAssemblyGeneratorTests.cs | 30 ++++--- .../Generator/TypeMapModelBuilderTests.cs | 21 +++-- 10 files changed, 192 insertions(+), 114 deletions(-) rename src/Mono.Android/Microsoft.Android.Runtime/{ITypeMapWithAliasing.cs => ITypeMap.cs} (66%) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 46c556bb1e6..dfa61a977c8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// Generates the root _Microsoft.Android.TypeMaps.dll assembly that: /// -/// References all per-assembly typemap assemblies via [assembly: TypeMapAssemblyTargetAttribute<__TypeMapAnchor>("name")]. +/// References all per-assembly typemap assemblies via [assembly: TypeMapAssemblyTargetAttribute<T>("name")]. /// Emits a TypeMapLoader class whose Initialize() method calls /// with the appropriate /// type mapping dictionaries. @@ -36,7 +36,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// TrimmableTypeMap.Initialize( /// TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(), /// TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>(), -/// arrayMapsByRank); +/// arrayMapsByAssemblyAndRank); /// /// // Option B: Per-assembly universes (aggregated) /// var typeMaps = new IReadOnlyDictionary<string, Type>[] { @@ -47,7 +47,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), /// TypeMapping.GetOrCreateProxyTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), /// }; -/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByRank); +/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByAssemblyAndRank); /// } /// } /// } @@ -76,7 +76,7 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// Maximum array rank for which per-assembly typemaps emitted __ArrayMapRank{N} /// sentinels. Must match the value passed to the per-assembly generators. 0 means - /// no array sentinels were emitted; the loader passes null for arrayMapsByRank. + /// no array sentinels were emitted; the loader passes null for array maps. /// public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) { @@ -123,11 +123,10 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha } // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access - // internal types (TrimmableTypeMap and friends in Mono.Android, and __TypeMapAnchor - // in each per-assembly typemap DLL when in aggregate mode). Shared rank anchors - // (__ArrayMapRank{N}) live in Mono.Android already. + // internal types (TrimmableTypeMap and friends in Mono.Android, and private anchors + // in each per-assembly typemap DLL when aggregate universes or array maps are used). var accessTargets = new List { "Mono.Android" }; - if (!useSharedTypemapUniverse) { + if (!useSharedTypemapUniverse || maxArrayRank > 0) { accessTargets.AddRange (perAssemblyTypeMapNames); } pe.EmitIgnoresAccessChecksToAttribute (accessTargets); @@ -217,29 +216,31 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var externalDictArrayTypeSpec = MakeIReadOnlyDictArrayTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); if (useSharedTypemapUniverse) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, maxArrayRank); + initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank); } else { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } } /// - /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either a - /// flat arrayMapsByRank[maxArrayRank] from shared __ArrayMapRank{N} - /// anchors or null when is 0. + /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either + /// arrayMapsByAssemblyAndRank[N][maxArrayRank] from per-assembly + /// __ArrayMapRank{N} anchors or null when is 0. /// static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, + TypeSpecificationHandle externalDictArrayTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, int maxArrayRank) { @@ -267,10 +268,10 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByRank-or-null) + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByAssemblyAndRank-or-null) encoder.LoadLocal (0); encoder.LoadLocal (1); - EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); @@ -306,7 +307,7 @@ static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHan } } - /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], arrayMapsByRank[]). + /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], arrayMapsByAssemblyAndRank[][]). static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -320,7 +321,8 @@ static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuil // Param 2: IReadOnlyDictionary[] blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Param 3: IReadOnlyDictionary?[] + // Param 3: IReadOnlyDictionary?[][] + blob.WriteByte (0x1D); blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, @@ -329,13 +331,14 @@ static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuil /// /// Shared-universe IL emit. Single merged main map (anchored on Java.Lang.Object) - /// plus either a flat arrayMapsByRank[maxArrayRank] from shared + /// plus either arrayMapsByAssemblyAndRank[N][maxArrayRank] from per-assembly /// __ArrayMapRank{N} anchors or null when is 0. /// static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, - TypeSpecificationHandle externalDictTypeSpec, + TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, + IReadOnlyList perAssemblyTypeMapNames, int maxArrayRank) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); @@ -345,19 +348,19 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByRank-or-null) + // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByAssemblyAndRank-or-null) encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSpec); encoder.OpCode (ILOpCode.Call); encoder.Token (getProxySpec); - EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); }); } - /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank[]). + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByAssemblyAndRank[][]). static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -368,29 +371,49 @@ static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); blob.WriteByte (0x1D); + blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); } /// - /// Emits IL that pushes either a fresh IReadOnlyDictionary<string, Type>?[maxArrayRank] + /// Emits IL that pushes either a fresh + /// IReadOnlyDictionary<string, Type>?[assemblyCount][maxArrayRank] /// (when > 0) or ldnull. /// - static void EmitArrayMapsByRankOrNull (PEAssemblyBuilder pe, InstructionEncoder encoder, - MemberReferenceHandle getExternalMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) + static void EmitArrayMapsByAssemblyAndRankOrNull (PEAssemblyBuilder pe, InstructionEncoder encoder, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, + TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, + int maxArrayRank) { if (maxArrayRank == 0) { encoder.OpCode (ILOpCode.Ldnull); return; } - var monoAndroidRuntimeNs = pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + encoder.LoadConstantI4 (perAssemblyTypeMapNames.Count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictArrayTypeSpec); + for (int i = 0; i < perAssemblyTypeMapNames.Count; i++) { + var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (i); + EmitArrayMapsByRank (pe, encoder, asmRef, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + encoder.OpCode (ILOpCode.Stelem_ref); + } + } + + static void EmitArrayMapsByRank (PEAssemblyBuilder pe, InstructionEncoder encoder, + AssemblyReferenceHandle assemblyRef, + MemberReferenceHandle getExternalMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) + { encoder.LoadConstantI4 (maxArrayRank); encoder.OpCode (ILOpCode.Newarr); encoder.Token (externalDictTypeSpec); for (int r = 0; r < maxArrayRank; r++) { - var rankRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, monoAndroidRuntimeNs, + var rankRef = pe.Metadata.AddTypeReference (assemblyRef, default, pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); var rankSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); encoder.OpCode (ILOpCode.Dup); @@ -443,6 +466,18 @@ static TypeSpecificationHandle MakeIReadOnlyDictTypeSpec (PEAssemblyBuilder pe, return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); } + /// + /// Creates a TypeSpec for IReadOnlyDictionary<K, V>[] (for jagged-array newarr). + /// + static TypeSpecificationHandle MakeIReadOnlyDictArrayTypeSpec (PEAssemblyBuilder pe, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, bool keyIsString) + { + var blob = new BlobBuilder (32); + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString); + return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); + } + static void EncodeIReadOnlyDictType (BlobBuilder blob, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, bool keyIsString) { blob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index af36173550f..4e74b6e3e54 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -292,10 +292,9 @@ void EmitAnchorType () } /// - /// Populates _rankAnchorHandles with TypeRefs to the shared - /// Microsoft.Android.Runtime.__ArrayMapRank{N} types in Mono.Android. All per-asm - /// typemap DLLs reference the same anchors so each rank's entries merge into one dict - /// at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>(). + /// Emits private __ArrayMapRank{N} classes used as group type parameters + /// for array TypeMap<T> entries. Each per-assembly typemap owns its + /// rank anchors so array maps stay scoped to the generated assembly. /// void EmitRankSentinels (TypeMapAssemblyData model) { @@ -304,11 +303,16 @@ void EmitRankSentinels (TypeMapAssemblyData model) } _rankAnchorHandles = new EntityHandle [model.MaxArrayRank]; - var ns = _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + var objectRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeRef, + _pe.Metadata.GetOrAddString ("System"), _pe.Metadata.GetOrAddString ("Object")); for (int i = 0; i < model.MaxArrayRank; i++) { - _rankAnchorHandles [i] = _pe.Metadata.AddTypeReference ( - _pe.MonoAndroidRef, ns, - _pe.Metadata.GetOrAddString ($"__ArrayMapRank{i + 1}")); + _rankAnchorHandles [i] = _pe.Metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, + default, + _pe.Metadata.GetOrAddString ($"__ArrayMapRank{i + 1}"), + objectRef, + MetadataTokens.FieldDefinitionHandle (_pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (_pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 090075d73bf..e25293ff982 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -9,12 +9,6 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; public class TrimmableTypeMapGenerator { - /// - /// Runtime-supported maximum array rank — must match the number of - /// __ArrayMapRank{N} types pre-defined in Mono.Android. - /// - public const int MaxSupportedArrayRank = 8; - readonly ITrimmableTypeMapLogger logger; static readonly HashSet RequiredFrameworkDeferredRegistrationTypes = new (StringComparer.Ordinal) { @@ -47,12 +41,6 @@ public TrimmableTypeMapResult Execute ( if (maxArrayRank < 0) { throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); } - if (maxArrayRank > MaxSupportedArrayRank) { - throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, - $"_AndroidTrimmableTypeMapMaxArrayRank={maxArrayRank} exceeds the runtime's supported maximum ({MaxSupportedArrayRank}). " + - $"To raise the limit, add additional __ArrayMapRank{{N}} types to Mono.Android."); - } - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs index 3a997d0bca6..0374570f12a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Runtime; /// results across all universes. Debug-only — each assembly has its own /// universe with an isolated TypeMapLazyDictionary. /// -sealed class AggregateTypeMap : ITypeMapWithAliasing +sealed class AggregateTypeMap : ITypeMap { readonly SingleUniverseTypeMap[] _universes; @@ -41,4 +41,16 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } + + public bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType) + { + foreach (var universe in _universes) { + if (universe.TryGetArrayType (jniName, rankIndex, out arrayType)) { + return true; + } + } + + arrayType = null; + return false; + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs similarity index 66% rename from src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs rename to src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs index df77c07041d..8aa230d273f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs @@ -7,12 +7,12 @@ namespace Microsoft.Android.Runtime; /// -/// Abstraction over the typemap dictionary that handles alias resolution. +/// Abstraction over a generated typemap universe. /// Both Debug (per-assembly universes) and Release (single merged universe) -/// go through this interface, so doesn't -/// need to know about aliasing mechanics. +/// go through this interface, so doesn't need +/// to know about aliasing mechanics or per-rank array map storage. /// -interface ITypeMapWithAliasing +interface ITypeMap { /// /// Returns all proxy types mapped to a JNI name, resolving alias holders. @@ -24,4 +24,9 @@ interface ITypeMapWithAliasing /// carries the attribute). /// bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); + + /// + /// Resolves a JNI leaf name and 0-based array rank index to a managed array type. + /// + bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs index d1094a2a283..a6afdba2bab 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -14,17 +14,35 @@ namespace Microsoft.Android.Runtime; /// alias resolution within that universe. /// Used in both Debug (one per assembly) and Release (single merged). /// -sealed class SingleUniverseTypeMap : ITypeMapWithAliasing +sealed class SingleUniverseTypeMap : ITypeMap { readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; + readonly IReadOnlyDictionary?[][] _arrayMapsByUniverseAndRank; public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) + : this (typeMap, proxyTypeMap, arrayMapsByRank: null) + { + } + + public SingleUniverseTypeMap ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyTypeMap, + IReadOnlyDictionary?[]? arrayMapsByRank) + : this (typeMap, proxyTypeMap, arrayMapsByRank is null ? null : [arrayMapsByRank]) + { + } + + public SingleUniverseTypeMap ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyTypeMap, + IReadOnlyDictionary?[][]? arrayMapsByUniverseAndRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyTypeMap); _typeMap = typeMap; _proxyTypeMap = proxyTypeMap; + _arrayMapsByUniverseAndRank = arrayMapsByUniverseAndRank ?? []; } public IEnumerable GetProxyTypes (string jniName) @@ -83,4 +101,18 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } + + public bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType) + { + foreach (var arrayMapsByRank in _arrayMapsByUniverseAndRank) { + if ((uint)rankIndex < (uint)arrayMapsByRank.Length && + arrayMapsByRank [rankIndex] is { } dict && + dict.TryGetValue (jniName, out arrayType)) { + return true; + } + } + + arrayType = null; + return false; + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ab994245c7c..58455d8ea79 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -13,7 +13,7 @@ namespace Microsoft.Android.Runtime; /// -/// Central type map for the trimmable typemap path. Owns the ITypeMapWithAliasing +/// Central type map for the trimmable typemap path. Owns the ITypeMap /// and provides peer creation, invoker resolution, container factories, and native /// method registration. All proxy attribute access is encapsulated here. /// @@ -28,18 +28,13 @@ public class TrimmableTypeMap s_instance ?? throw new InvalidOperationException ( "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); - readonly ITypeMapWithAliasing _typeMap; - // Per-rank array dictionaries, 0-indexed by (rank - 1). Sourced from the shared - // __ArrayMapRank{N} TypeMap groups, so each rank is one merged dict spanning every - // per-asm typemap DLL. Empty when no array entries were emitted (CoreCLR builds). - readonly IReadOnlyDictionary?[] _arrayMapsByRank; + readonly ITypeMap _typeMap; readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); - TrimmableTypeMap (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) + TrimmableTypeMap (ITypeMap typeMap) { _typeMap = typeMap; - _arrayMapsByRank = arrayMapsByRank ?? []; } /// @@ -65,33 +60,42 @@ public static void Initialize ( { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); - InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap), arrayMapsByRank); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapsByRank)); + } + + /// + /// Initializes the singleton with a single merged typemap universe and per-assembly array maps. + /// + public static void Initialize ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyMap, + IReadOnlyDictionary?[][]? arrayMapsByUniverseAndRank) + { + ArgumentNullException.ThrowIfNull (typeMap); + ArgumentNullException.ThrowIfNull (proxyMap); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapsByUniverseAndRank)); } /// /// Initializes the singleton with multiple per-assembly typemap universes and optional - /// merged per-rank array dictionaries. + /// per-universe array dictionaries. /// public static void Initialize ( IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) { - Initialize (typeMaps, proxyMaps, arrayMapsByRank: null); + Initialize (typeMaps, proxyMaps, arrayMapsByUniverseAndRank: null); } /// /// Initializes the singleton with multiple per-assembly typemap universes and optional - /// merged per-rank array dictionaries. + /// per-universe array dictionaries. /// - /// - /// 0-indexed by (rank - 1); null when no array entries were emitted. Same shape as - /// the single-universe overload — array entries live in the shared - /// __ArrayMapRank{N} TypeMap groups regardless of merge mode. - /// + /// Array maps indexed by universe, then by 0-based rank. public static void Initialize ( IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps, - IReadOnlyDictionary?[]? arrayMapsByRank) + IReadOnlyDictionary?[][]? arrayMapsByUniverseAndRank) { ArgumentNullException.ThrowIfNull (typeMaps); ArgumentNullException.ThrowIfNull (proxyMaps); @@ -101,22 +105,25 @@ public static void Initialize ( if (typeMaps.Length != proxyMaps.Length) { throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length}).", nameof (proxyMaps)); } + if (arrayMapsByUniverseAndRank is not null && arrayMapsByUniverseAndRank.Length != typeMaps.Length) { + throw new ArgumentException ($"arrayMapsByUniverseAndRank.Length ({arrayMapsByUniverseAndRank.Length}) must equal typeMaps.Length ({typeMaps.Length}).", nameof (arrayMapsByUniverseAndRank)); + } var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { - universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); + universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i], arrayMapsByUniverseAndRank? [i]); } - InitializeCore (new AggregateTypeMap (universes), arrayMapsByRank); + InitializeCore (new AggregateTypeMap (universes)); } - static void InitializeCore (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) + static void InitializeCore (ITypeMap typeMap) { lock (s_initLock) { if (s_instance is not null) { throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); } - s_instance = new TrimmableTypeMap (typeMap, arrayMapsByRank); + s_instance = new TrimmableTypeMap (typeMap); } } @@ -388,10 +395,7 @@ internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? ? TryGetPrimitiveJniName (leaf, out var p) ? p : null : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; - if (leafJniName is not null && - (uint)rankIndex < (uint)_arrayMapsByRank.Length && - _arrayMapsByRank [rankIndex] is { } dict && - dict.TryGetValue (leafJniName, out arrayType)) { + if (leafJniName is not null && _typeMap.TryGetArrayType (leafJniName, rankIndex, out arrayType)) { return true; } @@ -469,18 +473,3 @@ public MissingJavaPeerProxy () : base ("", typeof (Java.Lang.Object), n } } - -// Shared TypeMap group anchors for per-rank array entries. Per-assembly typemap DLLs -// reference these so all rank-N entries across all assemblies merge into a single -// dictionary at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>(). -// -// To support a higher MaxArrayRank, add additional types here and bump -// TrimmableTypeMapGenerator.MaxSupportedArrayRank. -internal sealed class __ArrayMapRank1 { } -internal sealed class __ArrayMapRank2 { } -internal sealed class __ArrayMapRank3 { } -internal sealed class __ArrayMapRank4 { } -internal sealed class __ArrayMapRank5 { } -internal sealed class __ArrayMapRank6 { } -internal sealed class __ArrayMapRank7 { } -internal sealed class __ArrayMapRank8 { } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 89bf52ac3b1..ea0c64a2ace 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -361,7 +361,7 @@ - + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 0d76ff22453..6f9ca13b9f5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -332,24 +332,32 @@ public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () Assert.True (pe.HasMetadata); } - [Fact] - public void Generate_MergedMode_WithArrays_ReferencesPerAsmRankSentinels () + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Generate_WithArrays_ReferencesPerAsmRankSentinels (bool useSharedTypemapUniverse) { using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], - useSharedTypemapUniverse: true, maxArrayRank: 2); + useSharedTypemapUniverse: useSharedTypemapUniverse, maxArrayRank: 2); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var typeRefNames = reader.TypeReferences - .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + var rankRefsByAssembly = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Where (t => reader.GetString (t.Name).StartsWith ("__ArrayMapRank", StringComparison.Ordinal)) + .Select (t => ( + Name: reader.GetString (t.Name), + Assembly: reader.GetString (reader.GetAssemblyReference ((AssemblyReferenceHandle) t.ResolutionScope).Name))) .ToList (); - Assert.Contains ("__ArrayMapRank1", typeRefNames); - Assert.Contains ("__ArrayMapRank2", typeRefNames); - Assert.DoesNotContain ("__ArrayMapRank3", typeRefNames); + Assert.Contains (("__ArrayMapRank1", "_App.TypeMap"), rankRefsByAssembly); + Assert.Contains (("__ArrayMapRank2", "_App.TypeMap"), rankRefsByAssembly); + Assert.Contains (("__ArrayMapRank1", "_Mono.Android.TypeMap"), rankRefsByAssembly); + Assert.Contains (("__ArrayMapRank2", "_Mono.Android.TypeMap"), rankRefsByAssembly); + Assert.DoesNotContain (rankRefsByAssembly, r => r.Name == "__ArrayMapRank3"); } [Fact] - public void Generate_MergedMode_WithArrays_NoPerAsmAccessNeeded () + public void Generate_MergedMode_WithArrays_HasIgnoresAccessChecksToArrayAssemblies () { using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: true, maxArrayRank: 3); @@ -358,7 +366,7 @@ public void Generate_MergedMode_WithArrays_NoPerAsmAccessNeeded () var accessAttrs = GetIgnoresAccessChecksToValues (reader); Assert.Contains ("Mono.Android", accessAttrs); - // Shared-mode root never needs per-asm internal access — rank anchors live in Mono.Android. - Assert.DoesNotContain ("_App.TypeMap", accessAttrs); + Assert.Contains ("_App.TypeMap", accessAttrs); + Assert.Contains ("_Mono.Android.TypeMap", accessAttrs); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 049bc74a654..ee29afa9466 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1022,7 +1022,7 @@ public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () public class ArrayEntriesPeBlob { [Fact] - public void FullPipeline_ArrayEntries_ReferencesSharedRankAnchors () + public void FullPipeline_ArrayEntries_DefinesPrivateRankAnchors () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); @@ -1030,21 +1030,19 @@ public void FullPipeline_ArrayEntries_ReferencesSharedRankAnchors () Assert.Equal (3, model.MaxArrayRank); EmitAndVerify (model, "ArrSentinels", (pe, reader) => { - // Per-asm DLLs no longer define their own __ArrayMapRank{N}; they reference - // the shared anchors in Mono.Android. var typeDefNames = reader.TypeDefinitions .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) .ToHashSet (StringComparer.Ordinal); - Assert.DoesNotContain ("__ArrayMapRank1", typeDefNames); + Assert.Contains ("__ArrayMapRank1", typeDefNames); + Assert.Contains ("__ArrayMapRank2", typeDefNames); + Assert.Contains ("__ArrayMapRank3", typeDefNames); - var rankRefsToMonoAndroid = reader.TypeReferences + var rankTypeRefs = reader.TypeReferences .Select (h => reader.GetTypeReference (h)) .Where (t => reader.GetString (t.Name).StartsWith ("__ArrayMapRank", StringComparison.Ordinal)) .Select (t => reader.GetString (t.Name)) .ToHashSet (StringComparer.Ordinal); - Assert.Contains ("__ArrayMapRank1", rankRefsToMonoAndroid); - Assert.Contains ("__ArrayMapRank2", rankRefsToMonoAndroid); - Assert.Contains ("__ArrayMapRank3", rankRefsToMonoAndroid); + Assert.Empty (rankTypeRefs); }); } @@ -1057,6 +1055,13 @@ public void FullPipeline_NoArrayEntries_DoesNotReferenceRankAnchors () Assert.Equal (0, model.MaxArrayRank); EmitAndVerify (model, "NoArrSentinels", (pe, reader) => { + var typeDefNames = reader.TypeDefinitions + .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) + .ToHashSet (StringComparer.Ordinal); + Assert.DoesNotContain ("__ArrayMapRank1", typeDefNames); + Assert.DoesNotContain ("__ArrayMapRank2", typeDefNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeDefNames); + var typeRefNames = reader.TypeReferences .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) .ToHashSet (StringComparer.Ordinal); From 1b7c6296848dc532d3e1cc3d308caf7cf2029d56 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 11:19:12 +0200 Subject: [PATCH 4/4] [TrimmableTypeMap] Add array rank target attributes Emit TypeMapAssemblyTargetAttribute entries for each per-assembly array rank anchor so NativeAOT TypeMapping sees the same private rank groups requested by TypeMapLoader.Initialize. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 19 +++++++ .../RootTypeMapAssemblyGeneratorTests.cs | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index dfa61a977c8..5d5f7434d49 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -121,6 +121,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha } else { EmitPerAssemblyUniverseAssemblyTargetAttributes (pe, perAssemblyTypeMapNames); } + EmitArrayAssemblyTargetAttributes (pe, perAssemblyTypeMapNames, maxArrayRank); // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access // internal types (TrimmableTypeMap and friends in Mono.Android, and private anchors @@ -158,6 +159,24 @@ static void EmitPerAssemblyUniverseAssemblyTargetAttributes (PEAssemblyBuilder p } } + static void EmitArrayAssemblyTargetAttributes (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, int maxArrayRank) + { + if (maxArrayRank == 0) { + return; + } + + var openAttrRef = GetTypeMapAssemblyTargetAttributeRef (pe); + foreach (var name in perAssemblyTypeMapNames) { + var asmRef = pe.FindOrAddAssemblyRef (name); + for (int rank = 1; rank <= maxArrayRank; rank++) { + var rankAnchorRef = pe.Metadata.AddTypeReference (asmRef, + default, pe.Metadata.GetOrAddString ($"__ArrayMapRank{rank}")); + var ctorRef = GetTypeMapAssemblyTargetAttributeCtorRef (pe, openAttrRef, rankAnchorRef); + EmitAssemblyTargetAttribute (pe, ctorRef, name); + } + } + } + static TypeReferenceHandle GetTypeMapAssemblyTargetAttributeRef (PEAssemblyBuilder pe) { return pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 6f9ca13b9f5..7de6aec0679 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -135,6 +135,25 @@ public void Generate_MergedMode_TargetAttributesUseSharedAnchor () }, targetAttributes); } + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Generate_WithArrays_TargetAttributesUsePerAssemblyRankAnchors (bool useSharedTypemapUniverse) + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + using var stream = GenerateRootAssembly (targets, useSharedTypemapUniverse, maxArrayRank: 2); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var targetAttributes = GetTypeMapAssemblyTargetAttributeTargetTypes (reader); + + Assert.Contains (("_App.TypeMap", "_App.TypeMap", "__ArrayMapRank1"), targetAttributes); + Assert.Contains (("_App.TypeMap", "_App.TypeMap", "__ArrayMapRank2"), targetAttributes); + Assert.Contains (("_Mono.Android.TypeMap", "_Mono.Android.TypeMap", "__ArrayMapRank1"), targetAttributes); + Assert.Contains (("_Mono.Android.TypeMap", "_Mono.Android.TypeMap", "__ArrayMapRank2"), targetAttributes); + Assert.DoesNotContain (targetAttributes, t => t.GenericArgumentTypeName == "__ArrayMapRank3"); + } + static List GetTypeMapAssemblyTargetAttributes (MetadataReader reader) { var result = new List (); @@ -170,6 +189,25 @@ static List GetTypeMapAssemblyTargetAttributes (MetadataReader return result; } + static List<(string TargetName, string GenericArgumentScope, string GenericArgumentTypeName)> GetTypeMapAssemblyTargetAttributeTargetTypes (MetadataReader reader) + { + var result = new List<(string TargetName, string GenericArgumentScope, string GenericArgumentTypeName)> (); + foreach (var attr in GetTypeMapAssemblyTargetAttributes (reader)) { + var targetName = GetTypeMapAssemblyTargetName (reader, attr); + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)attr.Constructor); + var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle)memberRef.Parent); + var blob = reader.GetBlobReader (typeSpec.Signature); + Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + blob.ReadCompressedInteger (); // TypeMapAssemblyTargetAttribute`1 type + Assert.Equal (1, blob.ReadCompressedInteger ()); + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + var targetType = DecodeTypeDefOrRefOrSpec (blob.ReadCompressedInteger ()); + result.Add ((targetName, GetResolutionScopeName (reader, targetType), GetTypeName (reader, targetType))); + } + return result; + } + static string GetTypeMapAssemblyTargetName (MetadataReader reader, CustomAttribute attr) { var blob = reader.GetBlobReader (attr.Value); @@ -211,6 +249,17 @@ static string GetResolutionScopeName (MetadataReader reader, EntityHandle handle }; } + static string GetTypeName (MetadataReader reader, EntityHandle handle) + { + if (handle.Kind == HandleKind.TypeDefinition) { + return reader.GetString (reader.GetTypeDefinition ((TypeDefinitionHandle)handle).Name); + } + if (handle.Kind != HandleKind.TypeReference) { + throw new InvalidOperationException ($"Unexpected type handle kind: {handle.Kind}"); + } + return reader.GetString (reader.GetTypeReference ((TypeReferenceHandle)handle).Name); + } + [Theory] [InlineData (true)] [InlineData (false)]