From bd207f4c0b63e283135ca1b07c1117527ebdaab6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 10:54:07 +0200 Subject: [PATCH 01/15] [TrimmableTypeMap] Generator: emit per-rank array TypeMap entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For every non-aliased, non-generic peer (excluding JNI primitive-keyword keys), the generator can now emit three speculative `TypeMap` entries keyed by the **element JNI name** and anchored to per-typemap-assembly `__ArrayMapRank{1,2,3}` sentinel TypeDefs: [assembly: TypeMap<__ArrayMapRank1>("java/lang/String", typeof(string[]), typeof(string[]))] [assembly: TypeMap<__ArrayMapRank2>("java/lang/String", typeof(string[][]), typeof(string[][]))] [assembly: TypeMap<__ArrayMapRank3>("java/lang/String", typeof(string[][][]), typeof(string[][][]))] The trim target is the closed array type itself, so ILC's per-shape conditional drops entries when the array shape is never constructed — validated on disjoint-set NativeAOT tests for both reference and value type element peers. * `Generator/Model/TypeMapAssemblyData.cs`: * `TypeMapAssemblyData.RankSentinels` (nullable `RankSentinelNames`) — when set, the emitter generates the three sentinel TypeDefs. * `TypeMapAttributeData.AnchorRank` (nullable int) — when set, overrides the model-level default anchor with the rank-{value} sentinel from the same assembly. * `Generator/TypeMapAssemblyEmitter.cs`: * `EmitRankSentinels` mirrors the existing `__TypeMapAnchor` pattern. * Per-anchor `TypeMap` 3-arg ctor refs are now built and cached lazily by anchor handle (`GetOrAddTypeMapAttr3ArgCtorRef`). * `EmitTypeMapAttribute` resolves rank-anchored entries to the local sentinel handle. * `Generator/ModelBuilder.cs`: * `Build` takes a new `bool emitArrayEntries` (default false). When true, sets `RankSentinels` and routes each peer through `EmitArrayEntries`, which produces the per-rank trio. * Skip rules: open-generic peers, JNI primitive-keyword keys, alias groups (would produce duplicate keys; deferred). * `tests/.../TypeMapModelBuilderTests.cs`: 14 new tests covering the default-off behavior, default sentinel names, per-rank-trio emission, element-only key, closed-array trim target, conditional-only entries, open-generic / alias / primitive-keyword skip rules, multiple-peer isolation, and PE blob round-trip (sentinels emitted, attribute blobs survive). Tracking: #11234 Phase 2 (arrays-only, container types stay untouched). Runtime wiring + MSBuild flag follow in subsequent commits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 54 +++++ .../Generator/ModelBuilder.cs | 78 ++++++- .../Generator/TypeMapAssemblyEmitter.cs | 100 ++++++++- .../Generator/TypeMapModelBuilderTests.cs | 211 ++++++++++++++++++ 4 files changed, 436 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 679423576f2..c744ea2ef11 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -40,12 +40,52 @@ sealed class TypeMapAssemblyData /// public List AliasHolders { get; } = new (); + /// + /// When non-null, the emitter will produce three additional internal + /// __ArrayMapRank{N} sealed type definitions to serve as the TGroup + /// arguments for per-rank array entries. + /// Each in whose + /// is non-null will reference one of + /// these generated TypeDefs via its rank index. + /// + /// + /// Mirrors the existing __TypeMapAnchor emission pattern in + /// : in per-assembly mode (Debug) each typemap + /// dll owns its own rank sentinels; in shared-universe mode (Release) the single + /// merged dll owns one set. The runtime TypeMapLoader queries each per-assembly + /// sentinel via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>() + /// to build the per-rank dictionaries passed to TrimmableTypeMap.Initialize. + /// + public RankSentinelNames? RankSentinels { get; set; } + /// /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. /// public List IgnoresAccessChecksTo { get; } = new (); } +/// +/// Names of the per-typemap-assembly array-rank sentinel types that the emitter generates. +/// All three live in the typemap assembly's root namespace. +/// +sealed record RankSentinelNames (string Rank1, string Rank2, string Rank3) +{ + /// + /// Default sentinel names used by . + /// + public static readonly RankSentinelNames Default = new ("__ArrayMapRank1", "__ArrayMapRank2", "__ArrayMapRank3"); + + /// + /// Returns the sentinel name for the given 1-based array rank, or null if rank is out of range. + /// + public string? GetForRank (int rank) => rank switch { + 1 => Rank1, + 2 => Rank2, + 3 => Rank3, + _ => null, + }; +} + /// /// One [assembly: TypeMap("jni/name", typeof(Proxy))] or /// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry. @@ -77,6 +117,20 @@ sealed record TypeMapAttributeData /// True for 2-arg unconditional entries (ACW types, essential runtime types). /// public bool IsUnconditional => TargetTypeReference == null; + + /// + /// When non-null, this entry uses the array-rank sentinel __ArrayMapRank{value} + /// from the same typemap assembly as its TGroup argument instead of the default + /// model-level anchor (__TypeMapAnchor / Java.Lang.Object). + /// + /// + /// Used by speculative array entries: + /// [assembly: TypeMap<__ArrayMapRank2>("java/lang/String", typeof(string[][]), typeof(string[][]))]. + /// The emitter resolves the rank to one of the names declared in + /// and emits a per-rank closed + /// TypeMapAttribute<TGroup> ctor reference. + /// + public int? AnchorRank { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 79570a14bda..e6ea873eafe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -40,7 +40,17 @@ static class ModelBuilder /// Scanned Java peer types (typically from a single input assembly). /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + /// + /// When true, additionally emit speculative [L<jni>;-shaped TypeMap entries + /// for ranks 1–3 keyed by the element JNI name, with per-rank __ArrayMapRank{N} + /// sentinel TGroup types generated into the same assembly. The runtime + /// TrimmableTypeMap uses these to satisfy JNIEnv.ArrayCreateInstance under + /// NativeAOT (where dynamic Array.CreateInstance isn't available). Should be + /// gated on $(PublishAot) == true by the MSBuild task — under CoreCLR/Mono the + /// runtime falls through to Array.CreateInstance directly and these entries are + /// never queried, so emitting them just adds dead-weight attribute metadata. + /// + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, bool emitArrayEntries = false) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -56,6 +66,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri AssemblyName = assemblyName, ModuleName = moduleName, }; + if (emitArrayEntries) { + model.RankSentinels = RankSentinelNames.Default; + } // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. @@ -89,6 +102,10 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); + + if (emitArrayEntries) { + EmitArrayEntries (model, jniName, peersForName); + } } // Compute IgnoresAccessChecksTo from cross-assembly references @@ -392,4 +409,63 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; + + const int MaxArrayRank = 3; + + /// + /// Emits speculative per-rank [L<jni>;-shaped + /// entries (ranks 1–3) for one peer group, keyed by the element JNI name and + /// anchored to the __ArrayMapRank{N} sentinel emitted into the same typemap + /// assembly. Each entry has the closed managed array type as both proxy and trim target, + /// so ILC's per-shape conditional drops the entry when the array shape is never + /// constructed. + /// + /// + /// Skips: + /// + /// Open-generic peers — typeof(T<>[]) is not a valid IL token. + /// JNI primitive-keyword keys (Z, B, C, S, + /// I, J, F, D) — primitive arrays are handled by the + /// legacy JniRuntime.JniTypeManager.GetPrimitiveArrayTypesForSimpleReference + /// path; emitting array entries here would shadow that built-in handling. + /// Alias groups (multiple peers sharing one JNI name) — would produce + /// duplicate keys in the per-rank dictionary; deferred until a real-world need + /// motivates an alias-aware design. + /// + /// + static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List peersForName) + { + if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) { + return; + } + if (peersForName.Count != 1) { + return; + } + + var peer = peersForName [0]; + if (peer.IsGenericDefinition) { + return; + } + + for (int rank = 1; rank <= MaxArrayRank; rank++) { + string arrayTypeRef = AssemblyQualify (peer.ManagedTypeName + Brackets (rank), peer.AssemblyName); + model.Entries.Add (new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = arrayTypeRef, + TargetTypeReference = arrayTypeRef, + AnchorRank = rank, + }); + } + } + + static string Brackets (int rank) => rank switch { + 1 => "[]", + 2 => "[][]", + 3 => "[][][]", + _ => string.Concat (Enumerable.Repeat ("[]", rank)), + }; + + static bool IsJniPrimitiveKeyword (char c) + => c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I' + || c == 'J' || c == 'F' || c == 'D' || c == 'V'; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 6f8647dcabe..3da5a3741c0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -119,6 +119,20 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; + // Per-rank array sentinel TypeDefs (1-indexed; index 0 unused). Populated when + // model.RankSentinels is non-null. Each rank-anchored TypeMap entry uses one of + // these as its TGroup token (e.g. typeof(__ArrayMapRank2)). + readonly EntityHandle[] _rankAnchorHandles = new EntityHandle [4]; + + // Per-anchor 3-arg TypeMap(string, Type, Type) ctor refs, keyed by anchor + // EntityHandle. Built lazily by GetOrAddTypeMapAttr3ArgCtorRef. The default-anchor + // 3-arg ctor (= _typeMapAttrCtorRef3Arg) is also stored here so all callers go + // through the same cache. + readonly Dictionary _typeMapAttr3ArgCtorRefByAnchor = new (); + + // Cached open TypeMapAttribute`1 ref so per-anchor closed TypeSpecs share metadata. + TypeReferenceHandle _typeMapAttrOpenRef; + /// /// Creates a new emitter. /// @@ -168,6 +182,7 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) } else { EmitAnchorType (); } + EmitRankSentinels (model); EmitMemberReferences (); // Track wrapper method names → handles for RegisterNatives @@ -265,6 +280,40 @@ void EmitAnchorType () MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); } + /// + /// Emits up to three internal __ArrayMapRank{1,2,3} classes used as the group + /// type parameters for speculative array-shape TypeMap<T> entries. Each + /// per-assembly typemap DLL emits its own set so the runtime TypeMapLoader + /// can collect per-assembly per-rank dictionaries via + /// TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>(). + /// No-op when is null. + /// + void EmitRankSentinels (TypeMapAssemblyData model) + { + if (model.RankSentinels is null) { + return; + } + + _rankAnchorHandles [1] = EmitRankSentinel (model.RankSentinels.Rank1); + _rankAnchorHandles [2] = EmitRankSentinel (model.RankSentinels.Rank2); + _rankAnchorHandles [3] = EmitRankSentinel (model.RankSentinels.Rank3); + } + + EntityHandle EmitRankSentinel (string typeName) + { + var metadata = _pe.Metadata; + var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); + + return metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, + default, + metadata.GetOrAddString (typeName), + objectRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + } + void EmitMemberReferences () { _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", @@ -387,13 +436,15 @@ void EmitMemberReferences () void EmitTypeMapAttributeCtorRef () { var metadata = _pe.Metadata; - var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + _typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAttribute`1")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, _anchorTypeHandle); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, _anchorTypeHandle); - // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional + // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional. Default anchor only; + // rank-anchored entries are always conditional (3-arg) so no per-rank 2-arg ctor is + // needed today. _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), @@ -402,8 +453,30 @@ void EmitTypeMapAttributeCtorRef () p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable - _typeMapAttrCtorRef3Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable. + // Cache by anchor so rank-anchored entries can build their own closed ctor on demand. + _typeMapAttrCtorRef3Arg = AddTypeMapAttr3ArgCtorRef (_anchorTypeHandle); + _typeMapAttr3ArgCtorRefByAnchor [_anchorTypeHandle] = _typeMapAttrCtorRef3Arg; + } + + /// + /// Returns the cached 3-arg TypeMap<TGroup> ctor ref for the given + /// anchor (group) type, building and caching it if not yet present. + /// + MemberReferenceHandle GetOrAddTypeMapAttr3ArgCtorRef (EntityHandle anchor) + { + if (_typeMapAttr3ArgCtorRefByAnchor.TryGetValue (anchor, out var cached)) { + return cached; + } + var ctorRef = AddTypeMapAttr3ArgCtorRef (anchor); + _typeMapAttr3ArgCtorRefByAnchor [anchor] = ctorRef; + return ctorRef; + } + + MemberReferenceHandle AddTypeMapAttr3ArgCtorRef (EntityHandle anchor) + { + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, anchor); + return _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, rt => rt.Void (), p => { @@ -1237,7 +1310,22 @@ void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle) void EmitTypeMapAttribute (TypeMapAttributeData entry) { - var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + MemberReferenceHandle ctorRef; + if (entry.AnchorRank is int rank) { + if (entry.IsUnconditional) { + throw new InvalidOperationException ( + $"Rank-anchored TypeMap entries must be conditional (3-arg). Entry '{entry.JniName}' rank={rank}."); + } + if (rank < 1 || rank > 3 || _rankAnchorHandles [rank] == default) { + throw new InvalidOperationException ( + $"No rank-{rank} anchor TypeDef was emitted for entry '{entry.JniName}'. " + + $"Ensure TypeMapAssemblyData.RankSentinels was set before emit."); + } + ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [rank]); + } else { + ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + } + var blob = _pe.BuildAttributeBlob (b => { b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 651f1ea3c40..eb9cdd2d449 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -16,6 +16,12 @@ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string return ModelBuilder.Build (peers, outputPath, assemblyName); } + static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null) + { + var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); + return ModelBuilder.Build (peers, outputPath, assemblyName, emitArrayEntries: true); + } + public class BasicStructure { [Fact] @@ -845,6 +851,211 @@ public void Build_SameInput_ProducesDeterministicOutput () } } + public class ArrayEntries + { + [Fact] + public void Build_DefaultEmitArrayEntriesFalse_NoArrayEntries () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Null (model.RankSentinels); + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_SetsDefaultRankSentinels () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + Assert.NotNull (model.RankSentinels); + Assert.Equal ("__ArrayMapRank1", model.RankSentinels!.Rank1); + Assert.Equal ("__ArrayMapRank2", model.RankSentinels.Rank2); + Assert.Equal ("__ArrayMapRank3", model.RankSentinels.Rank3); + } + + [Fact] + public void Build_EmitArrayEntries_EmitsRanks1Through3 () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); + Assert.Equal (3, arrayEntries.Count); + Assert.Equal (new int? [] { 1, 2, 3 }, arrayEntries.Select (e => e.AnchorRank).ToArray ()); + Assert.All (arrayEntries, e => Assert.Equal ("foo/Bar", e.JniName)); + } + + [Fact] + public void Build_EmitArrayEntries_KeyIsElementJniName () + { + // No "[L...;" prefix at runtime — the key is the bare element JNI name and rank + // is encoded by which sentinel anchor (TGroup) the entry uses. + var peer = MakeMcwPeer ("java/lang/String", "System.String", "System.Runtime"); + var model = BuildModelWithArrays (new [] { peer }); + + var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); + Assert.All (arrayEntries, e => Assert.Equal ("java/lang/String", e.JniName)); + Assert.All (arrayEntries, e => Assert.False (e.JniName.StartsWith ("[", StringComparison.Ordinal))); + } + + [Fact] + public void Build_EmitArrayEntries_TrimTargetIsClosedArrayType () + { + // 3rd ctor arg = the closed array type itself, so ILC's per-shape conditional + // drops the entry when the array shape is never constructed. + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + var rank1 = model.Entries.Single (e => e.AnchorRank == 1); + Assert.Equal ("Foo.Bar[], App", rank1.ProxyTypeReference); + Assert.Equal ("Foo.Bar[], App", rank1.TargetTypeReference); + var rank2 = model.Entries.Single (e => e.AnchorRank == 2); + Assert.Equal ("Foo.Bar[][], App", rank2.ProxyTypeReference); + Assert.Equal ("Foo.Bar[][], App", rank2.TargetTypeReference); + var rank3 = model.Entries.Single (e => e.AnchorRank == 3); + Assert.Equal ("Foo.Bar[][][], App", rank3.ProxyTypeReference); + Assert.Equal ("Foo.Bar[][][], App", rank3.TargetTypeReference); + } + + [Fact] + public void Build_EmitArrayEntries_AllConditional () + { + // 2-arg unconditional makes no sense for arrays — the trim conditioning on the + // array shape is the whole point. + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + foreach (var entry in model.Entries.Where (e => e.AnchorRank is not null)) { + Assert.False (entry.IsUnconditional); + Assert.NotNull (entry.TargetTypeReference); + } + } + + [Fact] + public void Build_EmitArrayEntries_OpenGenericPeer_Skipped () + { + // typeof(JavaList<>[]) is not a valid IL token. + var openGeneric = MakeMcwPeer ("java/util/ArrayList", "Android.Runtime.JavaList`1", "Mono.Android") + with { IsGenericDefinition = true }; + var model = BuildModelWithArrays (new [] { openGeneric }); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_AliasGroup_Skipped () + { + // Alias groups (multiple peers sharing one JNI name) would produce duplicate + // JNI array keys; deferred pending an alias-aware design. + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "App"), + MakeMcwPeer ("test/Dup", "Test.Second", "App"), + }; + var model = BuildModelWithArrays (peers); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Theory] + [InlineData ("Z")] + [InlineData ("B")] + [InlineData ("C")] + [InlineData ("S")] + [InlineData ("I")] + [InlineData ("J")] + [InlineData ("F")] + [InlineData ("D")] + public void Build_EmitArrayEntries_PrimitiveJniKeyword_Skipped (string jniKeyword) + { + // Primitive JNI keyword keys are handled by the legacy + // JniRuntime.JniTypeManager.GetPrimitiveArrayTypesForSimpleReference path. + // Emitting array entries here would shadow that built-in handling. + var peer = MakeMcwPeer (jniKeyword, "FakePrimitive.Wrapper", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () + { + var peers = new List { + MakeMcwPeer ("foo/A", "Foo.A", "App"), + MakeMcwPeer ("foo/B", "Foo.B", "App"), + }; + var model = BuildModelWithArrays (peers); + + var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); + Assert.Equal (6, arrayEntries.Count); // 2 peers × 3 ranks + + foreach (var jni in new [] { "foo/A", "foo/B" }) { + var perPeer = arrayEntries.Where (e => e.JniName == jni).OrderBy (e => e.AnchorRank).ToList (); + Assert.Equal (3, perPeer.Count); + Assert.Equal (new int? [] { 1, 2, 3 }, perPeer.Select (e => e.AnchorRank).ToArray ()); + } + } + } + + public class ArrayEntriesPeBlob + { + [Fact] + public void FullPipeline_ArrayEntries_EmitsSentinelTypeDefs () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrSentinels", emitArrayEntries: true); + Assert.NotNull (model.RankSentinels); + + EmitAndVerify (model, "ArrSentinels", (pe, reader) => { + var typeNames = reader.TypeDefinitions + .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) + .ToHashSet (StringComparer.Ordinal); + + Assert.Contains ("__ArrayMapRank1", typeNames); + Assert.Contains ("__ArrayMapRank2", typeNames); + Assert.Contains ("__ArrayMapRank3", typeNames); + }); + } + + [Fact] + public void FullPipeline_NoArrayEntries_DoesNotEmitSentinelTypeDefs () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "NoArrSentinels.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "NoArrSentinels"); + Assert.Null (model.RankSentinels); + + EmitAndVerify (model, "NoArrSentinels", (pe, reader) => { + var typeNames = reader.TypeDefinitions + .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) + .ToHashSet (StringComparer.Ordinal); + + Assert.DoesNotContain ("__ArrayMapRank1", typeNames); + Assert.DoesNotContain ("__ArrayMapRank2", typeNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeNames); + }); + } + + [Fact] + public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "ArrBlobs.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", emitArrayEntries: true); + + EmitAndVerify (model, "ArrBlobs", (pe, reader) => { + var attrs = ReadAllTypeMapAttributeBlobs (reader); + + // Three array entries should round-trip with the same JNI key + array trim targets. + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[], App"); + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][], App"); + Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][][], App"); + }); + } + } + static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) { var stream = new MemoryStream (); From 956a9994ee32d9b6085d4b85d79673400af0e2be Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 11:27:57 +0200 Subject: [PATCH 02/15] [TrimmableTypeMap] Runtime: array typemap fork on IsDynamicCodeSupported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `JNIEnv.ArrayCreateInstance` now branches on `RuntimeFeature.IsDynamicCodeSupported`: * CoreCLR / Mono (true) — `Array.CreateInstance(elementType, length)`. No typemap roundtrip; supports unlimited array rank. * NativeAOT (false) — typemap lookup → AOT-safe `Array.CreateInstanceFromArrayType`. Capped at the emitted ranks (1–3); miss throws `NotSupportedException` with diagnostic. The runtime fork lets us avoid emitting (and paying for) speculative array TypeMap entries on CoreCLR-only builds, where the runtime type loader can construct any `T[]` dynamically anyway. * `ITypeMapWithAliasing.TryGetArrayType(string jniElementTypeName, int rank, out Type? arrayType)` — new abstraction for the per-rank array dictionary lookup. `SingleUniverseTypeMap` carries three nullable `IReadOnlyDictionary?` fields (rank 1, 2, 3) populated at `TrimmableTypeMap.Initialize` time; `AggregateTypeMap` does first-wins iteration. * `TrimmableTypeMap.TryGetArrayType(Type elementType, out Type?)` — walks down `elementType.IsArray` / `GetElementType()` to find the leaf type and array depth, resolves the leaf JNI element name (primitive static dict OR `TryGetJniNameForManagedType` wrapped), and delegates the (jni, rank+1) lookup to the interface. * `TrimmableTypeMap.Initialize` gains 5-arg overloads (single + aggregate) accepting the per-rank dicts. Existing 2-arg overloads stay as wrappers passing null per-rank dicts so older generated assemblies keep working. * `RootTypeMapAssemblyGenerator`: the generated `TypeMapLoader.Initialize` IL now branches on the new `emitArrayEntries` flag. When true, it collects per-assembly `__ArrayMapRank{1,2,3}` sentinels via `TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>()` and passes the resulting dicts to the 5-arg `TrimmableTypeMap.Initialize`. Aggregate (Debug) path is fully implemented; merged-universe (Release) path throws at generation time with a clear message — wiring the shared-universe array sentinels is a small follow-up. * `GenerateTrimmableTypeMap` MSBuild task: new `EmitArrayEntries` property forwarded through `TrimmableTypeMapGenerator.Execute` and `RootTypeMapAssemblyGenerator.Generate`. SDK target sets it to `$(PublishAot)`. * `JavaPeerContainerFactory.CreateArray` and `CreateHigherRankArray` deleted. Container methods (`CreateList`, `CreateCollection`, `CreateDictionary*`) stay untouched — those are tracked separately in #11234. Validation: * 445 / 445 generator unit tests pass. * Trimmable + CoreCLR `RunTestApp` lane on emulator: **917 total / 0 errors / 3 failures** (pre-existing `TryGetJniNameForManagedType_*`, called out as out-of-scope in #11225). No regression. * The NativeAOT branch path is gated on dotnet/runtime#126380 (ships in .NET 11 nightly preview.5+); validated with the playground repro separately. Tracking: #11234 Phase 2 (arrays only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 216 +++++++++++++++++- .../Generator/TypeMapAssemblyGenerator.cs | 9 +- .../TrimmableTypeMapGenerator.cs | 11 +- src/Mono.Android/Android.Runtime/JNIEnv.cs | 24 +- .../Java.Interop/JavaPeerContainerFactory.cs | 42 +--- .../AggregateTypeMap.cs | 13 ++ .../ITypeMapWithAliasing.cs | 20 ++ .../SingleUniverseTypeMap.cs | 30 +++ .../TrimmableTypeMap.cs | 130 ++++++++++- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 12 +- 11 files changed, 454 insertions(+), 54 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index d6b7bae9a22..41debf2944d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -69,10 +69,17 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// Names of per-assembly typemap assemblies to reference. /// True to merge all assemblies into a single typemap universe, false for per-assembly universes. + /// + /// True when the per-assembly typemaps were generated with rank sentinels + /// (__ArrayMapRank{1,2,3}). The generated TypeMapLoader.Initialize + /// will then build per-rank dictionaries from each typemap and pass them to the + /// 5-arg TrimmableTypeMap.Initialize overload. Should match the + /// emitArrayEntries flag passed to the per-assembly typemap generators. + /// /// Stream to write the output PE to. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). /// Optional module name for the PE metadata. - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null) + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, bool emitArrayEntries = false) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -121,7 +128,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, emitArrayEntries); pe.WritePE (stream); } @@ -145,7 +152,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc } } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, bool emitArrayEntries) { var metadata = pe.Metadata; @@ -181,16 +188,207 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); if (useSharedTypemapUniverse) { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + if (emitArrayEntries) { + // 5-arg Initialize(typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3) + var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, getExternalMemberRef, initializeRef, getProxyMemberRef); + } else { + // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + } } else { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) - var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + + if (emitArrayEntries) { + var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + } else { + // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) + var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + } + } + } + + /// + /// Emits IL for the merged-universe + array-entries case. Calls + /// TypeMapping.GetOrCreateExternalTypeMapping<T> five times — once each + /// for the proxy-map anchor, external-map anchor, and the three rank sentinels — + /// and passes all five results to the 5-arg TrimmableTypeMap.Initialize. + /// + static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle initializeRef, + MemberReferenceHandle getProxyMemberRef) + { + // In merged-universe mode all per-assembly typemaps share the same anchor + // (Java.Lang.Object). The rank sentinels live in each per-assembly typemap + // dll, but in shared-universe mode there's only one such dll (the merged + // one) — and the merged one's rank sentinels are referenced via the same + // per-assembly anchor namespace. However, the merged dll's rank sentinels + // are emitted there as TypeDefs, so we need a TypeRef from this root dll + // back to those sentinels. + // + // In practice, when useSharedTypemapUniverse is true, perAssemblyTypeMapNames + // has exactly one entry (the merged dll). The sentinels are emitted into + // that dll. We'd need that dll's name to construct TypeRefs. + // + // For now, the merged-universe + array-entries path is unsupported because + // the existing single-universe loader doesn't iterate perAssemblyTypeMapNames. + // The aggregate path covers both Debug (per-assembly) and the future + // merged-but-with-array case. Throw at generation time so misconfiguration + // surfaces immediately. + throw new NotSupportedException ( + "Merged-universe (Release) + array typemap entries is not yet wired up. " + + "The aggregate/per-assembly path covers both Debug and the (future) merged case; " + + "callers should use that path when emitArrayEntries is true."); + } + + /// + /// Emits IL for the per-assembly + array-entries case. Builds five + /// IReadOnlyDictionary<...>[] arrays (typeMaps, proxyMaps, + /// arrayMapsRank1, arrayMapsRank2, arrayMapsRank3) by querying each + /// per-assembly typemap's __TypeMapAnchor + __ArrayMapRank{1,2,3} + /// sentinels via TypeMapping.GetOrCreateExternalTypeMapping<T>, then + /// passes them to the 5-arg TrimmableTypeMap.Initialize. + /// + static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle initializeRef, + TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var count = perAssemblyTypeMapNames.Count; + + var getExternalSpecs = new EntityHandle [count]; + var getProxySpecs = new EntityHandle [count]; + var getArrayRank1Specs = new EntityHandle [count]; + var getArrayRank2Specs = new EntityHandle [count]; + var getArrayRank3Specs = new EntityHandle [count]; + for (int i = 0; i < count; i++) { + var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); + var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, + default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); + var rank1Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank1")); + var rank2Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank2")); + var rank3Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank3")); + getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); + getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); + getArrayRank1Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank1Ref); + getArrayRank2Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank2Ref); + getArrayRank3Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank3Ref); + } + + pe.EmitBody ("Initialize", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // var typeMaps = new IReadOnlyDictionary[N]; (loc 0) + EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0); + EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0); + + // var proxyMaps = new IReadOnlyDictionary[N]; (loc 1) + EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); + EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); + + // var arrayMapsRank1 = new IReadOnlyDictionary[N]; (loc 2) + EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 2); + EmitFillArrayLocal (encoder, count, getArrayRank1Specs, slot: 2); + + // var arrayMapsRank2 = new IReadOnlyDictionary[N]; (loc 3) + EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 3); + EmitFillArrayLocal (encoder, count, getArrayRank2Specs, slot: 3); + + // var arrayMapsRank3 = new IReadOnlyDictionary[N]; (loc 4) + EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 4); + EmitFillArrayLocal (encoder, count, getArrayRank3Specs, slot: 4); + + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsRank1, arrayMapsRank2, arrayMapsRank3) + encoder.LoadLocal (0); + encoder.LoadLocal (1); + encoder.LoadLocal (2); + encoder.LoadLocal (3); + encoder.LoadLocal (4); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: localsSig => { + localsSig.WriteByte (0x07); // LOCAL_SIG + localsSig.WriteCompressedInteger (5); // count + for (int i = 0; i < 5; i++) { + // Each local: IReadOnlyDictionary[] + // Slot 1 (proxyMaps) is keyed by Type; the rest by string. + localsSig.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: i != 1); + } + }); + } + + static void EmitNewArrayLocal (InstructionEncoder encoder, int count, TypeSpecificationHandle elemSpec, int slot) + { + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (elemSpec); + encoder.StoreLocal (slot); + } + + static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHandle[] specs, int slot) + { + for (int i = 0; i < count; i++) { + encoder.LoadLocal (slot); + encoder.LoadConstantI4 (i); + encoder.OpCode (ILOpCode.Call); + encoder.Token (specs [i]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + } + + /// + /// Creates a MemberRef for the 5-arg single-universe TrimmableTypeMap.Initialize + /// (typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3). + /// All three array maps are nullable. + /// + static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (96); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (5); // parameter count + blob.WriteByte (0x01); // return type: void + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Creates a MemberRef for the 5-arg aggregate TrimmableTypeMap.Initialize + /// (typeMaps[], proxyMaps[], arrayMapsRank1[], arrayMapsRank2[], arrayMapsRank3[]). + /// + static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (96); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (5); // parameter count + blob.WriteByte (0x01); // return type: void + // Param 1: IReadOnlyDictionary[] + blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // Param 2: IReadOnlyDictionary[] + blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + // Params 3-5: IReadOnlyDictionary?[] + for (int i = 0; i < 3; i++) { + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); } + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); } static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 939689a43a5..99ee21a56c7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -27,9 +27,14 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false) + /// + /// When true, additionally emit per-rank array TypeMap entries (ranks 1–3) plus the + /// matching __ArrayMapRank{N} sentinel TypeDefs. Should be gated on + /// $(PublishAot) == true by the caller. + /// + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, bool emitArrayEntries = false) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, emitArrayEntries); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 07d689854bb..cb70d3b8b55 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -32,7 +32,8 @@ public TrimmableTypeMapResult Execute ( HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, - XDocument? manifestTemplate = null) + XDocument? manifestTemplate = null, + bool emitArrayEntries = false) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -48,7 +49,7 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, emitArrayEntries); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -139,7 +140,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, bool emitArrayEntries) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -168,14 +169,14 @@ List GenerateTypeMapAssemblies (List allPeers, string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, emitArrayEntries); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, emitArrayEntries: emitArrayEntries); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8104ee308a2..9dbc6399b81 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -27,9 +27,27 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { - var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - if (factory is not null) - return factory.CreateArray (length, 1); + if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { + // CoreCLR / Mono — runtime type loader can construct any T[] dynamically. + // No typemap roundtrip; supports unlimited array rank. The IL3050 analyzer + // warns this isn't AOT-safe; the surrounding `if (IsDynamicCodeSupported)` + // IS the live feature switch the trimmer uses to dead-code this branch under + // PublishAot=true. + #pragma warning disable IL3050 + return Array.CreateInstance (elementType, length); + #pragma warning restore IL3050 + } + + // NativeAOT — resolve the closed array type via the per-rank typemap + // (filled in at startup from generated __ArrayMapRank{N} sentinels) and + // construct via AOT-safe Array.CreateInstanceFromArrayType. + if (TrimmableTypeMap.Instance.TryGetArrayType (elementType, out var arrayType)) { + return Array.CreateInstanceFromArrayType (arrayType, length); + } + + throw new NotSupportedException ( + $"No TrimmableTypeMap array entry for element type '{elementType}'. " + + $"Add an [assembly: TypeMap] entry for the closed array type or report an issue."); } #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index 0f569b918bb..f9f14b587bb 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -10,16 +10,19 @@ namespace Java.Interop { /// - /// AOT-safe factory for creating typed containers (arrays, lists, collections, dictionaries) - /// without using MakeGenericType() or Array.CreateInstance(). + /// AOT-safe factory for creating typed containers (lists, collections, dictionaries) + /// without using MakeGenericType(). /// + /// + /// Array creation has moved out of this factory: + /// now branches on + /// + /// and uses either (CoreCLR / Mono) or + /// the per-rank trimmable typemap + + /// (NativeAOT). + /// public abstract class JavaPeerContainerFactory { - /// - /// Creates a typed jagged array. Rank 1 = T[], rank 2 = T[][], etc. - /// - internal abstract Array CreateArray (int length, int rank); - /// /// Creates a typed JavaList<T> from a JNI handle. /// @@ -71,31 +74,6 @@ public sealed class JavaPeerContainerFactory< JavaPeerContainerFactory () { } - // TODO (https://github.com/dotnet/android/issues/10794): This might cause unnecessary code bloat for NativeAOT. Revisit - // how we use this API and instead use a differnet approach that uses AOT-safe `Array.CreateInstanceFromArrayType` - // with statically provided array types based on a statically known array type. - internal override Array CreateArray (int length, int rank) => rank switch { - 1 => new T [length], - 2 => new T [length][], - 3 => new T [length][][], - _ when rank >= 0 => CreateHigherRankArray (length, rank), - _ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Rank must be non-negative."), - }; - - static Array CreateHigherRankArray (int length, int rank) - { - if (!RuntimeFeature.IsDynamicCodeSupported) { - throw new NotSupportedException ($"Cannot create array of rank {rank} because dynamic code is not supported."); - } - - var arrayType = typeof (T); - for (int i = 0; i < rank; i++) { - arrayType = arrayType.MakeArrayType (); - } - - return Array.CreateInstanceFromArrayType (arrayType, length); - } - internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) => new Android.Runtime.JavaList (handle, transfer); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs index 83925abd7c6..cb5b980c2f0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -41,4 +41,17 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } + + public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) + { + // First-wins: each (peer, rank) pair has its TypeMap entry in exactly one + // assembly. Walk the universes and stop at the first hit. + foreach (var universe in _universes) { + if (universe.TryGetArrayType (jniElementTypeName, rank, 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/ITypeMapWithAliasing.cs index 0849741e96d..c458b0ce00d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -26,4 +26,24 @@ interface ITypeMapWithAliasing /// carries the attribute). /// bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); + + /// + /// Looks up the closed managed array type for a given element JNI name and rank. + /// E.g. ("java/lang/String", 2)typeof(string[][]). + /// + /// + /// The JNI name of the array element type (the bare element name, NOT the JNI array + /// form — no leading '['). E.g. "java/lang/String", not + /// "[Ljava/lang/String;". + /// + /// 1-based array rank. Supported values: 1, 2, 3. + /// The closed managed array type on success. + /// True when an entry exists for the (element, rank) pair. + /// + /// Returns false when no per-rank dictionary was supplied at initialization + /// (e.g. CoreCLR builds with $(PublishAot) == false) — the runtime fork + /// in JNIEnv.ArrayCreateInstance short-circuits to Array.CreateInstance + /// in that case so the lookup is never reached. + /// + bool TryGetArrayType (string jniElementTypeName, int rank, [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 95ac2fe4163..3ea07b0a2e0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -19,12 +19,30 @@ sealed class SingleUniverseTypeMap : ITypeMapWithAliasing readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; + // Per-rank array dictionaries (1-indexed; index 0 unused). Null when the typemap + // universe was generated without array entries (e.g. CoreCLR builds with + // $(PublishAot)==false). Only consulted under NativeAOT via TryGetArrayType. + readonly IReadOnlyDictionary?[] _arrayMaps = new IReadOnlyDictionary? [4]; + public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) + : this (typeMap, proxyTypeMap, null, null, null) + { + } + + public SingleUniverseTypeMap ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyTypeMap, + IReadOnlyDictionary? arrayMapRank1, + IReadOnlyDictionary? arrayMapRank2, + IReadOnlyDictionary? arrayMapRank3) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyTypeMap); _typeMap = typeMap; _proxyTypeMap = proxyTypeMap; + _arrayMaps [1] = arrayMapRank1; + _arrayMaps [2] = arrayMapRank2; + _arrayMaps [3] = arrayMapRank3; } public IEnumerable GetTypes (string jniName) @@ -83,4 +101,16 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } + + public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) + { + if (rank >= 1 && rank <= 3) { + var dict = _arrayMaps [rank]; + if (dict is not null && dict.TryGetValue (jniElementTypeName, 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 2ca52f03f38..33089672c7d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -42,10 +42,26 @@ public class TrimmableTypeMap /// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds). /// public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) + => Initialize (typeMap, proxyMap, null, null, null); + + /// + /// Initializes the singleton with a single merged typemap universe plus per-rank + /// array dictionaries (used by JNIEnv.ArrayCreateInstance under NativeAOT). + /// + /// + /// JNI element name → typeof(T[]) map. Null when the typemap was generated + /// without array entries (CoreCLR builds with $(PublishAot) == false). + /// + public static void Initialize ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyMap, + IReadOnlyDictionary? arrayMapRank1, + IReadOnlyDictionary? arrayMapRank2, + IReadOnlyDictionary? arrayMapRank3) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); - InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap)); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3)); } /// @@ -54,6 +70,24 @@ public static void Initialize (IReadOnlyDictionary typeMap, IReadO /// (_Microsoft.Android.TypeMaps) when each assembly has its own typemap universe (Debug builds). /// public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) + => Initialize (typeMaps, proxyMaps, null, null, null); + + /// + /// Initializes the singleton with multiple per-assembly typemap universes plus + /// per-assembly per-rank array dictionaries. + /// + /// + /// All four / five arrays must have the same length. The per-rank arrays may be null + /// (when no typemap assembly emitted array entries, i.e. CoreCLR builds), in which + /// case all per-universe rank dicts are treated as null. When non-null, individual + /// elements may still be null for universes that didn't emit array entries. + /// + public static void Initialize ( + IReadOnlyDictionary[] typeMaps, + IReadOnlyDictionary[] proxyMaps, + IReadOnlyDictionary?[]? arrayMapsRank1, + IReadOnlyDictionary?[]? arrayMapsRank2, + IReadOnlyDictionary?[]? arrayMapsRank3) { ArgumentNullException.ThrowIfNull (typeMaps); ArgumentNullException.ThrowIfNull (proxyMaps); @@ -63,13 +97,32 @@ public static void Initialize (IReadOnlyDictionary[] typeMaps, IRe if (typeMaps.Length != proxyMaps.Length) { throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length}).", nameof (proxyMaps)); } + ValidateRankArrayLength (arrayMapsRank1, typeMaps.Length, nameof (arrayMapsRank1)); + ValidateRankArrayLength (arrayMapsRank2, typeMaps.Length, nameof (arrayMapsRank2)); + ValidateRankArrayLength (arrayMapsRank3, typeMaps.Length, nameof (arrayMapsRank3)); + 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], + arrayMapsRank1?[i], + arrayMapsRank2?[i], + arrayMapsRank3?[i]); } InitializeCore (new AggregateTypeMap (universes)); } + static void ValidateRankArrayLength (IReadOnlyDictionary?[]? array, int expected, string paramName) + { + if (array is null) { + return; + } + if (array.Length != expected) { + throw new ArgumentException ($"{paramName}.Length ({array.Length}) must equal typeMaps.Length ({expected}).", paramName); + } + } + static void InitializeCore (ITypeMapWithAliasing typeMap) { lock (s_initLock) { @@ -318,6 +371,79 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) return GetProxyForManagedType (type)?.GetContainerFactory (); } + /// + /// AOT-safe lookup of the closed managed array type for the given element type. + /// Used by JNIEnv.ArrayCreateInstance under NativeAOT (where dynamic + /// Array.CreateInstance is not available for arbitrary element types). + /// Walks the element-type's array nesting (so byte[][] gets treated as a + /// rank-2 lookup of byte), resolves the leaf JNI element name (primitive + /// or peer reference), and queries the per-rank dictionary supplied at + /// . + /// + /// + /// True with the closed array on success. False on any miss + /// (unknown leaf, no per-rank dict supplied, no entry, rank > 3). The caller + /// in JNIEnv.ArrayCreateInstance throws a diagnostic + /// in that case. + /// + internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) + { + if (elementType is null) { + arrayType = null; + return false; + } + + // Walk to the leaf element type, counting the array depth that is part of the + // element. The total rank we look up is depth+1 — the +1 accounts for the new + // outer rank that ArrayCreateInstance is constructing. + var leaf = elementType; + int elementDepth = 0; + while (leaf.IsArray) { + var next = leaf.GetElementType (); + if (next is null) { + arrayType = null; + return false; + } + leaf = next; + elementDepth++; + } + + string leafJniName; + if (leaf.IsPrimitive) { + if (!TryGetPrimitiveJniName (leaf, out var primitive)) { + arrayType = null; + return false; + } + leafJniName = primitive; + } else if (!TryGetJniNameForManagedType (leaf, out var jni)) { + arrayType = null; + return false; + } else { + leafJniName = jni; + } + + int rank = elementDepth + 1; + return _typeMap.TryGetArrayType (leafJniName, rank, out arrayType); + } + + /// + /// JNI single-letter encodings for primitive element types. Reference: + /// . + /// + static bool TryGetPrimitiveJniName (Type primitive, [NotNullWhen (true)] out string? jni) + { + if (primitive == typeof (bool)) { jni = "Z"; return true; } + if (primitive == typeof (byte)) { jni = "B"; return true; } + if (primitive == typeof (char)) { jni = "C"; return true; } + if (primitive == typeof (short)) { jni = "S"; return true; } + if (primitive == typeof (int)) { jni = "I"; return true; } + if (primitive == typeof (long)) { jni = "J"; return true; } + if (primitive == typeof (float)) { jni = "F"; return true; } + if (primitive == typeof (double)) { jni = "D"; return true; } + jni = null; + return false; + } + [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index ec8ad004c88..85d1f0a504d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -76,6 +76,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" + EmitArrayEntries="$(PublishAot)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f80a14157f1..4e3c49165b9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -68,6 +68,15 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } + + /// + /// When true, the generator emits per-rank array TypeMap entries plus the matching + /// __ArrayMapRank{1,2,3} sentinel TypeDefs. Should be set to $(PublishAot) + /// by the caller — under CoreCLR/Mono the runtime falls through to Array.CreateInstance + /// directly and these entries are never queried, so emitting them just adds dead-weight + /// attribute metadata. + /// + public bool EmitArrayEntries { get; set; } public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -131,7 +140,8 @@ public override bool RunTask () frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, manifestConfig, - manifestTemplate); + manifestTemplate, + emitArrayEntries: EmitArrayEntries); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); From b12791acef66d01f8f237f20289269568fd33172 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 12:10:24 +0200 Subject: [PATCH 03/15] [TrimmableTypeMap] Address PR feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * JNIEnv.ArrayCreateInstance: * Drop the IL3050 #pragma — IsDynamicCodeSupported acts as a [FeatureGuard] so the trimmer dead-codes the dynamic branch under PublishAot without needing a manual suppression. * Drop the 'Mono' mention from the comment — the trimmable typemap is a CoreCLR-only feature. * SingleUniverseTypeMap._arrayMapsByRank: * Switched from a 1-indexed array of fixed length 4 (with [0] unused) to a 0-indexed variable-length array (length determined by what the generator emitted). TryGetArrayType uses (rank - 1) for indexing and bounds-checks to the actual length. * Variable rank count throughout: * RankSentinelNames: replaced the fixed (Rank1, Rank2, Rank3) record with a name list + Count, generated via CreateDefault(maxRank). * TypeMapAssemblyEmitter: _rankAnchorHandles becomes a 0-indexed variable-length array sized to model.RankSentinels.Count. * ModelBuilder.Build: replaced 'bool emitArrayEntries' with 'int maxArrayRank' (0 = disabled). EmitArrayEntries takes the max and loops 1..maxArrayRank instead of using a const. * RootTypeMapAssemblyGenerator: 'int maxArrayRank' parameter; the generated TypeMapLoader.Initialize now builds a single jagged 'IReadOnlyDictionary?[][]' (per-universe per-rank) instead of fixed-rank-count locals. Rank loop is unrolled. * GenerateTrimmableTypeMap MSBuild task: 'int MaxArrayRank' property. * New $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild property — defaults to 3 under PublishAot=true, 0 otherwise. Users can override to support unlimited rank under NativeAOT (limited to what they're willing to pay in attribute metadata). * TrimmableTypeMap.Initialize: * Single overload: '(typeMap, proxyMap, IReadOnlyDictionary?[]? arrayMapsByRank)' — 0-indexed by (rank - 1). * Aggregate overload: '(typeMaps[], proxyMaps[], IReadOnlyDictionary?[]?[]? perUniverseArrayMaps)' — jagged, indexed first by universe, then by (rank - 1). * Existing 2-arg overloads retained as wrappers passing null. * Tests: updated BuildModelWithArrays to take maxArrayRank (default 3). Replaced 'RankSentinels.Rank{1,2,3}' assertions with Names[i] indexing. Added Build_EmitArrayEntries_HonoursMaxArrayRank test that verifies emission with maxArrayRank=1 and =5. Validation: * 446 / 446 generator unit tests pass (445 baseline + 1 new for the MaxArrayRank parametrization). * Trimmable + CoreCLR RunTestApp lane on emulator: 917 / 0 errors / 3 pre-existing failures. No regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 57 +++-- .../Generator/ModelBuilder.cs | 48 ++-- .../Generator/RootTypeMapAssemblyGenerator.cs | 213 ++++++++---------- .../Generator/TypeMapAssemblyEmitter.cs | 32 +-- .../Generator/TypeMapAssemblyGenerator.cs | 11 +- .../TrimmableTypeMapGenerator.cs | 10 +- src/Mono.Android/Android.Runtime/JNIEnv.cs | 12 +- .../SingleUniverseTypeMap.cs | 30 +-- .../TrimmableTypeMap.cs | 58 ++--- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 13 +- .../Tasks/GenerateTrimmableTypeMap.cs | 16 +- .../Generator/TypeMapModelBuilderTests.cs | 34 ++- 12 files changed, 283 insertions(+), 251 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index c744ea2ef11..c8267595129 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -41,12 +41,12 @@ sealed class TypeMapAssemblyData public List AliasHolders { get; } = new (); /// - /// When non-null, the emitter will produce three additional internal - /// __ArrayMapRank{N} sealed type definitions to serve as the TGroup - /// arguments for per-rank array entries. - /// Each in whose - /// is non-null will reference one of - /// these generated TypeDefs via its rank index. + /// When non-null, the emitter will produce a series of internal + /// __ArrayMapRank{N} sealed type definitions (one per name in the list) + /// to serve as the TGroup arguments for per-rank array + /// entries. Each + /// in whose is + /// non-null will reference one of these generated TypeDefs via its rank index. /// /// /// Mirrors the existing __TypeMapAnchor emission pattern in @@ -66,24 +66,47 @@ sealed class TypeMapAssemblyData /// /// Names of the per-typemap-assembly array-rank sentinel types that the emitter generates. -/// All three live in the typemap assembly's root namespace. +/// All sentinels live in the typemap assembly's root namespace. is +/// indexed 0-based by (rank - 1), so Names[0] is the rank-1 sentinel, +/// Names[1] is rank-2, etc. Length is whatever the generator was configured to +/// emit (defaults to 3). /// -sealed record RankSentinelNames (string Rank1, string Rank2, string Rank3) +sealed class RankSentinelNames { + public RankSentinelNames (IReadOnlyList names) + { + if (names is null) { + throw new ArgumentNullException (nameof (names)); + } + if (names.Count == 0) { + throw new ArgumentException ("At least one rank sentinel name must be provided.", nameof (names)); + } + Names = names; + } + + public IReadOnlyList Names { get; } + + public int Count => Names.Count; + /// - /// Default sentinel names used by . + /// Returns the sentinel name for the given 1-based array rank, or null if rank is out of range. /// - public static readonly RankSentinelNames Default = new ("__ArrayMapRank1", "__ArrayMapRank2", "__ArrayMapRank3"); + public string? GetForRank (int rank) => rank >= 1 && rank <= Names.Count ? Names [rank - 1] : null; /// - /// Returns the sentinel name for the given 1-based array rank, or null if rank is out of range. + /// Builds the default __ArrayMapRank1..__ArrayMapRank{maxRank} name set. /// - public string? GetForRank (int rank) => rank switch { - 1 => Rank1, - 2 => Rank2, - 3 => Rank3, - _ => null, - }; + public static RankSentinelNames CreateDefault (int maxRank) + { + if (maxRank < 1) { + throw new ArgumentOutOfRangeException (nameof (maxRank), maxRank, "Must be >= 1."); + } + var names = new string [maxRank]; + for (int i = 0; i < maxRank; i++) { + names [i] = $"__ArrayMapRank{i + 1}"; + } + return new RankSentinelNames (names); + } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index e6ea873eafe..f5389a970e3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -40,17 +40,20 @@ static class ModelBuilder /// Scanned Java peer types (typically from a single input assembly). /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . - /// - /// When true, additionally emit speculative [L<jni>;-shaped TypeMap entries - /// for ranks 1–3 keyed by the element JNI name, with per-rank __ArrayMapRank{N} - /// sentinel TGroup types generated into the same assembly. The runtime - /// TrimmableTypeMap uses these to satisfy JNIEnv.ArrayCreateInstance under - /// NativeAOT (where dynamic Array.CreateInstance isn't available). Should be - /// gated on $(PublishAot) == true by the MSBuild task — under CoreCLR/Mono the - /// runtime falls through to Array.CreateInstance directly and these entries are + /// + /// Maximum array rank for which to emit speculative [L<jni>;-shaped + /// entries (and the matching + /// __ArrayMapRank{N} sentinel TGroup types). 0 disables array entry + /// emission entirely. The runtime TrimmableTypeMap uses these to satisfy + /// JNIEnv.ArrayCreateInstance under NativeAOT (where dynamic + /// Array.CreateInstance isn't available). Should be gated on + /// $(PublishAot) == true by the MSBuild task — under CoreCLR the runtime + /// falls through to Array.CreateInstance directly and these entries are /// never queried, so emitting them just adds dead-weight attribute metadata. + /// Configurable via the $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild + /// property; defaults to 3. /// - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, bool emitArrayEntries = false) + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -58,6 +61,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (outputPath is null) { throw new ArgumentNullException (nameof (outputPath)); } + if (maxArrayRank < 0) { + throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); + } assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); string moduleName = Path.GetFileName (outputPath); @@ -66,8 +72,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri AssemblyName = assemblyName, ModuleName = moduleName, }; - if (emitArrayEntries) { - model.RankSentinels = RankSentinelNames.Default; + if (maxArrayRank > 0) { + model.RankSentinels = RankSentinelNames.CreateDefault (maxArrayRank); } // Invoker types are NOT emitted as separate proxies or TypeMap entries — @@ -103,8 +109,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); - if (emitArrayEntries) { - EmitArrayEntries (model, jniName, peersForName); + if (maxArrayRank > 0) { + EmitArrayEntries (model, jniName, peersForName, maxArrayRank); } } @@ -410,15 +416,13 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; - const int MaxArrayRank = 3; - /// /// Emits speculative per-rank [L<jni>;-shaped - /// entries (ranks 1–3) for one peer group, keyed by the element JNI name and - /// anchored to the __ArrayMapRank{N} sentinel emitted into the same typemap - /// assembly. Each entry has the closed managed array type as both proxy and trim target, - /// so ILC's per-shape conditional drops the entry when the array shape is never - /// constructed. + /// entries (ranks 1..) for one peer group, keyed by + /// the element JNI name and anchored to the __ArrayMapRank{N} + /// sentinel emitted into the same typemap assembly. Each entry has the closed + /// managed array type as both proxy and trim target, so ILC's per-shape conditional + /// drops the entry when the array shape is never constructed. /// /// /// Skips: @@ -433,7 +437,7 @@ static string AssemblyQualify (string typeName, string assemblyName) /// motivates an alias-aware design. /// /// - static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List peersForName) + static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List peersForName, int maxArrayRank) { if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) { return; @@ -447,7 +451,7 @@ static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List /// Names of per-assembly typemap assemblies to reference. /// True to merge all assemblies into a single typemap universe, false for per-assembly universes. - /// - /// True when the per-assembly typemaps were generated with rank sentinels - /// (__ArrayMapRank{1,2,3}). The generated TypeMapLoader.Initialize - /// will then build per-rank dictionaries from each typemap and pass them to the - /// 5-arg TrimmableTypeMap.Initialize overload. Should match the - /// emitArrayEntries flag passed to the per-assembly typemap generators. - /// /// Stream to write the output PE to. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). /// Optional module name for the PE metadata. - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, bool emitArrayEntries = false) + /// + /// Maximum array rank for which the per-assembly typemaps emitted + /// __ArrayMapRank{N} sentinels. The generated TypeMapLoader.Initialize + /// will collect ranks 1.. from each per-assembly + /// typemap and pass the resulting jagged array to the 3-arg + /// TrimmableTypeMap.Initialize overload. 0 disables array-dict plumbing + /// (the generated loader calls the existing 2-arg Initialize). + /// Should match the value passed to the per-assembly typemap generators. + /// + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -128,7 +130,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, emitArrayEntries); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank); pe.WritePE (stream); } @@ -152,7 +154,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc } } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, bool emitArrayEntries) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank) { var metadata = pe.Metadata; @@ -188,24 +190,26 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); if (useSharedTypemapUniverse) { - if (emitArrayEntries) { - // 5-arg Initialize(typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3) - var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, getExternalMemberRef, initializeRef, getProxyMemberRef); - } else { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + if (maxArrayRank > 0) { + // Merged-universe + array entries: not yet wired up. The aggregate path + // covers both Debug and the (future) merged-with-arrays case; callers + // should fall back to per-assembly until shared-universe array sentinels + // are wired here. + throw new NotSupportedException ( + "Merged-universe (Release) + array typemap entries is not yet wired up. " + + "Use the aggregate/per-assembly path when maxArrayRank > 0."); } + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); } else { var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - if (emitArrayEntries) { + if (maxArrayRank > 0) { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + var arrayMapsByUniverseElemSpec = MakeArrayOfIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, arrayMapsByUniverseElemSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } else { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); } @@ -213,117 +217,99 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand } /// - /// Emits IL for the merged-universe + array-entries case. Calls - /// TypeMapping.GetOrCreateExternalTypeMapping<T> five times — once each - /// for the proxy-map anchor, external-map anchor, and the three rank sentinels — - /// and passes all five results to the 5-arg TrimmableTypeMap.Initialize. - /// - static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, - MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle initializeRef, - MemberReferenceHandle getProxyMemberRef) - { - // In merged-universe mode all per-assembly typemaps share the same anchor - // (Java.Lang.Object). The rank sentinels live in each per-assembly typemap - // dll, but in shared-universe mode there's only one such dll (the merged - // one) — and the merged one's rank sentinels are referenced via the same - // per-assembly anchor namespace. However, the merged dll's rank sentinels - // are emitted there as TypeDefs, so we need a TypeRef from this root dll - // back to those sentinels. - // - // In practice, when useSharedTypemapUniverse is true, perAssemblyTypeMapNames - // has exactly one entry (the merged dll). The sentinels are emitted into - // that dll. We'd need that dll's name to construct TypeRefs. - // - // For now, the merged-universe + array-entries path is unsupported because - // the existing single-universe loader doesn't iterate perAssemblyTypeMapNames. - // The aggregate path covers both Debug (per-assembly) and the future - // merged-but-with-array case. Throw at generation time so misconfiguration - // surfaces immediately. - throw new NotSupportedException ( - "Merged-universe (Release) + array typemap entries is not yet wired up. " + - "The aggregate/per-assembly path covers both Debug and the (future) merged case; " + - "callers should use that path when emitArrayEntries is true."); - } - - /// - /// Emits IL for the per-assembly + array-entries case. Builds five - /// IReadOnlyDictionary<...>[] arrays (typeMaps, proxyMaps, - /// arrayMapsRank1, arrayMapsRank2, arrayMapsRank3) by querying each - /// per-assembly typemap's __TypeMapAnchor + __ArrayMapRank{1,2,3} - /// sentinels via TypeMapping.GetOrCreateExternalTypeMapping<T>, then - /// passes them to the 5-arg TrimmableTypeMap.Initialize. + /// Emits IL for the per-assembly + array-entries case. Builds three locals: + /// + /// typeMaps: IReadOnlyDictionary<string, Type>[N] + /// proxyMaps: IReadOnlyDictionary<Type, Type>[N] + /// perUniverseArrayMaps: IReadOnlyDictionary<string, Type>?[N][] + /// — a jagged array of N arrays, each of length , + /// indexed 0-based by (rank - 1). + /// + /// then passes them to the 3-arg aggregate TrimmableTypeMap.Initialize. /// static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + TypeSpecificationHandle arrayMapsByUniverseElemSpec, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, + int maxArrayRank) { var count = perAssemblyTypeMapNames.Count; var getExternalSpecs = new EntityHandle [count]; var getProxySpecs = new EntityHandle [count]; - var getArrayRank1Specs = new EntityHandle [count]; - var getArrayRank2Specs = new EntityHandle [count]; - var getArrayRank3Specs = new EntityHandle [count]; + + // rankSpecs [universe, rankIndex] — 0-based rankIndex. + var rankSpecs = new EntityHandle [count, maxArrayRank]; + for (int i = 0; i < count; i++) { var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); - var rank1Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank1")); - var rank2Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank2")); - var rank3Ref = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__ArrayMapRank3")); getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); - getArrayRank1Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank1Ref); - getArrayRank2Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank2Ref); - getArrayRank3Specs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, rank3Ref); + for (int r = 0; r < maxArrayRank; r++) { + var rankRef = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); + rankSpecs [i, r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + } } pe.EmitBody ("Initialize", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // var typeMaps = new IReadOnlyDictionary[N]; (loc 0) + // var typeMaps = new IReadOnlyDictionary[N]; (loc 0) EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0); EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0); - // var proxyMaps = new IReadOnlyDictionary[N]; (loc 1) + // var proxyMaps = new IReadOnlyDictionary[N]; (loc 1) EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); - // var arrayMapsRank1 = new IReadOnlyDictionary[N]; (loc 2) - EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 2); - EmitFillArrayLocal (encoder, count, getArrayRank1Specs, slot: 2); - - // var arrayMapsRank2 = new IReadOnlyDictionary[N]; (loc 3) - EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 3); - EmitFillArrayLocal (encoder, count, getArrayRank2Specs, slot: 3); - - // var arrayMapsRank3 = new IReadOnlyDictionary[N]; (loc 4) - EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 4); - EmitFillArrayLocal (encoder, count, getArrayRank3Specs, slot: 4); + // var perUniverseArrayMaps = new IReadOnlyDictionary?[][N]; (loc 2) + EmitNewArrayLocal (encoder, count, arrayMapsByUniverseElemSpec, slot: 2); + + // Fill perUniverseArrayMaps[u] = new IReadOnlyDictionary?[maxArrayRank] + // populated with GetOrCreateExternalTypeMapping<__ArrayMapRank{r+1}_u>() + for (int u = 0; u < count; u++) { + encoder.LoadLocal (2); + encoder.LoadConstantI4 (u); + encoder.LoadConstantI4 (maxArrayRank); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + for (int r = 0; r < maxArrayRank; r++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (r); + encoder.OpCode (ILOpCode.Call); + encoder.Token (rankSpecs [u, r]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + encoder.OpCode (ILOpCode.Stelem_ref); + } - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsRank1, arrayMapsRank2, arrayMapsRank3) + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, perUniverseArrayMaps) encoder.LoadLocal (0); encoder.LoadLocal (1); encoder.LoadLocal (2); - encoder.LoadLocal (3); - encoder.LoadLocal (4); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); }, encodeLocals: localsSig => { localsSig.WriteByte (0x07); // LOCAL_SIG - localsSig.WriteCompressedInteger (5); // count - for (int i = 0; i < 5; i++) { - // Each local: IReadOnlyDictionary[] - // Slot 1 (proxyMaps) is keyed by Type; the rest by string. - localsSig.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: i != 1); - } + localsSig.WriteCompressedInteger (3); // count + // loc 0: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // loc 1: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + // loc 2: IReadOnlyDictionary?[][] — outer SZARRAY of inner SZARRAY + localsSig.WriteByte (0x1D); + localsSig.WriteByte (0x1D); + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); }); } @@ -347,46 +333,39 @@ static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHan } /// - /// Creates a MemberRef for the 5-arg single-universe TrimmableTypeMap.Initialize - /// (typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3). - /// All three array maps are nullable. + /// Creates a TypeSpec for IReadOnlyDictionary<string, Type>?[] — used as + /// the element type of the per-universe outer array. /// - static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + static TypeSpecificationHandle MakeArrayOfIReadOnlyDictTypeSpec (PEAssemblyBuilder pe, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { - var blob = new BlobBuilder (96); - blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (5); // parameter count - blob.WriteByte (0x01); // return type: void - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var blob = new BlobBuilder (32); + blob.WriteByte (0x1D); // SZARRAY EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - return pe.Metadata.AddMemberReference (trimmableTypeMapRef, - pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); } /// - /// Creates a MemberRef for the 5-arg aggregate TrimmableTypeMap.Initialize - /// (typeMaps[], proxyMaps[], arrayMapsRank1[], arrayMapsRank2[], arrayMapsRank3[]). + /// Creates a MemberRef for the 3-arg aggregate TrimmableTypeMap.Initialize + /// (typeMaps[], proxyMaps[], perUniverseArrayMaps[][]). /// static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { var blob = new BlobBuilder (96); blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (5); // parameter count + blob.WriteCompressedInteger (3); // parameter count blob.WriteByte (0x01); // return type: void // Param 1: IReadOnlyDictionary[] - blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); // Param 2: IReadOnlyDictionary[] - blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Params 3-5: IReadOnlyDictionary?[] - for (int i = 0; i < 3; i++) { - blob.WriteByte (0x1D); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - } + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + // Param 3: IReadOnlyDictionary?[][] + 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)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 3da5a3741c0..ae3aef9cfb0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -119,10 +119,10 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; - // Per-rank array sentinel TypeDefs (1-indexed; index 0 unused). Populated when - // model.RankSentinels is non-null. Each rank-anchored TypeMap entry uses one of - // these as its TGroup token (e.g. typeof(__ArrayMapRank2)). - readonly EntityHandle[] _rankAnchorHandles = new EntityHandle [4]; + // Per-rank array sentinel TypeDefs indexed 0-based by (rank - 1). Length equals + // model.RankSentinels.Count when array entries are emitted, empty otherwise. + // Each rank-anchored TypeMap entry uses one of these as its TGroup token. + EntityHandle [] _rankAnchorHandles = Array.Empty (); // Per-anchor 3-arg TypeMap(string, Type, Type) ctor refs, keyed by anchor // EntityHandle. Built lazily by GetOrAddTypeMapAttr3ArgCtorRef. The default-anchor @@ -281,10 +281,11 @@ void EmitAnchorType () } /// - /// Emits up to three internal __ArrayMapRank{1,2,3} classes used as the group - /// type parameters for speculative array-shape TypeMap<T> entries. Each - /// per-assembly typemap DLL emits its own set so the runtime TypeMapLoader - /// can collect per-assembly per-rank dictionaries via + /// Emits internal __ArrayMapRank{N} classes used as the group type + /// parameters for speculative array-shape TypeMap<T> entries — one per + /// name in . Each per-assembly + /// typemap DLL emits its own set so the runtime TypeMapLoader can collect + /// per-assembly per-rank dictionaries via /// TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>(). /// No-op when is null. /// @@ -294,9 +295,11 @@ void EmitRankSentinels (TypeMapAssemblyData model) return; } - _rankAnchorHandles [1] = EmitRankSentinel (model.RankSentinels.Rank1); - _rankAnchorHandles [2] = EmitRankSentinel (model.RankSentinels.Rank2); - _rankAnchorHandles [3] = EmitRankSentinel (model.RankSentinels.Rank3); + var sentinels = model.RankSentinels; + _rankAnchorHandles = new EntityHandle [sentinels.Count]; + for (int i = 0; i < sentinels.Count; i++) { + _rankAnchorHandles [i] = EmitRankSentinel (sentinels.Names [i]); + } } EntityHandle EmitRankSentinel (string typeName) @@ -1316,12 +1319,13 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) throw new InvalidOperationException ( $"Rank-anchored TypeMap entries must be conditional (3-arg). Entry '{entry.JniName}' rank={rank}."); } - if (rank < 1 || rank > 3 || _rankAnchorHandles [rank] == default) { + int anchorIndex = rank - 1; + if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length || _rankAnchorHandles [anchorIndex] == default) { throw new InvalidOperationException ( $"No rank-{rank} anchor TypeDef was emitted for entry '{entry.JniName}'. " + - $"Ensure TypeMapAssemblyData.RankSentinels was set before emit."); + $"Ensure TypeMapAssemblyData.RankSentinels was set (with sufficient Count) before emit."); } - ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [rank]); + ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [anchorIndex]); } else { ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 99ee21a56c7..51261d149c2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -27,14 +27,13 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// - /// - /// When true, additionally emit per-rank array TypeMap entries (ranks 1–3) plus the - /// matching __ArrayMapRank{N} sentinel TypeDefs. Should be gated on - /// $(PublishAot) == true by the caller. + /// + /// Maximum array rank for which to emit speculative TypeMap entries plus the + /// matching __ArrayMapRank{N} sentinels. 0 disables array entry emission. /// - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, bool emitArrayEntries = false) + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, emitArrayEntries); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index cb70d3b8b55..5fb0bd65b60 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -33,7 +33,7 @@ public TrimmableTypeMapResult Execute ( bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, - bool emitArrayEntries = false) + int maxArrayRank = 0) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -49,7 +49,7 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, emitArrayEntries); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -140,7 +140,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, bool emitArrayEntries) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -169,14 +169,14 @@ List GenerateTypeMapAssemblies (List allPeers, string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, emitArrayEntries); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, emitArrayEntries: emitArrayEntries); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 9dbc6399b81..8d756ab31b9 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -28,14 +28,12 @@ static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { - // CoreCLR / Mono — runtime type loader can construct any T[] dynamically. - // No typemap roundtrip; supports unlimited array rank. The IL3050 analyzer - // warns this isn't AOT-safe; the surrounding `if (IsDynamicCodeSupported)` - // IS the live feature switch the trimmer uses to dead-code this branch under - // PublishAot=true. - #pragma warning disable IL3050 + // CoreCLR — runtime type loader can construct any T[] dynamically. + // No typemap roundtrip; supports unlimited array rank. + // IsDynamicCodeSupported acts as a [FeatureGuard], so the trimmer + // dead-codes this branch under PublishAot and no IL3050 suppression + // is needed. return Array.CreateInstance (elementType, length); - #pragma warning restore IL3050 } // NativeAOT — resolve the closed array type via the per-rank typemap diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs index 3ea07b0a2e0..960fb592654 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -19,30 +19,29 @@ sealed class SingleUniverseTypeMap : ITypeMapWithAliasing readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // Per-rank array dictionaries (1-indexed; index 0 unused). Null when the typemap - // universe was generated without array entries (e.g. CoreCLR builds with - // $(PublishAot)==false). Only consulted under NativeAOT via TryGetArrayType. - readonly IReadOnlyDictionary?[] _arrayMaps = new IReadOnlyDictionary? [4]; + // Per-rank array dictionaries indexed 0-based by (rank - 1): + // [0] is the rank-1 dictionary, [1] rank-2, etc. + // Length is whatever the generator emitted (defaults to 3, configurable via + // the _AndroidTrimmableTypeMapMaxArrayRank MSBuild property). Empty / null when + // the typemap universe was generated without array entries (e.g. CoreCLR builds + // with $(PublishAot) == false). Only consulted under NativeAOT via TryGetArrayType. + readonly IReadOnlyDictionary?[] _arrayMapsByRank; public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) - : this (typeMap, proxyTypeMap, null, null, null) + : this (typeMap, proxyTypeMap, arrayMapsByRank: null) { } public SingleUniverseTypeMap ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap, - IReadOnlyDictionary? arrayMapRank1, - IReadOnlyDictionary? arrayMapRank2, - IReadOnlyDictionary? arrayMapRank3) + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyTypeMap); _typeMap = typeMap; _proxyTypeMap = proxyTypeMap; - _arrayMaps [1] = arrayMapRank1; - _arrayMaps [2] = arrayMapRank2; - _arrayMaps [3] = arrayMapRank3; + _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?> (); } public IEnumerable GetTypes (string jniName) @@ -104,8 +103,13 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) { - if (rank >= 1 && rank <= 3) { - var dict = _arrayMaps [rank]; + // The dictionary array is 0-based, so rank N lives at index N - 1. + // rank < 1 is invalid; rank > _arrayMapsByRank.Length means we don't have + // per-rank entries that high (either generator emitted up to a smaller + // MaxArrayRank, or the universe has no array entries at all). + int index = rank - 1; + if ((uint)index < (uint)_arrayMapsByRank.Length) { + var dict = _arrayMapsByRank [index]; if (dict is not null && dict.TryGetValue (jniElementTypeName, out arrayType)) { return true; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 33089672c7d..890f14e8dfc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -42,26 +42,28 @@ public class TrimmableTypeMap /// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds). /// public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) - => Initialize (typeMap, proxyMap, null, null, null); + => Initialize (typeMap, proxyMap, arrayMapsByRank: null); /// /// Initializes the singleton with a single merged typemap universe plus per-rank /// array dictionaries (used by JNIEnv.ArrayCreateInstance under NativeAOT). /// - /// - /// JNI element name → typeof(T[]) map. Null when the typemap was generated - /// without array entries (CoreCLR builds with $(PublishAot) == false). + /// + /// 0-based per-rank array of dictionaries — arrayMapsByRank[0] is the rank-1 + /// dictionary (JNI element name → typeof(T[])), [1] is rank-2, etc. + /// Length is whatever the generator emitted (defaults to 3, configurable via + /// $(_AndroidTrimmableTypeMapMaxArrayRank)). Null when the typemap was + /// generated without array entries (CoreCLR builds with $(PublishAot) == false). + /// Individual elements may be null when a particular rank has no entries. /// public static void Initialize ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap, - IReadOnlyDictionary? arrayMapRank1, - IReadOnlyDictionary? arrayMapRank2, - IReadOnlyDictionary? arrayMapRank3) + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); - InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapRank1, arrayMapRank2, arrayMapRank3)); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapsByRank)); } /// @@ -70,24 +72,22 @@ public static void Initialize ( /// (_Microsoft.Android.TypeMaps) when each assembly has its own typemap universe (Debug builds). /// public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) - => Initialize (typeMaps, proxyMaps, null, null, null); + => Initialize (typeMaps, proxyMaps, perUniverseArrayMaps: null); /// /// Initializes the singleton with multiple per-assembly typemap universes plus - /// per-assembly per-rank array dictionaries. + /// per-universe per-rank array dictionaries. /// - /// - /// All four / five arrays must have the same length. The per-rank arrays may be null - /// (when no typemap assembly emitted array entries, i.e. CoreCLR builds), in which - /// case all per-universe rank dicts are treated as null. When non-null, individual - /// elements may still be null for universes that didn't emit array entries. - /// + /// + /// Jagged array indexed first by universe, then 0-based by (rank - 1) within that + /// universe. perUniverseArrayMaps[i][r] is universe i's rank-(r+1) + /// dictionary. Null when no typemap assembly emitted array entries (CoreCLR builds); + /// individual outer elements may be null for universes that didn't emit array entries. + /// public static void Initialize ( IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps, - IReadOnlyDictionary?[]? arrayMapsRank1, - IReadOnlyDictionary?[]? arrayMapsRank2, - IReadOnlyDictionary?[]? arrayMapsRank3) + IReadOnlyDictionary?[]?[]? perUniverseArrayMaps) { ArgumentNullException.ThrowIfNull (typeMaps); ArgumentNullException.ThrowIfNull (proxyMaps); @@ -97,32 +97,20 @@ public static void Initialize ( if (typeMaps.Length != proxyMaps.Length) { throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length}).", nameof (proxyMaps)); } - ValidateRankArrayLength (arrayMapsRank1, typeMaps.Length, nameof (arrayMapsRank1)); - ValidateRankArrayLength (arrayMapsRank2, typeMaps.Length, nameof (arrayMapsRank2)); - ValidateRankArrayLength (arrayMapsRank3, typeMaps.Length, nameof (arrayMapsRank3)); + if (perUniverseArrayMaps is not null && perUniverseArrayMaps.Length != typeMaps.Length) { + throw new ArgumentException ($"perUniverseArrayMaps.Length ({perUniverseArrayMaps.Length}) must equal typeMaps.Length ({typeMaps.Length}).", nameof (perUniverseArrayMaps)); + } var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { universes [i] = new SingleUniverseTypeMap ( typeMaps [i], proxyMaps [i], - arrayMapsRank1?[i], - arrayMapsRank2?[i], - arrayMapsRank3?[i]); + perUniverseArrayMaps?[i]); } InitializeCore (new AggregateTypeMap (universes)); } - static void ValidateRankArrayLength (IReadOnlyDictionary?[]? array, int expected, string paramName) - { - if (array is null) { - return; - } - if (array.Length != expected) { - throw new ArgumentException ($"{paramName}.Length ({array.Length}) must equal typeMaps.Length ({expected}).", paramName); - } - } - static void InitializeCore (ITypeMapWithAliasing typeMap) { lock (s_initLock) { diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 85d1f0a504d..1967c2a1532 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -19,6 +19,17 @@ <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java + + + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 @@ -76,7 +87,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" - EmitArrayEntries="$(PublishAot)" + MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 4e3c49165b9..f8ca3858a6f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -70,13 +70,15 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool EmbedAssemblies { get; set; } /// - /// When true, the generator emits per-rank array TypeMap entries plus the matching - /// __ArrayMapRank{1,2,3} sentinel TypeDefs. Should be set to $(PublishAot) - /// by the caller — under CoreCLR/Mono the runtime falls through to Array.CreateInstance - /// directly and these entries are never queried, so emitting them just adds dead-weight - /// attribute metadata. + /// Maximum array rank for which the generator emits per-rank TypeMap entries + /// plus the matching __ArrayMapRank{N} sentinel TypeDefs. 0 (default) + /// disables array entry emission entirely. Should normally be set to + /// $(_AndroidTrimmableTypeMapMaxArrayRank) when $(PublishAot) == true + /// — under CoreCLR the runtime falls through to Array.CreateInstance + /// directly and these entries are never queried, so emitting them just adds + /// dead-weight attribute metadata. /// - public bool EmitArrayEntries { get; set; } + public int MaxArrayRank { get; set; } public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -141,7 +143,7 @@ public override bool RunTask () useSharedTypemapUniverse: !Debug, manifestConfig, manifestTemplate, - emitArrayEntries: EmitArrayEntries); + maxArrayRank: MaxArrayRank); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index eb9cdd2d449..8f4e1687183 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -16,10 +16,10 @@ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string return ModelBuilder.Build (peers, outputPath, assemblyName); } - static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null) + static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3) { var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); - return ModelBuilder.Build (peers, outputPath, assemblyName, emitArrayEntries: true); + return ModelBuilder.Build (peers, outputPath, assemblyName, maxArrayRank); } public class BasicStructure @@ -870,9 +870,29 @@ public void Build_EmitArrayEntries_SetsDefaultRankSentinels () var model = BuildModelWithArrays (new [] { peer }); Assert.NotNull (model.RankSentinels); - Assert.Equal ("__ArrayMapRank1", model.RankSentinels!.Rank1); - Assert.Equal ("__ArrayMapRank2", model.RankSentinels.Rank2); - Assert.Equal ("__ArrayMapRank3", model.RankSentinels.Rank3); + Assert.Equal (3, model.RankSentinels!.Count); + Assert.Equal ("__ArrayMapRank1", model.RankSentinels.Names [0]); + Assert.Equal ("__ArrayMapRank2", model.RankSentinels.Names [1]); + Assert.Equal ("__ArrayMapRank3", model.RankSentinels.Names [2]); + } + + [Fact] + public void Build_EmitArrayEntries_HonoursMaxArrayRank () + { + // Caller can ask for fewer or more ranks than the default. Verifies the + // $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild property's effect. + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + + var model5 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 5); + Assert.Equal (5, model5.RankSentinels!.Count); + Assert.Equal ("__ArrayMapRank5", model5.RankSentinels.Names [4]); + var rank5Entries = model5.Entries.Where (e => e.AnchorRank is not null).ToList (); + Assert.Equal (5, rank5Entries.Count); + Assert.Equal ("Foo.Bar[][][][][], App", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); + + var model1 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 1); + Assert.Equal (1, model1.RankSentinels!.Count); + Assert.Single (model1.Entries.Where (e => e.AnchorRank is not null)); } [Fact] @@ -1005,7 +1025,7 @@ public void FullPipeline_ArrayEntries_EmitsSentinelTypeDefs () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); - var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrSentinels", emitArrayEntries: true); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrSentinels", maxArrayRank: 3); Assert.NotNull (model.RankSentinels); EmitAndVerify (model, "ArrSentinels", (pe, reader) => { @@ -1043,7 +1063,7 @@ public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrBlobs.dll"); - var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", emitArrayEntries: true); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", maxArrayRank: 3); EmitAndVerify (model, "ArrBlobs", (pe, reader) => { var attrs = ReadAllTypeMapAttributeBlobs (reader); From ce92d446dc9957c066b9216ff43724de3e473366 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 12:31:39 +0200 Subject: [PATCH 04/15] [TrimmableTypeMap] Trim verbose doc comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tightens XML doc comments and inline notes added in this PR — keeps the intent but drops walls of explanation that were repeating what the code already says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 44 ++++------------- .../Generator/ModelBuilder.cs | 35 ++------------ .../Generator/RootTypeMapAssemblyGenerator.cs | 32 ++++--------- .../Generator/TypeMapAssemblyEmitter.cs | 26 +++------- .../Generator/TypeMapAssemblyGenerator.cs | 5 +- src/Mono.Android/Android.Runtime/JNIEnv.cs | 14 ++---- .../Java.Interop/JavaPeerContainerFactory.cs | 12 +---- .../AggregateTypeMap.cs | 3 +- .../ITypeMapWithAliasing.cs | 17 +------ .../SingleUniverseTypeMap.cs | 12 +---- .../TrimmableTypeMap.cs | 47 +++---------------- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 10 +--- .../Tasks/GenerateTrimmableTypeMap.cs | 10 ++-- 13 files changed, 51 insertions(+), 216 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index c8267595129..1fbe3e7d501 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -41,21 +41,9 @@ sealed class TypeMapAssemblyData public List AliasHolders { get; } = new (); /// - /// When non-null, the emitter will produce a series of internal - /// __ArrayMapRank{N} sealed type definitions (one per name in the list) - /// to serve as the TGroup arguments for per-rank array - /// entries. Each - /// in whose is - /// non-null will reference one of these generated TypeDefs via its rank index. - /// - /// - /// Mirrors the existing __TypeMapAnchor emission pattern in - /// : in per-assembly mode (Debug) each typemap - /// dll owns its own rank sentinels; in shared-universe mode (Release) the single - /// merged dll owns one set. The runtime TypeMapLoader queries each per-assembly - /// sentinel via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>() - /// to build the per-rank dictionaries passed to TrimmableTypeMap.Initialize. - /// + /// Names of the per-rank __ArrayMapRank{N} sentinel TypeDefs to emit. + /// Null disables array-shape emission. + /// public RankSentinelNames? RankSentinels { get; set; } /// @@ -65,11 +53,7 @@ sealed class TypeMapAssemblyData } /// -/// Names of the per-typemap-assembly array-rank sentinel types that the emitter generates. -/// All sentinels live in the typemap assembly's root namespace. is -/// indexed 0-based by (rank - 1), so Names[0] is the rank-1 sentinel, -/// Names[1] is rank-2, etc. Length is whatever the generator was configured to -/// emit (defaults to 3). +/// Names of the array-rank sentinel TypeDefs to emit, indexed 0-based by (rank - 1). /// sealed class RankSentinelNames { @@ -88,14 +72,10 @@ public RankSentinelNames (IReadOnlyList names) public int Count => Names.Count; - /// - /// Returns the sentinel name for the given 1-based array rank, or null if rank is out of range. - /// + /// Returns the sentinel name for the given 1-based array rank, or null if out of range. public string? GetForRank (int rank) => rank >= 1 && rank <= Names.Count ? Names [rank - 1] : null; - /// - /// Builds the default __ArrayMapRank1..__ArrayMapRank{maxRank} name set. - /// + /// Builds __ArrayMapRank1..__ArrayMapRank{maxRank}. public static RankSentinelNames CreateDefault (int maxRank) { if (maxRank < 1) { @@ -142,17 +122,9 @@ sealed record TypeMapAttributeData public bool IsUnconditional => TargetTypeReference == null; /// - /// When non-null, this entry uses the array-rank sentinel __ArrayMapRank{value} - /// from the same typemap assembly as its TGroup argument instead of the default - /// model-level anchor (__TypeMapAnchor / Java.Lang.Object). + /// 1-based array rank when this entry should use a __ArrayMapRank{value} + /// sentinel as its TGroup instead of the default model anchor. /// - /// - /// Used by speculative array entries: - /// [assembly: TypeMap<__ArrayMapRank2>("java/lang/String", typeof(string[][]), typeof(string[][]))]. - /// The emitter resolves the rank to one of the names declared in - /// and emits a per-rank closed - /// TypeMapAttribute<TGroup> ctor reference. - /// public int? AnchorRank { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index f5389a970e3..a4a9c7d152d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -41,17 +41,8 @@ static class ModelBuilder /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . /// - /// Maximum array rank for which to emit speculative [L<jni>;-shaped - /// entries (and the matching - /// __ArrayMapRank{N} sentinel TGroup types). 0 disables array entry - /// emission entirely. The runtime TrimmableTypeMap uses these to satisfy - /// JNIEnv.ArrayCreateInstance under NativeAOT (where dynamic - /// Array.CreateInstance isn't available). Should be gated on - /// $(PublishAot) == true by the MSBuild task — under CoreCLR the runtime - /// falls through to Array.CreateInstance directly and these entries are - /// never queried, so emitting them just adds dead-weight attribute metadata. - /// Configurable via the $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild - /// property; defaults to 3. + /// Emit per-rank array TypeMap entries + __ArrayMapRank{N} sentinels + /// for ranks 1... 0 disables array entry emission. /// public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) { @@ -417,26 +408,10 @@ static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; /// - /// Emits speculative per-rank [L<jni>;-shaped - /// entries (ranks 1..) for one peer group, keyed by - /// the element JNI name and anchored to the __ArrayMapRank{N} - /// sentinel emitted into the same typemap assembly. Each entry has the closed - /// managed array type as both proxy and trim target, so ILC's per-shape conditional - /// drops the entry when the array shape is never constructed. + /// Emits per-rank [L<jni>;-shaped TypeMap entries for one peer, anchored to + /// the per-assembly __ArrayMapRank{N} sentinels. Skips open generics, primitive + /// JNI keyword keys (handled by the legacy primitive-array path), and alias groups. /// - /// - /// Skips: - /// - /// Open-generic peers — typeof(T<>[]) is not a valid IL token. - /// JNI primitive-keyword keys (Z, B, C, S, - /// I, J, F, D) — primitive arrays are handled by the - /// legacy JniRuntime.JniTypeManager.GetPrimitiveArrayTypesForSimpleReference - /// path; emitting array entries here would shadow that built-in handling. - /// Alias groups (multiple peers sharing one JNI name) — would produce - /// duplicate keys in the per-rank dictionary; deferred until a real-world need - /// motivates an alias-aware design. - /// - /// static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List peersForName, int maxArrayRank) { if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 49983d406d1..c0a747b8e96 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -73,13 +73,9 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). /// Optional module name for the PE metadata. /// - /// Maximum array rank for which the per-assembly typemaps emitted - /// __ArrayMapRank{N} sentinels. The generated TypeMapLoader.Initialize - /// will collect ranks 1.. from each per-assembly - /// typemap and pass the resulting jagged array to the 3-arg - /// TrimmableTypeMap.Initialize overload. 0 disables array-dict plumbing - /// (the generated loader calls the existing 2-arg Initialize). - /// Should match the value passed to the per-assembly typemap generators. + /// 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 uses the 2-arg Initialize. /// public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) { @@ -217,15 +213,9 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand } /// - /// Emits IL for the per-assembly + array-entries case. Builds three locals: - /// - /// typeMaps: IReadOnlyDictionary<string, Type>[N] - /// proxyMaps: IReadOnlyDictionary<Type, Type>[N] - /// perUniverseArrayMaps: IReadOnlyDictionary<string, Type>?[N][] - /// — a jagged array of N arrays, each of length , - /// indexed 0-based by (rank - 1). - /// - /// then passes them to the 3-arg aggregate TrimmableTypeMap.Initialize. + /// Per-assembly + array-entries IL emission. Builds three locals: typeMaps[N], + /// proxyMaps[N], and a jagged perUniverseArrayMaps[N][maxArrayRank]; + /// passes them to the 3-arg aggregate TrimmableTypeMap.Initialize. /// static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, @@ -332,10 +322,7 @@ static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHan } } - /// - /// Creates a TypeSpec for IReadOnlyDictionary<string, Type>?[] — used as - /// the element type of the per-universe outer array. - /// + /// TypeSpec for IReadOnlyDictionary<string, Type>?[], used as the per-universe inner array type. static TypeSpecificationHandle MakeArrayOfIReadOnlyDictTypeSpec (PEAssemblyBuilder pe, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -345,10 +332,7 @@ static TypeSpecificationHandle MakeArrayOfIReadOnlyDictTypeSpec (PEAssemblyBuild return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); } - /// - /// Creates a MemberRef for the 3-arg aggregate TrimmableTypeMap.Initialize - /// (typeMaps[], proxyMaps[], perUniverseArrayMaps[][]). - /// + /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], perUniverseArrayMaps[][]). static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index ae3aef9cfb0..1861cebe51d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -119,18 +119,14 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; - // Per-rank array sentinel TypeDefs indexed 0-based by (rank - 1). Length equals - // model.RankSentinels.Count when array entries are emitted, empty otherwise. - // Each rank-anchored TypeMap entry uses one of these as its TGroup token. + // Per-rank array sentinel TypeDefs, 0-indexed by (rank - 1). Empty when array entries + // aren't emitted. EntityHandle [] _rankAnchorHandles = Array.Empty (); - // Per-anchor 3-arg TypeMap(string, Type, Type) ctor refs, keyed by anchor - // EntityHandle. Built lazily by GetOrAddTypeMapAttr3ArgCtorRef. The default-anchor - // 3-arg ctor (= _typeMapAttrCtorRef3Arg) is also stored here so all callers go - // through the same cache. + // Per-anchor TypeMap(string, Type, Type) ctor refs, lazily built. readonly Dictionary _typeMapAttr3ArgCtorRefByAnchor = new (); - // Cached open TypeMapAttribute`1 ref so per-anchor closed TypeSpecs share metadata. + // Cached open TypeMapAttribute`1 ref shared across closed TypeSpecs. TypeReferenceHandle _typeMapAttrOpenRef; /// @@ -281,13 +277,8 @@ void EmitAnchorType () } /// - /// Emits internal __ArrayMapRank{N} classes used as the group type - /// parameters for speculative array-shape TypeMap<T> entries — one per - /// name in . Each per-assembly - /// typemap DLL emits its own set so the runtime TypeMapLoader can collect - /// per-assembly per-rank dictionaries via - /// TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>(). - /// No-op when is null. + /// Emits one internal sealed __ArrayMapRank{N} TypeDef per name in + /// . No-op when null. /// void EmitRankSentinels (TypeMapAssemblyData model) { @@ -462,10 +453,7 @@ void EmitTypeMapAttributeCtorRef () _typeMapAttr3ArgCtorRefByAnchor [_anchorTypeHandle] = _typeMapAttrCtorRef3Arg; } - /// - /// Returns the cached 3-arg TypeMap<TGroup> ctor ref for the given - /// anchor (group) type, building and caching it if not yet present. - /// + /// Cached 3-arg TypeMap<TGroup> ctor ref for the given anchor, built on first use. MemberReferenceHandle GetOrAddTypeMapAttr3ArgCtorRef (EntityHandle anchor) { if (_typeMapAttr3ArgCtorRefByAnchor.TryGetValue (anchor, out var cached)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 51261d149c2..48ca89f45bc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -27,10 +27,7 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// - /// - /// Maximum array rank for which to emit speculative TypeMap entries plus the - /// matching __ArrayMapRank{N} sentinels. 0 disables array entry emission. - /// + /// Max rank for per-rank array TypeMap entries. 0 disables. public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8d756ab31b9..42d19c76dcb 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -28,17 +28,13 @@ static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { - // CoreCLR — runtime type loader can construct any T[] dynamically. - // No typemap roundtrip; supports unlimited array rank. - // IsDynamicCodeSupported acts as a [FeatureGuard], so the trimmer - // dead-codes this branch under PublishAot and no IL3050 suppression - // is needed. + // CoreCLR runtime type loader can construct any T[] dynamically. + // IsDynamicCodeSupported is a [FeatureGuard] so this branch is + // dead-coded under PublishAot. return Array.CreateInstance (elementType, length); } - // NativeAOT — resolve the closed array type via the per-rank typemap - // (filled in at startup from generated __ArrayMapRank{N} sentinels) and - // construct via AOT-safe Array.CreateInstanceFromArrayType. + // NativeAOT: resolve via per-rank typemap + Array.CreateInstanceFromArrayType. if (TrimmableTypeMap.Instance.TryGetArrayType (elementType, out var arrayType)) { return Array.CreateInstanceFromArrayType (arrayType, length); } @@ -48,7 +44,7 @@ static Array ArrayCreateInstance (Type elementType, int length) $"Add an [assembly: TypeMap] entry for the closed array type or report an issue."); } - #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path + #pragma warning disable IL3050 // legacy fallback path return Array.CreateInstance (elementType, length); #pragma warning restore IL3050 } diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index f9f14b587bb..04995ea206c 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -10,17 +10,9 @@ namespace Java.Interop { /// - /// AOT-safe factory for creating typed containers (lists, collections, dictionaries) - /// without using MakeGenericType(). + /// AOT-safe factory for creating typed containers (lists, collections, dictionaries). + /// Array creation lives in JNIEnv.ArrayCreateInstance. /// - /// - /// Array creation has moved out of this factory: - /// now branches on - /// - /// and uses either (CoreCLR / Mono) or - /// the per-rank trimmable typemap + - /// (NativeAOT). - /// public abstract class JavaPeerContainerFactory { /// diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs index cb5b980c2f0..4f6f7c4f8ff 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -44,8 +44,7 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) { - // First-wins: each (peer, rank) pair has its TypeMap entry in exactly one - // assembly. Walk the universes and stop at the first hit. + // First-wins: each (peer, rank) lives in exactly one assembly's typemap. foreach (var universe in _universes) { if (universe.TryGetArrayType (jniElementTypeName, rank, out arrayType)) { return true; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs index c458b0ce00d..69806bf0fd6 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -28,22 +28,7 @@ interface ITypeMapWithAliasing bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); /// - /// Looks up the closed managed array type for a given element JNI name and rank. - /// E.g. ("java/lang/String", 2)typeof(string[][]). + /// Looks up the closed managed array type for the given element JNI name and 1-based rank. /// - /// - /// The JNI name of the array element type (the bare element name, NOT the JNI array - /// form — no leading '['). E.g. "java/lang/String", not - /// "[Ljava/lang/String;". - /// - /// 1-based array rank. Supported values: 1, 2, 3. - /// The closed managed array type on success. - /// True when an entry exists for the (element, rank) pair. - /// - /// Returns false when no per-rank dictionary was supplied at initialization - /// (e.g. CoreCLR builds with $(PublishAot) == false) — the runtime fork - /// in JNIEnv.ArrayCreateInstance short-circuits to Array.CreateInstance - /// in that case so the lookup is never reached. - /// bool TryGetArrayType (string jniElementTypeName, int rank, [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 960fb592654..86e59bc3a45 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -19,12 +19,8 @@ sealed class SingleUniverseTypeMap : ITypeMapWithAliasing readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // Per-rank array dictionaries indexed 0-based by (rank - 1): - // [0] is the rank-1 dictionary, [1] rank-2, etc. - // Length is whatever the generator emitted (defaults to 3, configurable via - // the _AndroidTrimmableTypeMapMaxArrayRank MSBuild property). Empty / null when - // the typemap universe was generated without array entries (e.g. CoreCLR builds - // with $(PublishAot) == false). Only consulted under NativeAOT via TryGetArrayType. + // Per-rank array dictionaries, 0-indexed by (rank - 1). Empty/null when no array + // entries were emitted (CoreCLR builds). Only consulted under NativeAOT. readonly IReadOnlyDictionary?[] _arrayMapsByRank; public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) @@ -103,10 +99,6 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) { - // The dictionary array is 0-based, so rank N lives at index N - 1. - // rank < 1 is invalid; rank > _arrayMapsByRank.Length means we don't have - // per-rank entries that high (either generator emitted up to a smaller - // MaxArrayRank, or the universe has no array entries at all). int index = rank - 1; if ((uint)index < (uint)_arrayMapsByRank.Length) { var dict = _arrayMapsByRank [index]; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 890f14e8dfc..af69f6d55c3 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -38,24 +38,15 @@ public class TrimmableTypeMap /// /// Initializes the singleton with a single merged typemap universe. - /// Called from in the generated root assembly - /// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds). /// public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) => Initialize (typeMap, proxyMap, arrayMapsByRank: null); /// /// Initializes the singleton with a single merged typemap universe plus per-rank - /// array dictionaries (used by JNIEnv.ArrayCreateInstance under NativeAOT). + /// array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). /// - /// - /// 0-based per-rank array of dictionaries — arrayMapsByRank[0] is the rank-1 - /// dictionary (JNI element name → typeof(T[])), [1] is rank-2, etc. - /// Length is whatever the generator emitted (defaults to 3, configurable via - /// $(_AndroidTrimmableTypeMapMaxArrayRank)). Null when the typemap was - /// generated without array entries (CoreCLR builds with $(PublishAot) == false). - /// Individual elements may be null when a particular rank has no entries. - /// + /// 0-indexed by (rank - 1); null when no array entries were emitted. public static void Initialize ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap, @@ -68,8 +59,6 @@ public static void Initialize ( /// /// Initializes the singleton with multiple per-assembly typemap universes. - /// Called from in the generated root assembly - /// (_Microsoft.Android.TypeMaps) when each assembly has its own typemap universe (Debug builds). /// public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) => Initialize (typeMaps, proxyMaps, perUniverseArrayMaps: null); @@ -78,12 +67,7 @@ public static void Initialize (IReadOnlyDictionary[] typeMaps, IRe /// Initializes the singleton with multiple per-assembly typemap universes plus /// per-universe per-rank array dictionaries. /// - /// - /// Jagged array indexed first by universe, then 0-based by (rank - 1) within that - /// universe. perUniverseArrayMaps[i][r] is universe i's rank-(r+1) - /// dictionary. Null when no typemap assembly emitted array entries (CoreCLR builds); - /// individual outer elements may be null for universes that didn't emit array entries. - /// + /// Jagged: [universe][rank - 1]. public static void Initialize ( IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps, @@ -359,21 +343,7 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) return GetProxyForManagedType (type)?.GetContainerFactory (); } - /// - /// AOT-safe lookup of the closed managed array type for the given element type. - /// Used by JNIEnv.ArrayCreateInstance under NativeAOT (where dynamic - /// Array.CreateInstance is not available for arbitrary element types). - /// Walks the element-type's array nesting (so byte[][] gets treated as a - /// rank-2 lookup of byte), resolves the leaf JNI element name (primitive - /// or peer reference), and queries the per-rank dictionary supplied at - /// . - /// - /// - /// True with the closed array on success. False on any miss - /// (unknown leaf, no per-rank dict supplied, no entry, rank > 3). The caller - /// in JNIEnv.ArrayCreateInstance throws a diagnostic - /// in that case. - /// + /// AOT-safe lookup of the closed managed array type for the given element type. internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) { if (elementType is null) { @@ -381,9 +351,7 @@ internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? return false; } - // Walk to the leaf element type, counting the array depth that is part of the - // element. The total rank we look up is depth+1 — the +1 accounts for the new - // outer rank that ArrayCreateInstance is constructing. + // Walk array nesting to the leaf; total rank = element depth + 1 (the outer rank we're constructing). var leaf = elementType; int elementDepth = 0; while (leaf.IsArray) { @@ -414,10 +382,7 @@ internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? return _typeMap.TryGetArrayType (leafJniName, rank, out arrayType); } - /// - /// JNI single-letter encodings for primitive element types. Reference: - /// . - /// + /// JNI single-letter encoding for primitive element types. static bool TryGetPrimitiveJniName (Type primitive, [NotNullWhen (true)] out string? jni) { if (primitive == typeof (bool)) { jni = "Z"; return true; } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 1967c2a1532..10504fafd19 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -20,14 +20,8 @@ <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f8ca3858a6f..d3797bbddb5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -70,13 +70,9 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool EmbedAssemblies { get; set; } /// - /// Maximum array rank for which the generator emits per-rank TypeMap entries - /// plus the matching __ArrayMapRank{N} sentinel TypeDefs. 0 (default) - /// disables array entry emission entirely. Should normally be set to - /// $(_AndroidTrimmableTypeMapMaxArrayRank) when $(PublishAot) == true - /// — under CoreCLR the runtime falls through to Array.CreateInstance - /// directly and these entries are never queried, so emitting them just adds - /// dead-weight attribute metadata. + /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} + /// sentinels and TypeMap entries. 0 disables. Set via + /// $(_AndroidTrimmableTypeMapMaxArrayRank). /// public int MaxArrayRank { get; set; } public string? ManifestPlaceholders { get; set; } From b19a2ce2fa871572f2484e9684b7d9e42afff4a7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 12:51:49 +0200 Subject: [PATCH 05/15] [TrimmableTypeMap] Wire up merged-universe + array entries path Replaces the NotSupportedException stub in RootTypeMapAssemblyGenerator with a fully functional shared-universe + array entries IL emit path, covering the Release build configuration when array typemap entries are enabled (`_AndroidTrimmableTypeMapMaxArrayRank > 0`). Each per-assembly typemap DLL still owns its own `__ArrayMapRank{N}` sentinel TypeDefs, so each rank's entries are split across N per-asm dicts. The new `CompositeStringTypeReadOnlyDictionary` wraps an array of source dicts and routes `TryGetValue` first-hit, letting the existing single-universe-with-arrays `TrimmableTypeMap.Initialize` overload consume them as a single per-rank dict each. Generated IL (shared + arrays): var arrayMapsByRank = new IReadOnlyDictionary?[maxRank]; for each rank r: var sources = new IReadOnlyDictionary?[asmCount]; sources[i] = TypeMapping.GetOrCreateExternalTypeMapping(); arrayMapsByRank[r-1] = new CompositeStringTypeReadOnlyDictionary(sources); TrimmableTypeMap.Initialize( TypeMapping.GetOrCreateExternalTypeMapping(), TypeMapping.GetOrCreateProxyTypeMapping(), arrayMapsByRank); Also extends `IgnoresAccessChecksTo` to cover per-asm DLLs in shared mode when arrays are enabled (root needs to reach their internal sealed `__ArrayMapRank{N}` types). Adds 4 generator unit tests; all 450 tests pass. Validated default Release CoreCLR trimmable lane: 917/0/3 (matches baseline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 143 ++++++++++++++++-- .../CompositeStringTypeReadOnlyDictionary.cs | 50 ++++++ .../RootTypeMapAssemblyGeneratorTests.cs | 57 ++++++- 3 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index c0a747b8e96..5fe80d6f534 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -118,9 +118,9 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access // internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android, - // and __TypeMapAnchor in each per-assembly typemap DLL). + // and __TypeMapAnchor / __ArrayMapRank{N} in each per-assembly typemap DLL). var accessTargets = new List { "Mono.Android" }; - if (!useSharedTypemapUniverse) { + if (!useSharedTypemapUniverse || maxArrayRank > 0) { accessTargets.AddRange (perAssemblyTypeMapNames); } pe.EmitIgnoresAccessChecksToAttribute (accessTargets); @@ -187,16 +187,12 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (useSharedTypemapUniverse) { if (maxArrayRank > 0) { - // Merged-universe + array entries: not yet wired up. The aggregate path - // covers both Debug and the (future) merged-with-arrays case; callers - // should fall back to per-assembly until shared-universe array sentinels - // are wired here. - throw new NotSupportedException ( - "Merged-universe (Release) + array typemap entries is not yet wired up. " + - "Use the aggregate/per-assembly path when maxArrayRank > 0."); + var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + } else { + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); } - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); } else { var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); @@ -378,6 +374,131 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle }); } + /// + /// Shared-universe + arrays IL emission. Builds a single merged main typemap (anchored on + /// Java.Lang.Object) plus a maxArrayRank-wide jagged composite of per-asm + /// rank dicts; calls the single-universe-with-arrays Initialize overload. + /// + static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, + EntityHandle anchorTypeHandle, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle initializeRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, + int maxArrayRank) + { + var count = perAssemblyTypeMapNames.Count; + + // MethodSpecs for GetOrCreateExternalTypeMapping<...> per (assembly, rank). + var rankSpecs = new EntityHandle [count, maxArrayRank]; + for (int i = 0; i < count; i++) { + var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); + for (int r = 0; r < maxArrayRank; r++) { + var rankRef = pe.Metadata.AddTypeReference (asmRef, default, + pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); + rankSpecs [i, r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + } + } + + var getExternalSharedSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); + var getProxySharedSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); + + // Composite dict ctor: CompositeStringTypeReadOnlyDictionary..ctor(IReadOnlyDictionary?[]) + var compositeTypeRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), + pe.Metadata.GetOrAddString ("CompositeStringTypeReadOnlyDictionary")); + var compositeCtorRef = AddCompositeCtorRef (pe, compositeTypeRef, iReadOnlyDictOpenRef, systemTypeRef); + + var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + + pe.EmitBody ("Initialize", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // loc 0: arrayMapsByRank = new IReadOnlyDictionary?[maxArrayRank] + encoder.LoadConstantI4 (maxArrayRank); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + encoder.StoreLocal (0); + + // For each rank r: arrayMapsByRank[r] = new CompositeStringTypeReadOnlyDictionary(sources) + // where sources is a fresh IReadOnlyDictionary?[count] populated from each asm. + for (int r = 0; r < maxArrayRank; r++) { + encoder.LoadLocal (0); + encoder.LoadConstantI4 (r); + + // new IReadOnlyDictionary?[count] + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + for (int u = 0; u < count; u++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (u); + encoder.OpCode (ILOpCode.Call); + encoder.Token (rankSpecs [u, r]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + + // new CompositeStringTypeReadOnlyDictionary(sources) + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (compositeCtorRef); + + // arrayMapsByRank[r] = composite + encoder.OpCode (ILOpCode.Stelem_ref); + } + + // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByRank) + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSharedSpec); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySharedSpec); + encoder.LoadLocal (0); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: localsSig => { + localsSig.WriteByte (0x07); // LOCAL_SIG + localsSig.WriteCompressedInteger (1); // 1 local + // loc 0: IReadOnlyDictionary?[] + localsSig.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + }); + } + + /// MemberRef for CompositeStringTypeReadOnlyDictionary..ctor(IReadOnlyDictionary<string, Type>?[]). + static MemberReferenceHandle AddCompositeCtorRef (PEAssemblyBuilder pe, TypeReferenceHandle compositeTypeRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (32); + blob.WriteByte (0x20); // HASTHIS + blob.WriteCompressedInteger (1); // parameter count + blob.WriteByte (0x01); // return type: void + blob.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + return pe.Metadata.AddMemberReference (compositeTypeRef, + pe.Metadata.GetOrAddString (".ctor"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank). + static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (96); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (3); // parameter count + blob.WriteByte (0x01); // return type: void + // Param 1: IReadOnlyDictionary + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // Param 2: IReadOnlyDictionary + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + // Param 3: IReadOnlyDictionary?[] + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs b/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs new file mode 100644 index 00000000000..166c277fb48 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Android.Runtime; + +/// +/// First-hit composite of multiple IReadOnlyDictionary<string, Type> sources. +/// Used to merge per-assembly per-rank array dicts in shared-universe mode without +/// enumerating -backed +/// dictionaries (which only support TryGetValue). +/// +sealed class CompositeStringTypeReadOnlyDictionary : IReadOnlyDictionary +{ + readonly IReadOnlyDictionary?[] _sources; + + public CompositeStringTypeReadOnlyDictionary (IReadOnlyDictionary?[] sources) + { + ArgumentNullException.ThrowIfNull (sources); + _sources = sources; + } + + public bool TryGetValue (string key, [MaybeNullWhen (false)] out Type value) + { + foreach (var source in _sources) { + if (source is not null && source.TryGetValue (key, out var v)) { + value = v; + return true; + } + } + value = null; + return false; + } + + public bool ContainsKey (string key) => TryGetValue (key, out _); + + public Type this [string key] => TryGetValue (key, out var v) + ? v + : throw new KeyNotFoundException (key); + + // Composite sources may be TypeMapping-backed dicts that throw on enumeration. + // The trimmable typemap consumer only calls TryGetValue, so these throw to fail fast. + public int Count => throw new NotSupportedException (); + public IEnumerable Keys => throw new NotSupportedException (); + public IEnumerable Values => throw new NotSupportedException (); + public IEnumerator> GetEnumerator () => throw new NotSupportedException (); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () => throw new NotSupportedException (); +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 84dd6a0a2f4..20374004ed1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -11,11 +11,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank); stream.Position = 0; return stream; } @@ -239,4 +239,57 @@ static List GetIgnoresAccessChecksToValues (MetadataReader reader) } return result; } + + [Fact] + public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 3); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + } + + [Fact] + public void Generate_MergedMode_WithArrays_ReferencesCompositeDictionary () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 3); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + .ToList (); + Assert.Contains ("CompositeStringTypeReadOnlyDictionary", typeRefNames); + } + + [Fact] + public void Generate_MergedMode_WithArrays_ReferencesPerAsmRankSentinels () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 2); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + .ToList (); + Assert.Contains ("__ArrayMapRank1", typeRefNames); + Assert.Contains ("__ArrayMapRank2", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeRefNames); + } + + [Fact] + public void Generate_MergedMode_WithArrays_HasIgnoresAccessChecksToPerAsm () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 3); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var accessAttrs = GetIgnoresAccessChecksToValues (reader); + Assert.Contains ("Mono.Android", accessAttrs); + Assert.Contains ("_App.TypeMap", accessAttrs); + Assert.Contains ("_Mono.Android.TypeMap", accessAttrs); + } } From e29ca74f292ac863a5cb0f56be48f6c23a9069b8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 12:55:38 +0200 Subject: [PATCH 06/15] [TrimmableTypeMap] Tighten comments in shared+arrays IL emit Removes obvious redundant inline comments and trims a couple of doc summaries; behavior unchanged. 450/450 unit tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 15 +++++---------- .../CompositeStringTypeReadOnlyDictionary.cs | 11 ++++------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 5fe80d6f534..9128c9d26c8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -375,9 +375,9 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle } /// - /// Shared-universe + arrays IL emission. Builds a single merged main typemap (anchored on - /// Java.Lang.Object) plus a maxArrayRank-wide jagged composite of per-asm - /// rank dicts; calls the single-universe-with-arrays Initialize overload. + /// Shared-universe + arrays IL emit. Single merged main map (anchored on + /// Java.Lang.Object) plus a per-rank array of CompositeStringTypeReadOnlyDictionary + /// over per-asm sources; calls the single-universe-with-arrays Initialize overload. /// static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, @@ -403,7 +403,6 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, var getExternalSharedSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySharedSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); - // Composite dict ctor: CompositeStringTypeReadOnlyDictionary..ctor(IReadOnlyDictionary?[]) var compositeTypeRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), pe.Metadata.GetOrAddString ("CompositeStringTypeReadOnlyDictionary")); @@ -415,19 +414,17 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // loc 0: arrayMapsByRank = new IReadOnlyDictionary?[maxArrayRank] + // loc 0 = new IReadOnlyDictionary?[maxArrayRank] encoder.LoadConstantI4 (maxArrayRank); encoder.OpCode (ILOpCode.Newarr); encoder.Token (externalDictTypeSpec); encoder.StoreLocal (0); - // For each rank r: arrayMapsByRank[r] = new CompositeStringTypeReadOnlyDictionary(sources) - // where sources is a fresh IReadOnlyDictionary?[count] populated from each asm. + // arrayMapsByRank[r] = new CompositeStringTypeReadOnlyDictionary() for (int r = 0; r < maxArrayRank; r++) { encoder.LoadLocal (0); encoder.LoadConstantI4 (r); - // new IReadOnlyDictionary?[count] encoder.LoadConstantI4 (count); encoder.OpCode (ILOpCode.Newarr); encoder.Token (externalDictTypeSpec); @@ -439,11 +436,9 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, encoder.OpCode (ILOpCode.Stelem_ref); } - // new CompositeStringTypeReadOnlyDictionary(sources) encoder.OpCode (ILOpCode.Newobj); encoder.Token (compositeCtorRef); - // arrayMapsByRank[r] = composite encoder.OpCode (ILOpCode.Stelem_ref); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs b/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs index 166c277fb48..060c302993c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs @@ -8,9 +8,9 @@ namespace Microsoft.Android.Runtime; /// /// First-hit composite of multiple IReadOnlyDictionary<string, Type> sources. -/// Used to merge per-assembly per-rank array dicts in shared-universe mode without -/// enumerating -backed -/// dictionaries (which only support TryGetValue). +/// Used to merge per-assembly per-rank array dicts in shared-universe mode. Only +/// TryGetValue / ContainsKey / indexer are supported; enumeration throws +/// to match -backed sources. /// sealed class CompositeStringTypeReadOnlyDictionary : IReadOnlyDictionary { @@ -25,8 +25,7 @@ public CompositeStringTypeReadOnlyDictionary (IReadOnlyDictionary? public bool TryGetValue (string key, [MaybeNullWhen (false)] out Type value) { foreach (var source in _sources) { - if (source is not null && source.TryGetValue (key, out var v)) { - value = v; + if (source is not null && source.TryGetValue (key, out value)) { return true; } } @@ -40,8 +39,6 @@ public bool TryGetValue (string key, [MaybeNullWhen (false)] out Type value) ? v : throw new KeyNotFoundException (key); - // Composite sources may be TypeMapping-backed dicts that throw on enumeration. - // The trimmable typemap consumer only calls TryGetValue, so these throw to fail fast. public int Count => throw new NotSupportedException (); public IEnumerable Keys => throw new NotSupportedException (); public IEnumerable Values => throw new NotSupportedException (); From 90b0b8fb31e8d0025411fcb95cfabe316dcbed72 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 13:09:56 +0200 Subject: [PATCH 07/15] [TrimmableTypeMap] Reject multi-dim arrays in TryGetArrayType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an IsSZArray guard inside the leaf walk so multi-dimensional arrays (e.g. byte[,], byte[,][]) bail out cleanly rather than walking through GetElementType() as if they were jagged szarray chains. JNI only supports single-dim zero-based arrays so any multi-dim element type reaching this method indicates a caller bug — return false rather than silently producing a misleading rank-N lookup. In practice this is unreachable from MCW-generated marshaling (JNI arrays are always szarrays) so behavior is unchanged for real callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index af69f6d55c3..56cc58b1ada 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -352,9 +352,14 @@ internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? } // Walk array nesting to the leaf; total rank = element depth + 1 (the outer rank we're constructing). + // Reject multi-dim arrays (byte[,]) — JNI only supports single-dim zero-based arrays. var leaf = elementType; int elementDepth = 0; while (leaf.IsArray) { + if (!leaf.IsSZArray) { + arrayType = null; + return false; + } var next = leaf.GetElementType (); if (next is null) { arrayType = null; From 86eaeff51411c4f32939246b26db8280d7369aed Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 13:18:37 +0200 Subject: [PATCH 08/15] [TrimmableTypeMap] Inline rank sentinel name generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces TypeMapAssemblyData.RankSentinels (a RankSentinelNames wrapper class storing a list of derived-from-rank strings) with a plain int MaxArrayRank field. The sentinel names are always `__ArrayMapRank{N}` by convention — both the per-asm emitter and the runtime/root loader hard-code that pattern — so the wrapper added no value beyond rank storage. Removes ~35 LOC and a public-ish helper class. 450/450 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 43 ++----------------- .../Generator/ModelBuilder.cs | 2 +- .../Generator/TypeMapAssemblyEmitter.cs | 15 +++---- .../Generator/TypeMapModelBuilderTests.cs | 19 +++----- 4 files changed, 18 insertions(+), 61 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 1fbe3e7d501..3bc2ccd9464 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -41,10 +41,10 @@ sealed class TypeMapAssemblyData public List AliasHolders { get; } = new (); /// - /// Names of the per-rank __ArrayMapRank{N} sentinel TypeDefs to emit. - /// Null disables array-shape emission. + /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} + /// sentinel TypeDefs and TypeMap entries. 0 disables. /// - public RankSentinelNames? RankSentinels { get; set; } + public int MaxArrayRank { get; set; } /// /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. @@ -52,43 +52,6 @@ sealed class TypeMapAssemblyData public List IgnoresAccessChecksTo { get; } = new (); } -/// -/// Names of the array-rank sentinel TypeDefs to emit, indexed 0-based by (rank - 1). -/// -sealed class RankSentinelNames -{ - public RankSentinelNames (IReadOnlyList names) - { - if (names is null) { - throw new ArgumentNullException (nameof (names)); - } - if (names.Count == 0) { - throw new ArgumentException ("At least one rank sentinel name must be provided.", nameof (names)); - } - Names = names; - } - - public IReadOnlyList Names { get; } - - public int Count => Names.Count; - - /// Returns the sentinel name for the given 1-based array rank, or null if out of range. - public string? GetForRank (int rank) => rank >= 1 && rank <= Names.Count ? Names [rank - 1] : null; - - /// Builds __ArrayMapRank1..__ArrayMapRank{maxRank}. - public static RankSentinelNames CreateDefault (int maxRank) - { - if (maxRank < 1) { - throw new ArgumentOutOfRangeException (nameof (maxRank), maxRank, "Must be >= 1."); - } - var names = new string [maxRank]; - for (int i = 0; i < maxRank; i++) { - names [i] = $"__ArrayMapRank{i + 1}"; - } - return new RankSentinelNames (names); - } -} - /// /// One [assembly: TypeMap("jni/name", typeof(Proxy))] or /// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index a4a9c7d152d..023d8a55ace 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -64,7 +64,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri ModuleName = moduleName, }; if (maxArrayRank > 0) { - model.RankSentinels = RankSentinelNames.CreateDefault (maxArrayRank); + model.MaxArrayRank = maxArrayRank; } // Invoker types are NOT emitted as separate proxies or TypeMap entries — diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 1861cebe51d..0f1a41abe0e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -277,19 +277,18 @@ void EmitAnchorType () } /// - /// Emits one internal sealed __ArrayMapRank{N} TypeDef per name in - /// . No-op when null. + /// Emits one internal sealed __ArrayMapRank{N} TypeDef for ranks + /// 1... No-op when 0. /// void EmitRankSentinels (TypeMapAssemblyData model) { - if (model.RankSentinels is null) { + if (model.MaxArrayRank <= 0) { return; } - var sentinels = model.RankSentinels; - _rankAnchorHandles = new EntityHandle [sentinels.Count]; - for (int i = 0; i < sentinels.Count; i++) { - _rankAnchorHandles [i] = EmitRankSentinel (sentinels.Names [i]); + _rankAnchorHandles = new EntityHandle [model.MaxArrayRank]; + for (int i = 0; i < model.MaxArrayRank; i++) { + _rankAnchorHandles [i] = EmitRankSentinel ($"__ArrayMapRank{i + 1}"); } } @@ -1311,7 +1310,7 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length || _rankAnchorHandles [anchorIndex] == default) { throw new InvalidOperationException ( $"No rank-{rank} anchor TypeDef was emitted for entry '{entry.JniName}'. " + - $"Ensure TypeMapAssemblyData.RankSentinels was set (with sufficient Count) before emit."); + $"Ensure TypeMapAssemblyData.MaxArrayRank was >= {rank} before emit."); } ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [anchorIndex]); } else { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 8f4e1687183..5a2a71c4bb4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -859,21 +859,17 @@ public void Build_DefaultEmitArrayEntriesFalse_NoArrayEntries () var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModel (new [] { peer }); - Assert.Null (model.RankSentinels); + Assert.Equal (0, model.MaxArrayRank); Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); } [Fact] - public void Build_EmitArrayEntries_SetsDefaultRankSentinels () + public void Build_EmitArrayEntries_SetsMaxArrayRank () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModelWithArrays (new [] { peer }); - Assert.NotNull (model.RankSentinels); - Assert.Equal (3, model.RankSentinels!.Count); - Assert.Equal ("__ArrayMapRank1", model.RankSentinels.Names [0]); - Assert.Equal ("__ArrayMapRank2", model.RankSentinels.Names [1]); - Assert.Equal ("__ArrayMapRank3", model.RankSentinels.Names [2]); + Assert.Equal (3, model.MaxArrayRank); } [Fact] @@ -884,14 +880,13 @@ public void Build_EmitArrayEntries_HonoursMaxArrayRank () var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model5 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 5); - Assert.Equal (5, model5.RankSentinels!.Count); - Assert.Equal ("__ArrayMapRank5", model5.RankSentinels.Names [4]); + Assert.Equal (5, model5.MaxArrayRank); var rank5Entries = model5.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (5, rank5Entries.Count); Assert.Equal ("Foo.Bar[][][][][], App", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); var model1 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 1); - Assert.Equal (1, model1.RankSentinels!.Count); + Assert.Equal (1, model1.MaxArrayRank); Assert.Single (model1.Entries.Where (e => e.AnchorRank is not null)); } @@ -1026,7 +1021,7 @@ public void FullPipeline_ArrayEntries_EmitsSentinelTypeDefs () var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrSentinels", maxArrayRank: 3); - Assert.NotNull (model.RankSentinels); + Assert.Equal (3, model.MaxArrayRank); EmitAndVerify (model, "ArrSentinels", (pe, reader) => { var typeNames = reader.TypeDefinitions @@ -1045,7 +1040,7 @@ public void FullPipeline_NoArrayEntries_DoesNotEmitSentinelTypeDefs () var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "NoArrSentinels.dll"); var model = ModelBuilder.Build (new [] { peer }, outputPath, "NoArrSentinels"); - Assert.Null (model.RankSentinels); + Assert.Equal (0, model.MaxArrayRank); EmitAndVerify (model, "NoArrSentinels", (pe, reader) => { var typeNames = reader.TypeDefinitions From 6c814cec40a390826d816066b76cbbb4bacb927b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 13:25:37 +0200 Subject: [PATCH 09/15] [TrimmableTypeMap] Drop CompositeStringTypeReadOnlyDictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds the multi-source first-hit logic into SingleUniverseTypeMap by making its rank dict storage jagged (`[rank-1][source]`). The composite class was just a thin wrapper around a list of dicts that did first-hit TryGetValue — exactly what the type map's own array lookup needs to do. Native jagged storage avoids the wrapper allocation, removes one type, and eliminates a member-ref + newobj from the generated IL per rank. Initialize signature changes: the single-universe-with-arrays overload now takes `IReadOnlyDictionary?[]?[]?` (jagged) instead of `IReadOnlyDictionary?[]?`. The aggregate path wraps each universe's 1-source rank dicts into 1-element-inner-jagged at construction time so SingleUniverseTypeMap's storage shape is uniform. Mono.Android Release builds clean. 449/449 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 45 +++++------------- .../CompositeStringTypeReadOnlyDictionary.cs | 47 ------------------- .../SingleUniverseTypeMap.cs | 21 +++++---- .../TrimmableTypeMap.cs | 23 ++++++--- .../RootTypeMapAssemblyGeneratorTests.cs | 14 ------ 5 files changed, 43 insertions(+), 107 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 9128c9d26c8..74012c47b65 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -376,8 +376,8 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle /// /// Shared-universe + arrays IL emit. Single merged main map (anchored on - /// Java.Lang.Object) plus a per-rank array of CompositeStringTypeReadOnlyDictionary - /// over per-asm sources; calls the single-universe-with-arrays Initialize overload. + /// Java.Lang.Object) plus a jagged [rank][source] array of per-asm + /// rank dicts; calls the single-universe-with-arrays Initialize overload. /// static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, @@ -403,24 +403,20 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, var getExternalSharedSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySharedSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); - var compositeTypeRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, - pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), - pe.Metadata.GetOrAddString ("CompositeStringTypeReadOnlyDictionary")); - var compositeCtorRef = AddCompositeCtorRef (pe, compositeTypeRef, iReadOnlyDictOpenRef, systemTypeRef); - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var arrayOfDictTypeSpec = MakeArrayOfIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef); pe.EmitBody ("Initialize", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // loc 0 = new IReadOnlyDictionary?[maxArrayRank] + // loc 0 = new IReadOnlyDictionary?[][maxArrayRank] (outer rank array) encoder.LoadConstantI4 (maxArrayRank); encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); + encoder.Token (arrayOfDictTypeSpec); encoder.StoreLocal (0); - // arrayMapsByRank[r] = new CompositeStringTypeReadOnlyDictionary() + // arrayMapsByRank[r] = new IReadOnlyDictionary?[count] { GetExternal(), ... } for (int r = 0; r < maxArrayRank; r++) { encoder.LoadLocal (0); encoder.LoadConstantI4 (r); @@ -436,9 +432,6 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, encoder.OpCode (ILOpCode.Stelem_ref); } - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (compositeCtorRef); - encoder.OpCode (ILOpCode.Stelem_ref); } @@ -455,27 +448,14 @@ static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, encodeLocals: localsSig => { localsSig.WriteByte (0x07); // LOCAL_SIG localsSig.WriteCompressedInteger (1); // 1 local - // loc 0: IReadOnlyDictionary?[] - localsSig.WriteByte (0x1D); // SZARRAY + // loc 0: IReadOnlyDictionary?[][] + localsSig.WriteByte (0x1D); // SZARRAY (outer) + localsSig.WriteByte (0x1D); // SZARRAY (inner) EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); }); } - /// MemberRef for CompositeStringTypeReadOnlyDictionary..ctor(IReadOnlyDictionary<string, Type>?[]). - static MemberReferenceHandle AddCompositeCtorRef (PEAssemblyBuilder pe, TypeReferenceHandle compositeTypeRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (32); - blob.WriteByte (0x20); // HASTHIS - blob.WriteCompressedInteger (1); // parameter count - blob.WriteByte (0x01); // return type: void - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - return pe.Metadata.AddMemberReference (compositeTypeRef, - pe.Metadata.GetOrAddString (".ctor"), pe.Metadata.GetOrAddBlob (blob)); - } - - /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank). + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank[][]). static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -487,8 +467,9 @@ static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); // Param 2: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Param 3: IReadOnlyDictionary?[] - blob.WriteByte (0x1D); + // Param 3: IReadOnlyDictionary?[][] + blob.WriteByte (0x1D); // SZARRAY (outer) + blob.WriteByte (0x1D); // SZARRAY (inner) EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs b/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs deleted file mode 100644 index 060c302993c..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/CompositeStringTypeReadOnlyDictionary.cs +++ /dev/null @@ -1,47 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Android.Runtime; - -/// -/// First-hit composite of multiple IReadOnlyDictionary<string, Type> sources. -/// Used to merge per-assembly per-rank array dicts in shared-universe mode. Only -/// TryGetValue / ContainsKey / indexer are supported; enumeration throws -/// to match -backed sources. -/// -sealed class CompositeStringTypeReadOnlyDictionary : IReadOnlyDictionary -{ - readonly IReadOnlyDictionary?[] _sources; - - public CompositeStringTypeReadOnlyDictionary (IReadOnlyDictionary?[] sources) - { - ArgumentNullException.ThrowIfNull (sources); - _sources = sources; - } - - public bool TryGetValue (string key, [MaybeNullWhen (false)] out Type value) - { - foreach (var source in _sources) { - if (source is not null && source.TryGetValue (key, out value)) { - return true; - } - } - value = null; - return false; - } - - public bool ContainsKey (string key) => TryGetValue (key, out _); - - public Type this [string key] => TryGetValue (key, out var v) - ? v - : throw new KeyNotFoundException (key); - - public int Count => throw new NotSupportedException (); - public IEnumerable Keys => throw new NotSupportedException (); - public IEnumerable Values => throw new NotSupportedException (); - public IEnumerator> GetEnumerator () => throw new NotSupportedException (); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () => throw new NotSupportedException (); -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs index 86e59bc3a45..ec699e6fb18 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -19,9 +19,10 @@ sealed class SingleUniverseTypeMap : ITypeMapWithAliasing readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // Per-rank array dictionaries, 0-indexed by (rank - 1). Empty/null when no array - // entries were emitted (CoreCLR builds). Only consulted under NativeAOT. - readonly IReadOnlyDictionary?[] _arrayMapsByRank; + // Jagged: [rank-1][source]. Empty/null inner array → no entries for that rank. + // Aggregate path uses inner-length 1 (one source per universe per rank); shared+arrays + // path uses inner-length N (N per-asm dicts merged via first-hit walk). + readonly IReadOnlyDictionary?[]?[] _arrayMapsByRank; public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) : this (typeMap, proxyTypeMap, arrayMapsByRank: null) @@ -31,13 +32,13 @@ public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOn public SingleUniverseTypeMap ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap, - IReadOnlyDictionary?[]? arrayMapsByRank) + IReadOnlyDictionary?[]?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyTypeMap); _typeMap = typeMap; _proxyTypeMap = proxyTypeMap; - _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?> (); + _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?[]?> (); } public IEnumerable GetTypes (string jniName) @@ -101,9 +102,13 @@ public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen ( { int index = rank - 1; if ((uint)index < (uint)_arrayMapsByRank.Length) { - var dict = _arrayMapsByRank [index]; - if (dict is not null && dict.TryGetValue (jniElementTypeName, out arrayType)) { - return true; + var sources = _arrayMapsByRank [index]; + if (sources is not null) { + foreach (var dict in sources) { + if (dict is not null && dict.TryGetValue (jniElementTypeName, out arrayType)) { + return true; + } + } } } arrayType = null; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 56cc58b1ada..5775c280121 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -46,11 +46,14 @@ public static void Initialize (IReadOnlyDictionary typeMap, IReadO /// Initializes the singleton with a single merged typemap universe plus per-rank /// array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). /// - /// 0-indexed by (rank - 1); null when no array entries were emitted. + /// + /// Jagged: [rank - 1][source]. TryGetArrayType walks the inner sources first-hit. + /// Null when no array entries were emitted. + /// public static void Initialize ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap, - IReadOnlyDictionary?[]? arrayMapsByRank) + IReadOnlyDictionary?[]?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); @@ -87,10 +90,18 @@ public static void Initialize ( var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { - universes [i] = new SingleUniverseTypeMap ( - typeMaps [i], - proxyMaps [i], - perUniverseArrayMaps?[i]); + IReadOnlyDictionary?[]?[]? perRank = null; + var perUniverseRanks = perUniverseArrayMaps?[i]; + if (perUniverseRanks is not null) { + // Wrap each rank's single-source dict into a 1-element source array so + // SingleUniverseTypeMap's jagged storage shape is uniform across paths. + perRank = new IReadOnlyDictionary?[]? [perUniverseRanks.Length]; + for (int r = 0; r < perUniverseRanks.Length; r++) { + var dict = perUniverseRanks [r]; + perRank [r] = dict is null ? null : new [] { dict }; + } + } + universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i], perRank); } InitializeCore (new AggregateTypeMap (universes)); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 20374004ed1..d10f6b98f76 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -249,20 +249,6 @@ public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () Assert.True (pe.HasMetadata); } - [Fact] - public void Generate_MergedMode_WithArrays_ReferencesCompositeDictionary () - { - using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], - useSharedTypemapUniverse: true, maxArrayRank: 3); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var typeRefNames = reader.TypeReferences - .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) - .ToList (); - Assert.Contains ("CompositeStringTypeReadOnlyDictionary", typeRefNames); - } - [Fact] public void Generate_MergedMode_WithArrays_ReferencesPerAsmRankSentinels () { From f4f288aba843c69e416705359f8f8c3e653b8863 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:10:10 +0200 Subject: [PATCH 10/15] [TrimmableTypeMap] Use shared rank anchors in Mono.Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defines `Microsoft.Android.Runtime.__ArrayMapRank1` ... `__ArrayMapRank8` as internal sealed types in Mono.Android. Per-asm typemap DLLs no longer emit their own rank sentinel TypeDefs — they reference the shared anchors instead. Per rank, every per-asm DLL contributes to the SAME TypeMap group, so `TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>()` returns ONE merged dict spanning all per-asm DLLs. Knock-on simplifications: - `SingleUniverseTypeMap` and `AggregateTypeMap` no longer hold rank dicts; the rank lookup table lives directly on `TrimmableTypeMap` (singleton) since it's logically global, not per-universe. - `ITypeMapWithAliasing.TryGetArrayType` removed — no longer needed. - `Initialize(typeMaps[], proxyMaps[], perUniverseArrayMaps[][])` collapses to `Initialize(typeMaps[], proxyMaps[], arrayMapsByRank[])` (1D, same shape as the single-universe overload). Both modes pass the SAME array-shape — the only thing that varies is whether the main typemap is single (shared) or N-element (aggregate). - Root assembly IL: shared and aggregate paths share an `EmitBuildArrayMapsByRank` helper that emits `maxArrayRank` calls to `GetOrCreateExternalTypeMapping()` directly. Shared mode no longer needs IgnoresAccessChecksTo on per-asm DLLs. - The supported maximum is `MaxSupportedArrayRank = 8`. The generator throws `ArgumentOutOfRangeException` if `_AndroidTrimmableTypeMapMaxArrayRank` exceeds it (with a message pointing at the Mono.Android types to add for more). Net -82 LOC. Mono.Android Release builds clean. 449/449 unit tests pass. Device test (Release CoreCLR trimmable): 917 / 0 errors / 3 pre-existing failures — matches baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 174 ++++++------------ .../Generator/TypeMapAssemblyEmitter.cs | 26 +-- .../TrimmableTypeMapGenerator.cs | 14 ++ .../AggregateTypeMap.cs | 12 -- .../ArrayMapAnchors.cs | 19 ++ .../ITypeMapWithAliasing.cs | 5 - .../SingleUniverseTypeMap.cs | 31 ---- .../TrimmableTypeMap.cs | 62 ++++--- .../RootTypeMapAssemblyGeneratorTests.cs | 6 +- .../Generator/TypeMapModelBuilderTests.cs | 31 ++-- 10 files changed, 149 insertions(+), 231 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 74012c47b65..fff26ad7f31 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -117,10 +117,11 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames); // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access - // internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android, - // and __TypeMapAnchor / __ArrayMapRank{N} in each per-assembly typemap DLL). + // 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. var accessTargets = new List { "Mono.Android" }; - if (!useSharedTypemapUniverse || maxArrayRank > 0) { + if (!useSharedTypemapUniverse) { accessTargets.AddRange (perAssemblyTypeMapNames); } pe.EmitIgnoresAccessChecksToAttribute (accessTargets); @@ -188,7 +189,7 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (useSharedTypemapUniverse) { if (maxArrayRank > 0) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } else { var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); @@ -199,8 +200,7 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (maxArrayRank > 0) { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - var arrayMapsByUniverseElemSpec = MakeArrayOfIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, arrayMapsByUniverseElemSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } else { var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); @@ -209,16 +209,47 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand } /// - /// Per-assembly + array-entries IL emission. Builds three locals: typeMaps[N], - /// proxyMaps[N], and a jagged perUniverseArrayMaps[N][maxArrayRank]; - /// passes them to the 3-arg aggregate TrimmableTypeMap.Initialize. + /// Emits IL onto that constructs a fresh + /// IReadOnlyDictionary<string, Type>?[maxArrayRank] on the stack, populated by + /// calls to TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{r+1}>() + /// against shared anchors in Mono.Android. The same 1D shape works for both shared and + /// aggregate runtime paths. + /// + static void EmitBuildArrayMapsByRank (PEAssemblyBuilder pe, InstructionEncoder encoder, + MemberReferenceHandle getExternalMemberRef, + TypeSpecificationHandle externalDictTypeSpec, + int maxArrayRank) + { + var monoAndroidRuntimeNs = pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + var rankSpecs = new EntityHandle [maxArrayRank]; + for (int r = 0; r < maxArrayRank; r++) { + var rankRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, monoAndroidRuntimeNs, + pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); + rankSpecs [r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + } + + // new IReadOnlyDictionary?[maxArrayRank] { GetExternal<__ArrayMapRank1>(), ... } + encoder.LoadConstantI4 (maxArrayRank); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + for (int r = 0; r < maxArrayRank; r++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (r); + encoder.OpCode (ILOpCode.Call); + encoder.Token (rankSpecs [r]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + } + + /// + /// Aggregate + arrays IL emit. Builds typeMaps[N], proxyMaps[N], plus a + /// flat arrayMapsByRank[maxArrayRank] from shared __ArrayMapRank{N} anchors. /// static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, - TypeSpecificationHandle arrayMapsByUniverseElemSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, int maxArrayRank) { @@ -226,20 +257,12 @@ static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, var getExternalSpecs = new EntityHandle [count]; var getProxySpecs = new EntityHandle [count]; - - // rankSpecs [universe, rankIndex] — 0-based rankIndex. - var rankSpecs = new EntityHandle [count, maxArrayRank]; - for (int i = 0; i < count; i++) { var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); - for (int r = 0; r < maxArrayRank; r++) { - var rankRef = pe.Metadata.AddTypeReference (asmRef, default, pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); - rankSpecs [i, r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); - } } pe.EmitBody ("Initialize", @@ -254,48 +277,23 @@ static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); - // var perUniverseArrayMaps = new IReadOnlyDictionary?[][N]; (loc 2) - EmitNewArrayLocal (encoder, count, arrayMapsByUniverseElemSpec, slot: 2); - - // Fill perUniverseArrayMaps[u] = new IReadOnlyDictionary?[maxArrayRank] - // populated with GetOrCreateExternalTypeMapping<__ArrayMapRank{r+1}_u>() - for (int u = 0; u < count; u++) { - encoder.LoadLocal (2); - encoder.LoadConstantI4 (u); - encoder.LoadConstantI4 (maxArrayRank); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); - for (int r = 0; r < maxArrayRank; r++) { - encoder.OpCode (ILOpCode.Dup); - encoder.LoadConstantI4 (r); - encoder.OpCode (ILOpCode.Call); - encoder.Token (rankSpecs [u, r]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, perUniverseArrayMaps) + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, new IReadOnlyDictionary?[]{ ... }) encoder.LoadLocal (0); encoder.LoadLocal (1); - encoder.LoadLocal (2); + EmitBuildArrayMapsByRank (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); }, encodeLocals: localsSig => { localsSig.WriteByte (0x07); // LOCAL_SIG - localsSig.WriteCompressedInteger (3); // count + localsSig.WriteCompressedInteger (2); // count // loc 0: IReadOnlyDictionary[] - localsSig.WriteByte (0x1D); // SZARRAY + localsSig.WriteByte (0x1D); EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); // loc 1: IReadOnlyDictionary[] localsSig.WriteByte (0x1D); EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // loc 2: IReadOnlyDictionary?[][] — outer SZARRAY of inner SZARRAY - localsSig.WriteByte (0x1D); - localsSig.WriteByte (0x1D); - EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); }); } @@ -318,17 +316,7 @@ static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHan } } - /// TypeSpec for IReadOnlyDictionary<string, Type>?[], used as the per-universe inner array type. - static TypeSpecificationHandle MakeArrayOfIReadOnlyDictTypeSpec (PEAssemblyBuilder pe, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (32); - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); - } - - /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], perUniverseArrayMaps[][]). + /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], arrayMapsByRank[]). static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -342,8 +330,7 @@ static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuil // Param 2: IReadOnlyDictionary[] blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Param 3: IReadOnlyDictionary?[][] - blob.WriteByte (0x1D); + // Param 3: IReadOnlyDictionary?[] blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, @@ -361,13 +348,10 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>() encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSpec); - // TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>() encoder.OpCode (ILOpCode.Call); encoder.Token (getProxySpec); - // TrimmableTypeMap.Initialize(typeMap, proxyMap) encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); @@ -376,86 +360,37 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle /// /// Shared-universe + arrays IL emit. Single merged main map (anchored on - /// Java.Lang.Object) plus a jagged [rank][source] array of per-asm - /// rank dicts; calls the single-universe-with-arrays Initialize overload. + /// Java.Lang.Object) plus a flat arrayMapsByRank[maxArrayRank] from shared + /// __ArrayMapRank{N} anchors. /// static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, - IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, int maxArrayRank) { - var count = perAssemblyTypeMapNames.Count; - - // MethodSpecs for GetOrCreateExternalTypeMapping<...> per (assembly, rank). - var rankSpecs = new EntityHandle [count, maxArrayRank]; - for (int i = 0; i < count; i++) { - var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); - for (int r = 0; r < maxArrayRank; r++) { - var rankRef = pe.Metadata.AddTypeReference (asmRef, default, - pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); - rankSpecs [i, r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); - } - } - var getExternalSharedSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySharedSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - var arrayOfDictTypeSpec = MakeArrayOfIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef); pe.EmitBody ("Initialize", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // loc 0 = new IReadOnlyDictionary?[][maxArrayRank] (outer rank array) - encoder.LoadConstantI4 (maxArrayRank); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (arrayOfDictTypeSpec); - encoder.StoreLocal (0); - - // arrayMapsByRank[r] = new IReadOnlyDictionary?[count] { GetExternal(), ... } - for (int r = 0; r < maxArrayRank; r++) { - encoder.LoadLocal (0); - encoder.LoadConstantI4 (r); - - encoder.LoadConstantI4 (count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); - for (int u = 0; u < count; u++) { - encoder.OpCode (ILOpCode.Dup); - encoder.LoadConstantI4 (u); - encoder.OpCode (ILOpCode.Call); - encoder.Token (rankSpecs [u, r]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByRank) + // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), new IReadOnlyDictionary?[]{ ... }) encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSharedSpec); encoder.OpCode (ILOpCode.Call); encoder.Token (getProxySharedSpec); - encoder.LoadLocal (0); + EmitBuildArrayMapsByRank (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); - }, - encodeLocals: localsSig => { - localsSig.WriteByte (0x07); // LOCAL_SIG - localsSig.WriteCompressedInteger (1); // 1 local - // loc 0: IReadOnlyDictionary?[][] - localsSig.WriteByte (0x1D); // SZARRAY (outer) - localsSig.WriteByte (0x1D); // SZARRAY (inner) - EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); }); } - /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank[][]). + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank[]). static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { @@ -467,9 +402,8 @@ static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); // Param 2: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Param 3: IReadOnlyDictionary?[][] - blob.WriteByte (0x1D); // SZARRAY (outer) - blob.WriteByte (0x1D); // SZARRAY (inner) + // Param 3: IReadOnlyDictionary?[] + blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 0f1a41abe0e..d449c0fa741 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -277,8 +277,10 @@ void EmitAnchorType () } /// - /// Emits one internal sealed __ArrayMapRank{N} TypeDef for ranks - /// 1... No-op when 0. + /// 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}>(). /// void EmitRankSentinels (TypeMapAssemblyData model) { @@ -287,26 +289,14 @@ void EmitRankSentinels (TypeMapAssemblyData model) } _rankAnchorHandles = new EntityHandle [model.MaxArrayRank]; + var ns = _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); for (int i = 0; i < model.MaxArrayRank; i++) { - _rankAnchorHandles [i] = EmitRankSentinel ($"__ArrayMapRank{i + 1}"); + _rankAnchorHandles [i] = _pe.Metadata.AddTypeReference ( + _pe.MonoAndroidRef, ns, + _pe.Metadata.GetOrAddString ($"__ArrayMapRank{i + 1}")); } } - EntityHandle EmitRankSentinel (string typeName) - { - var metadata = _pe.Metadata; - var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); - - return metadata.AddTypeDefinition ( - TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, - default, - metadata.GetOrAddString (typeName), - objectRef, - MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), - MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - } - void EmitMemberReferences () { _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5fb0bd65b60..090075d73bf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -9,6 +9,12 @@ 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) { @@ -38,6 +44,14 @@ public TrimmableTypeMapResult Execute ( _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); + 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) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs index 4f6f7c4f8ff..83925abd7c6 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -41,16 +41,4 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } - - public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) - { - // First-wins: each (peer, rank) lives in exactly one assembly's typemap. - foreach (var universe in _universes) { - if (universe.TryGetArrayType (jniElementTypeName, rank, out arrayType)) { - return true; - } - } - arrayType = null; - return false; - } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs b/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs new file mode 100644 index 00000000000..e0bf2f2568d --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs @@ -0,0 +1,19 @@ +#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 +// TrimmableTypeMap.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/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs index 69806bf0fd6..0849741e96d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -26,9 +26,4 @@ interface ITypeMapWithAliasing /// carries the attribute). /// bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); - - /// - /// Looks up the closed managed array type for the given element JNI name and 1-based rank. - /// - bool TryGetArrayType (string jniElementTypeName, int rank, [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 ec699e6fb18..95ac2fe4163 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -19,26 +19,12 @@ sealed class SingleUniverseTypeMap : ITypeMapWithAliasing readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // Jagged: [rank-1][source]. Empty/null inner array → no entries for that rank. - // Aggregate path uses inner-length 1 (one source per universe per rank); shared+arrays - // path uses inner-length N (N per-asm dicts merged via first-hit walk). - readonly IReadOnlyDictionary?[]?[] _arrayMapsByRank; - public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) - : this (typeMap, proxyTypeMap, arrayMapsByRank: null) - { - } - - public SingleUniverseTypeMap ( - IReadOnlyDictionary typeMap, - IReadOnlyDictionary proxyTypeMap, - IReadOnlyDictionary?[]?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyTypeMap); _typeMap = typeMap; _proxyTypeMap = proxyTypeMap; - _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?[]?> (); } public IEnumerable GetTypes (string jniName) @@ -97,21 +83,4 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr proxyType = null; return false; } - - public bool TryGetArrayType (string jniElementTypeName, int rank, [NotNullWhen (true)] out Type? arrayType) - { - int index = rank - 1; - if ((uint)index < (uint)_arrayMapsByRank.Length) { - var sources = _arrayMapsByRank [index]; - if (sources is not null) { - foreach (var dict in sources) { - if (dict is not null && dict.TryGetValue (jniElementTypeName, 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 5775c280121..698461182d2 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -28,12 +28,17 @@ public class TrimmableTypeMap "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 ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); - TrimmableTypeMap (ITypeMapWithAliasing typeMap) + TrimmableTypeMap (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) { _typeMap = typeMap; + _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?> (); } /// @@ -46,35 +51,36 @@ public static void Initialize (IReadOnlyDictionary typeMap, IReadO /// Initializes the singleton with a single merged typemap universe plus per-rank /// array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). /// - /// - /// Jagged: [rank - 1][source]. TryGetArrayType walks the inner sources first-hit. - /// Null when no array entries were emitted. - /// + /// 0-indexed by (rank - 1); null when no array entries were emitted. public static void Initialize ( IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap, - IReadOnlyDictionary?[]?[]? arrayMapsByRank) + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); - InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap, arrayMapsByRank)); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap), arrayMapsByRank); } /// /// Initializes the singleton with multiple per-assembly typemap universes. /// public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) - => Initialize (typeMaps, proxyMaps, perUniverseArrayMaps: null); + => Initialize (typeMaps, proxyMaps, arrayMapsByRank: null); /// /// Initializes the singleton with multiple per-assembly typemap universes plus - /// per-universe per-rank array dictionaries. + /// merged per-rank array dictionaries. /// - /// Jagged: [universe][rank - 1]. + /// + /// 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. + /// public static void Initialize ( IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps, - IReadOnlyDictionary?[]?[]? perUniverseArrayMaps) + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMaps); ArgumentNullException.ThrowIfNull (proxyMaps); @@ -84,36 +90,22 @@ 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 (perUniverseArrayMaps is not null && perUniverseArrayMaps.Length != typeMaps.Length) { - throw new ArgumentException ($"perUniverseArrayMaps.Length ({perUniverseArrayMaps.Length}) must equal typeMaps.Length ({typeMaps.Length}).", nameof (perUniverseArrayMaps)); - } var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { - IReadOnlyDictionary?[]?[]? perRank = null; - var perUniverseRanks = perUniverseArrayMaps?[i]; - if (perUniverseRanks is not null) { - // Wrap each rank's single-source dict into a 1-element source array so - // SingleUniverseTypeMap's jagged storage shape is uniform across paths. - perRank = new IReadOnlyDictionary?[]? [perUniverseRanks.Length]; - for (int r = 0; r < perUniverseRanks.Length; r++) { - var dict = perUniverseRanks [r]; - perRank [r] = dict is null ? null : new [] { dict }; - } - } - universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i], perRank); + universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); } - InitializeCore (new AggregateTypeMap (universes)); + InitializeCore (new AggregateTypeMap (universes), arrayMapsByRank); } - static void InitializeCore (ITypeMapWithAliasing typeMap) + static void InitializeCore (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) { lock (s_initLock) { if (s_instance is not null) { throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); } - var instance = new TrimmableTypeMap (typeMap); + var instance = new TrimmableTypeMap (typeMap, arrayMapsByRank); instance.RegisterNatives (); s_instance = instance; } @@ -395,7 +387,17 @@ internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? } int rank = elementDepth + 1; - return _typeMap.TryGetArrayType (leafJniName, rank, out arrayType); + int index = rank - 1; + if ((uint)index >= (uint)_arrayMapsByRank.Length) { + arrayType = null; + return false; + } + var dict = _arrayMapsByRank [index]; + if (dict is null) { + arrayType = null; + return false; + } + return dict.TryGetValue (leafJniName, out arrayType); } /// JNI single-letter encoding for primitive element types. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index d10f6b98f76..7abe1227635 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -266,7 +266,7 @@ public void Generate_MergedMode_WithArrays_ReferencesPerAsmRankSentinels () } [Fact] - public void Generate_MergedMode_WithArrays_HasIgnoresAccessChecksToPerAsm () + public void Generate_MergedMode_WithArrays_NoPerAsmAccessNeeded () { using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: true, maxArrayRank: 3); @@ -275,7 +275,7 @@ public void Generate_MergedMode_WithArrays_HasIgnoresAccessChecksToPerAsm () var accessAttrs = GetIgnoresAccessChecksToValues (reader); Assert.Contains ("Mono.Android", accessAttrs); - Assert.Contains ("_App.TypeMap", accessAttrs); - Assert.Contains ("_Mono.Android.TypeMap", accessAttrs); + // Shared-mode root never needs per-asm internal access — rank anchors live in Mono.Android. + Assert.DoesNotContain ("_App.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 5a2a71c4bb4..209f16dae5c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1016,7 +1016,7 @@ public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () public class ArrayEntriesPeBlob { [Fact] - public void FullPipeline_ArrayEntries_EmitsSentinelTypeDefs () + public void FullPipeline_ArrayEntries_ReferencesSharedRankAnchors () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); @@ -1024,18 +1024,26 @@ public void FullPipeline_ArrayEntries_EmitsSentinelTypeDefs () Assert.Equal (3, model.MaxArrayRank); EmitAndVerify (model, "ArrSentinels", (pe, reader) => { - var typeNames = reader.TypeDefinitions + // 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", typeNames); - Assert.Contains ("__ArrayMapRank2", typeNames); - Assert.Contains ("__ArrayMapRank3", typeNames); + var rankRefsToMonoAndroid = 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); }); } [Fact] - public void FullPipeline_NoArrayEntries_DoesNotEmitSentinelTypeDefs () + public void FullPipeline_NoArrayEntries_DoesNotReferenceRankAnchors () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "NoArrSentinels.dll"); @@ -1043,13 +1051,12 @@ public void FullPipeline_NoArrayEntries_DoesNotEmitSentinelTypeDefs () Assert.Equal (0, model.MaxArrayRank); EmitAndVerify (model, "NoArrSentinels", (pe, reader) => { - var typeNames = reader.TypeDefinitions - .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) .ToHashSet (StringComparer.Ordinal); - - Assert.DoesNotContain ("__ArrayMapRank1", typeNames); - Assert.DoesNotContain ("__ArrayMapRank2", typeNames); - Assert.DoesNotContain ("__ArrayMapRank3", typeNames); + Assert.DoesNotContain ("__ArrayMapRank1", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank2", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeRefNames); }); } From 1df33a734641315a038dafa8a565fa3172138433 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:14:32 +0200 Subject: [PATCH 11/15] [TrimmableTypeMap] Simplify TryGetArrayType Drops the unreachable elementType null check (param is non-nullable under #nullable enable), collapses the redundant rank/index arithmetic (rankIndex = elementDepth = rank - 1), and folds the JNI name resolution into a single ternary. Behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 44 +++++-------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 698461182d2..e73058b809e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -349,55 +349,33 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) /// AOT-safe lookup of the closed managed array type for the given element type. internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) { - if (elementType is null) { - arrayType = null; - return false; - } + arrayType = null; - // Walk array nesting to the leaf; total rank = element depth + 1 (the outer rank we're constructing). - // Reject multi-dim arrays (byte[,]) — JNI only supports single-dim zero-based arrays. + // Walk array nesting to the leaf; rankIndex = depth = (rank - 1). + // Reject multi-dim arrays (byte[,]) — JNI only supports szarrays. var leaf = elementType; - int elementDepth = 0; + int rankIndex = 0; while (leaf.IsArray) { if (!leaf.IsSZArray) { - arrayType = null; return false; } var next = leaf.GetElementType (); if (next is null) { - arrayType = null; return false; } leaf = next; - elementDepth++; + rankIndex++; } - string leafJniName; - if (leaf.IsPrimitive) { - if (!TryGetPrimitiveJniName (leaf, out var primitive)) { - arrayType = null; - return false; - } - leafJniName = primitive; - } else if (!TryGetJniNameForManagedType (leaf, out var jni)) { - arrayType = null; + if ((uint)rankIndex >= (uint)_arrayMapsByRank.Length || _arrayMapsByRank [rankIndex] is not { } dict) { return false; - } else { - leafJniName = jni; } - int rank = elementDepth + 1; - int index = rank - 1; - if ((uint)index >= (uint)_arrayMapsByRank.Length) { - arrayType = null; - return false; - } - var dict = _arrayMapsByRank [index]; - if (dict is null) { - arrayType = null; - return false; - } - return dict.TryGetValue (leafJniName, out arrayType); + string? leafJniName = leaf.IsPrimitive + ? TryGetPrimitiveJniName (leaf, out var p) ? p : null + : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; + + return leafJniName is not null && dict.TryGetValue (leafJniName, out arrayType); } /// JNI single-letter encoding for primitive element types. From b53a07bbdad8093cc8ccfbe6162813cd5fba3f30 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:22:23 +0200 Subject: [PATCH 12/15] [TrimmableTypeMap] Unify Initialize IL emit paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapses 4 IL emit paths × 4 MemberRef helpers in the root assembly generator down to 2 of each. Both paths always emit the 3-arg `Initialize` (with arrayMapsByRank); when maxArrayRank=0 the new `EmitArrayMapsByRankOrNull` helper just emits `ldnull`. Also drops the matching no-array `Initialize` C# overloads from `TrimmableTypeMap` since nothing calls them — the generated root assembly always picks the 3-arg shape now. The internal callers can pass `arrayMapsByRank: null`. Net -157 LOC. 449/449 unit tests pass; Mono.Android Release builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 239 ++++-------------- .../TrimmableTypeMap.cs | 18 +- 2 files changed, 50 insertions(+), 207 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index fff26ad7f31..159eda48de1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -186,66 +186,26 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + var initializeSingleRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + var initializeAggregateRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + if (useSharedTypemapUniverse) { - if (maxArrayRank > 0) { - var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapAndArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); - } else { - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); - } + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, + initializeSingleRef, externalDictTypeSpec, maxArrayRank); } else { - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - - if (maxArrayRank > 0) { - var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithAggregateTypeMapAndArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); - } else { - var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); - } - } - } - - /// - /// Emits IL onto that constructs a fresh - /// IReadOnlyDictionary<string, Type>?[maxArrayRank] on the stack, populated by - /// calls to TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{r+1}>() - /// against shared anchors in Mono.Android. The same 1D shape works for both shared and - /// aggregate runtime paths. - /// - static void EmitBuildArrayMapsByRank (PEAssemblyBuilder pe, InstructionEncoder encoder, - MemberReferenceHandle getExternalMemberRef, - TypeSpecificationHandle externalDictTypeSpec, - int maxArrayRank) - { - var monoAndroidRuntimeNs = pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); - var rankSpecs = new EntityHandle [maxArrayRank]; - for (int r = 0; r < maxArrayRank; r++) { - var rankRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, monoAndroidRuntimeNs, - pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); - rankSpecs [r] = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); - } - - // new IReadOnlyDictionary?[maxArrayRank] { GetExternal<__ArrayMapRank1>(), ... } - encoder.LoadConstantI4 (maxArrayRank); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); - for (int r = 0; r < maxArrayRank; r++) { - encoder.OpCode (ILOpCode.Dup); - encoder.LoadConstantI4 (r); - encoder.OpCode (ILOpCode.Call); - encoder.Token (rankSpecs [r]); - encoder.OpCode (ILOpCode.Stelem_ref); + EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, + initializeAggregateRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } } /// - /// Aggregate + arrays IL emit. Builds typeMaps[N], proxyMaps[N], plus a - /// flat arrayMapsByRank[maxArrayRank] from shared __ArrayMapRank{N} anchors. + /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either a + /// flat arrayMapsByRank[maxArrayRank] from shared __ArrayMapRank{N} + /// anchors or null when is 0. /// - static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, + static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, @@ -277,10 +237,10 @@ static void EmitInitializeWithAggregateTypeMapAndArrays (PEAssemblyBuilder pe, EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, new IReadOnlyDictionary?[]{ ... }) + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByRank-or-null) encoder.LoadLocal (0); encoder.LoadLocal (1); - EmitBuildArrayMapsByRank (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); @@ -337,9 +297,16 @@ static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuil pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); } + /// + /// Shared-universe IL emit. Single merged main map (anchored on Java.Lang.Object) + /// plus either a flat arrayMapsByRank[maxArrayRank] from shared + /// __ArrayMapRank{N} anchors or null when is 0. + /// static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle initializeRef) + MemberReferenceHandle initializeRef, + TypeSpecificationHandle externalDictTypeSpec, + int maxArrayRank) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -348,42 +315,12 @@ 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) encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSpec); encoder.OpCode (ILOpCode.Call); encoder.Token (getProxySpec); - encoder.OpCode (ILOpCode.Call); - encoder.Token (initializeRef); - encoder.OpCode (ILOpCode.Ret); - }); - } - - /// - /// Shared-universe + arrays IL emit. Single merged main map (anchored on - /// Java.Lang.Object) plus a flat arrayMapsByRank[maxArrayRank] from shared - /// __ArrayMapRank{N} anchors. - /// - static void EmitInitializeWithSingleTypeMapAndArrays (PEAssemblyBuilder pe, - EntityHandle anchorTypeHandle, - MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle initializeRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, - int maxArrayRank) - { - var getExternalSharedSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); - var getProxySharedSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - - pe.EmitBody ("Initialize", - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), - encoder => { - // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), new IReadOnlyDictionary?[]{ ... }) - encoder.OpCode (ILOpCode.Call); - encoder.Token (getExternalSharedSpec); - encoder.OpCode (ILOpCode.Call); - encoder.Token (getProxySharedSpec); - EmitBuildArrayMapsByRank (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); @@ -398,86 +335,40 @@ static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder blob.WriteByte (0x00); // DEFAULT (static) blob.WriteCompressedInteger (3); // parameter count blob.WriteByte (0x01); // return type: void - // Param 1: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // Param 2: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // Param 3: IReadOnlyDictionary?[] blob.WriteByte (0x1D); EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); return pe.Metadata.AddMemberReference (trimmableTypeMapRef, pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); } - static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, - IReadOnlyList perAssemblyTypeMapNames, - MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle initializeRef, - TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + /// + /// Emits IL that pushes either a fresh IReadOnlyDictionary<string, Type>?[maxArrayRank] + /// (when > 0) or ldnull. + /// + static void EmitArrayMapsByRankOrNull (PEAssemblyBuilder pe, InstructionEncoder encoder, + MemberReferenceHandle getExternalMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) { - var count = perAssemblyTypeMapNames.Count; - - var getExternalSpecs = new EntityHandle [count]; - var getProxySpecs = new EntityHandle [count]; - for (int i = 0; i < count; i++) { - var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); - var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, - default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); - getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); - getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); + if (maxArrayRank == 0) { + encoder.OpCode (ILOpCode.Ldnull); + return; } - pe.EmitBody ("Initialize", - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), - encoder => { - // var typeMaps = new IReadOnlyDictionary[N]; - encoder.LoadConstantI4 (count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); - encoder.OpCode (ILOpCode.Stloc_0); - - for (int i = 0; i < count; i++) { - encoder.OpCode (ILOpCode.Ldloc_0); - encoder.LoadConstantI4 (i); - encoder.OpCode (ILOpCode.Call); - encoder.Token (getExternalSpecs [i]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // var proxyMaps = new IReadOnlyDictionary[N]; - encoder.LoadConstantI4 (count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (proxyDictTypeSpec); - encoder.OpCode (ILOpCode.Stloc_1); - - for (int i = 0; i < count; i++) { - encoder.OpCode (ILOpCode.Ldloc_1); - encoder.LoadConstantI4 (i); - encoder.OpCode (ILOpCode.Call); - encoder.Token (getProxySpecs [i]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps) - encoder.OpCode (ILOpCode.Ldloc_0); - encoder.OpCode (ILOpCode.Ldloc_1); - encoder.OpCode (ILOpCode.Call); - encoder.Token (initializeRef); - encoder.OpCode (ILOpCode.Ret); - }, - encodeLocals: localsSig => { - // LOCAL_SIG header + 2 locals - localsSig.WriteByte (0x07); // LOCAL_SIG - localsSig.WriteCompressedInteger (2); // count - // local 0: IReadOnlyDictionary[] - localsSig.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // local 1: IReadOnlyDictionary[] - localsSig.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - }); + var monoAndroidRuntimeNs = pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + 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, + pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); + var rankSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (r); + encoder.OpCode (ILOpCode.Call); + encoder.Token (rankSpec); + encoder.OpCode (ILOpCode.Stelem_ref); + } } /// @@ -511,42 +402,6 @@ static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, Type pe.Metadata.GetOrAddString (methodName), pe.Metadata.GetOrAddBlob (blob)); } - /// - /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). - /// - static MemberReferenceHandle AddInitializeSingleRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (64); - blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (2); // parameter count - blob.WriteByte (0x01); // return type: void - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - return pe.Metadata.AddMemberReference (trimmableTypeMapRef, - pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); - } - - /// - /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>[], IReadOnlyDictionary<Type, Type>[]). - /// - static MemberReferenceHandle AddInitializeAggregateRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (64); - blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (2); // parameter count - blob.WriteByte (0x01); // return type: void - // Param 1: IReadOnlyDictionary[] - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // Param 2: IReadOnlyDictionary[] - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - return pe.Metadata.AddMemberReference (trimmableTypeMapRef, - pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); - } - /// /// Creates a TypeSpec for a closed IReadOnlyDictionary<K, V> generic type (for newarr). /// diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index e73058b809e..9e220e2d023 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -42,14 +42,8 @@ public class TrimmableTypeMap } /// - /// Initializes the singleton with a single merged typemap universe. - /// - public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) - => Initialize (typeMap, proxyMap, arrayMapsByRank: null); - - /// - /// Initializes the singleton with a single merged typemap universe plus per-rank - /// array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). + /// Initializes the singleton with a single merged typemap universe and optional + /// per-rank array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). /// /// 0-indexed by (rank - 1); null when no array entries were emitted. public static void Initialize ( @@ -63,13 +57,7 @@ public static void Initialize ( } /// - /// Initializes the singleton with multiple per-assembly typemap universes. - /// - public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) - => Initialize (typeMaps, proxyMaps, arrayMapsByRank: null); - - /// - /// Initializes the singleton with multiple per-assembly typemap universes plus + /// Initializes the singleton with multiple per-assembly typemap universes and optional /// merged per-rank array dictionaries. /// /// From f4564dfd198c2fc37d22e639840d89c1621d5830 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:24:30 +0200 Subject: [PATCH 13/15] [TrimmableTypeMap] Two small simplifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. EmitTypeMapLoader: only build the Initialize MemberRef for the path actually taken (was building both shared and aggregate variants unconditionally — added a stray MemberRef row to every assembly). 2. ModelBuilder.Brackets: replace LINQ Enumerable.Repeat fallback for rank>=4 with a straightforward StringBuilder loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 11 ++++++++++- .../Generator/RootTypeMapAssemblyGenerator.cs | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 023d8a55ace..58ba5a112d1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -441,9 +441,18 @@ static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List "[]", 2 => "[][]", 3 => "[][][]", - _ => string.Concat (Enumerable.Repeat ("[]", rank)), + _ => BuildBrackets (rank), }; + static string BuildBrackets (int rank) + { + var sb = new StringBuilder (rank * 2); + for (int i = 0; i < rank; i++) { + sb.Append ("[]"); + } + return sb.ToString (); + } + static bool IsJniPrimitiveKeyword (char c) => c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I' || c == 'J' || c == 'F' || c == 'D' || c == 'V'; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 159eda48de1..9f3f0d60b88 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -186,17 +186,17 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - var initializeSingleRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - var initializeAggregateRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); if (useSharedTypemapUniverse) { + var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, - initializeSingleRef, externalDictTypeSpec, maxArrayRank); + initializeRef, externalDictTypeSpec, maxArrayRank); } else { + var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeAggregateRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } } From 8e957888695ff34997476bcc6c39e15fd4f07be0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:26:18 +0200 Subject: [PATCH 14/15] [TrimmableTypeMap] Fold model setup into a single object initializer Drops a redundant `if (maxArrayRank > 0)` guard (the default is 0 already and the parameter is validated non-negative above) and inlines `moduleName`. Behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 58ba5a112d1..6c687784fc3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -57,15 +57,12 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); - string moduleName = Path.GetFileName (outputPath); var model = new TypeMapAssemblyData { AssemblyName = assemblyName, - ModuleName = moduleName, + ModuleName = Path.GetFileName (outputPath), + MaxArrayRank = maxArrayRank, }; - if (maxArrayRank > 0) { - model.MaxArrayRank = maxArrayRank; - } // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. From af650a51f4068722511f6201c271edbb778d1d6f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 29 Apr 2026 14:27:51 +0200 Subject: [PATCH 15/15] [TrimmableTypeMap] Drop stale 'TypeDef was emitted' check + message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The == default check on _rankAnchorHandles[i] was redundant — since EmitRankSentinels populates all slots up to MaxArrayRank, the length check is sufficient. The error message also referenced 'TypeDef' when we now emit TypeRefs (shared anchors live in Mono.Android). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index d449c0fa741..f5977b08a52 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1297,9 +1297,9 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) $"Rank-anchored TypeMap entries must be conditional (3-arg). Entry '{entry.JniName}' rank={rank}."); } int anchorIndex = rank - 1; - if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length || _rankAnchorHandles [anchorIndex] == default) { + if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length) { throw new InvalidOperationException ( - $"No rank-{rank} anchor TypeDef was emitted for entry '{entry.JniName}'. " + + $"No rank-{rank} anchor available for entry '{entry.JniName}'. " + $"Ensure TypeMapAssemblyData.MaxArrayRank was >= {rank} before emit."); } ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [anchorIndex]);