From d326db174c62faf1fc55f5ad242ea911d1335aaf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 16:11:49 +0200 Subject: [PATCH 1/9] [TrimmableTypeMap] Implement alias support in codegen and runtime (#11103) Add alias holder approach for typemap aliases: when multiple .NET types map to the same JNI name, generate an alias holder class extending JavaPeerProxy + IJavaPeerAliases with explicit alias key list. Build-time: - IJavaPeerAliases interface with string[] Aliases property - AliasHolderData in the codegen model - ModelBuilder generates alias holder, [0]-based indexed entries, TypeMapAssociation links to holder - Emitter emits alias holder class with TargetType/CreateInstance throwing NotSupportedException, Aliases property returning key array - Self-application (AddCustomAttribute) for proxy and alias holder types Runtime: - GetProxyForManagedType: if (proxy is IJavaPeerAliases) -> iterate exact keys from Aliases property (zero cost for non-alias types) - GetAllTypesForJniName: detect alias holder, enumerate listed keys - ActivateInstance: same alias holder detection Tests: - 3 alias fixture types (3-way alias group) - Model builder tests for alias holders, 3-way aliases, mixed activation - Fixture scanner and integration tests Fixes: https://github.com/dotnet/android/issues/11103 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 32 +++- .../Generator/ModelBuilder.cs | 65 +++++--- .../Generator/TypeMapAssemblyEmitter.cs | 89 +++++++++++ .../Java.Interop/JavaPeerProxy.cs | 15 ++ .../TrimmableTypeMap.cs | 66 ++++++-- .../TrimmableTypeMapTypeManager.cs | 2 +- .../TypeMapAssemblyGeneratorTests.cs | 63 ++++++++ .../Generator/TypeMapModelBuilderTests.cs | 146 ++++++++++++++++-- .../TestFixtures/TestTypes.cs | 34 ++++ 9 files changed, 468 insertions(+), 44 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index d21c2e56da2..679423576f2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -35,6 +35,11 @@ sealed class TypeMapAssemblyData /// public List Associations { get; } = new (); + /// + /// Alias holder types to emit — one per alias group (≥2 types sharing a JNI name). + /// + public List AliasHolders { get; } = new (); + /// /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. /// @@ -252,7 +257,7 @@ sealed record ActivationCtorData /// /// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry. -/// Links a managed type to the proxy that holds its alias TypeMap entry. +/// Links a managed type to the alias holder that owns the alias group. /// sealed record TypeMapAssociationData { @@ -262,7 +267,30 @@ sealed record TypeMapAssociationData public required string SourceTypeReference { get; init; } /// - /// Assembly-qualified proxy type reference (the alias holder proxy). + /// Assembly-qualified proxy type reference (the alias holder). /// public required string AliasProxyTypeReference { get; init; } } + +/// +/// An alias holder class to generate in the TypeMap assembly. +/// Extends JavaPeerProxy and implements IJavaPeerAliases. +/// Emitted when multiple .NET types map to the same JNI name. +/// +sealed class AliasHolderData +{ + /// + /// Simple type name, e.g., "Test_AliasTarget_Aliases". + /// + public required string TypeName { get; init; } + + /// + /// Namespace for alias holder types. + /// + public string Namespace { get; init; } = "_TypeMap.Aliases"; + + /// + /// Indexed TypeMap keys, e.g., ["test/AliasTarget[0]", "test/AliasTarget[1]"]. + /// + public required List AliasKeys { get; init; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index b46f27b8ff3..19d16c1d329 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -108,13 +108,11 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri static void EmitPeers (TypeMapAssemblyData model, string jniName, List peersForName, string assemblyName, HashSet usedProxyNames) { - // First peer is the "primary" — it gets the base JNI name entry. - // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... - JavaPeerProxyData? primaryProxy = null; - for (int i = 0; i < peersForName.Count; i++) { - var peer = peersForName [i]; - string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; + bool isAliasGroup = peersForName.Count > 1; + if (!isAliasGroup) { + // Single peer — no aliases needed, emit directly with the base JNI name + var peer = peersForName [0]; bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; @@ -124,25 +122,52 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - if (i == 0) { - primaryProxy = proxy; + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, jniName)); + return; + } + + // Alias group: generate an alias holder and indexed entries for each peer. + // The base JNI name maps to the alias holder; each peer gets "[0]", "[1]", etc. + var aliasKeys = new List (); + string holderTypeName = jniName.Replace ('/', '_').Replace ('$', '_') + "_Aliases"; + var holderNamespace = "_TypeMap.Aliases"; + string holderRef = AssemblyQualify ($"{holderNamespace}.{holderTypeName}", assemblyName); + + for (int i = 0; i < peersForName.Count; i++) { + var peer = peersForName [i]; + string entryJniName = $"{jniName}[{i}]"; + aliasKeys.Add (entryJniName); + + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; + bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; + + JavaPeerProxyData? proxy = null; + if (hasProxy) { + proxy = BuildProxyType (peer, jniName, usedProxyNames, isAcw); + model.ProxyTypes.Add (proxy); } model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); - // Emit TypeMapAssociation for all proxy-backed types so managed → proxy - // lookup works even when the final JNI name differs from the type's attributes. - // Generic definitions are included — their proxy types derive from the - // non-generic `JavaPeerProxy` base so the CLR can load them without - // resolving an open generic argument. - var assocProxy = (i > 0 && primaryProxy != null) ? primaryProxy : proxy; - if (assocProxy != null) { - model.Associations.Add (new TypeMapAssociationData { - SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), - AliasProxyTypeReference = AssemblyQualify ($"{assocProxy.Namespace}.{assocProxy.TypeName}", assemblyName), - }); - } + // Link each alias type to the alias holder for trimming + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), + AliasProxyTypeReference = holderRef, + }); } + + // Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations) + model.Entries.Add (new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = holderRef, + TargetTypeReference = holderRef, + }); + + model.AliasHolders.Add (new AliasHolderData { + TypeName = holderTypeName, + Namespace = holderNamespace, + AliasKeys = aliasKeys, + }); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 423b27102a8..78cc9441e8a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -81,6 +81,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; + TypeReferenceHandle _iJavaPeerAliasesRef; MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; @@ -150,6 +151,10 @@ void EmitCore (TypeMapAssemblyData model) EmitProxyType (proxy, wrapperHandles); } + foreach (var holder in model.AliasHolders) { + EmitAliasHolderType (holder); + } + foreach (var entry in model.Entries) { EmitTypeMapAttribute (entry); } @@ -190,6 +195,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + _iJavaPeerAliasesRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerAliases")); _jniNativeMethodRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniNativeMethod")); @@ -452,6 +459,88 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (3, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + var typeDefHandle = metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (holder.Namespace), + metadata.GetOrAddString (holder.TypeName), + _javaPeerProxyNonGenericRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // Implement IJavaPeerAliases + metadata.AddInterfaceImplementation (typeDefHandle, _iJavaPeerAliasesRef); + + // .ctor — call base(jniName: "", targetType: typeof(void), invokerType: null) + var ctorHandle = _pe.EmitBody (".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_0); + encoder.LoadString (metadata.GetOrAddUserString ("")); + encoder.OpCode (ILOpCode.Ldtoken); + var voidTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Void")); + encoder.Token (voidTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.OpCode (ILOpCode.Ldnull); + encoder.Call (baseCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + // Self-apply the attribute: [AliasHolder] on the alias holder class itself. + var emptyBlob = _pe.BuildAttributeBlob (b => { }); + metadata.AddCustomAttribute (typeDefHandle, ctorHandle, emptyBlob); + + // CreateInstance → throw new NotSupportedException() + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); + + // get_Aliases → return new string[] { "key[0]", "key[1]", ... } + EmitAliasesGetter (holder.AliasKeys); + } + + void EmitAliasesGetter (List aliasKeys) + { + var metadata = _pe.Metadata; + var stringTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); + + _pe.EmitBody ("get_Aliases", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().SZArray ().String (), + p => { }), + encoder => { + encoder.LoadConstantI4 (aliasKeys.Count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (stringTypeRef); + for (int i = 0; i < aliasKeys.Count; i++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (i); + encoder.LoadString (metadata.GetOrAddUserString (aliasKeys [i])); + encoder.OpCode (ILOpCode.Stelem_ref); + } + encoder.OpCode (ILOpCode.Ret); + }); + } + void EmitCreateInstance (JavaPeerProxyData proxy) { if (!proxy.HasActivation) { diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index c4309d78f85..b61d8b669e9 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -6,6 +6,21 @@ namespace Java.Interop { + /// + /// Implemented by generated alias holder proxy types. When multiple .NET types + /// map to the same JNI name (e.g., JavaCollection and JavaCollection<T> + /// both map to "java/util/Collection"), the base JNI name entry points to + /// an alias holder that lists the indexed TypeMap keys for each alias type. + /// + public interface IJavaPeerAliases + { + /// + /// Gets the indexed TypeMap keys for this alias group (e.g., "java/util/Collection[0]", + /// "java/util/Collection[1]"). + /// + string[] Aliases { get; } + } + /// /// Base attribute class for generated proxy types that enable AOT-safe type mapping /// between Java and .NET types. diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 58491410db0..73f438bb4be 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -98,6 +98,28 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] /// closed generic peer still requires a closed at the call /// site and is tracked separately. /// + /// + /// Enumerates all proxy types for a JNI name, including alias entries. + /// If the base JNI name maps to an alias holder (implementing ), + /// enumerates the explicit alias keys. Otherwise yields only the primary type. + /// + internal IEnumerable GetAllTypesForJniName (string jniName) + { + if (!_typeMap.TryGetValue (jniName, out var primaryType)) { + yield break; + } + + var proxy = primaryType.GetCustomAttribute (inherit: false); + if (proxy is IJavaPeerAliases aliases) { + foreach (var key in aliases.Aliases) { + if (_typeMap.TryGetValue (key, out var aliasType)) { + yield return aliasType; + } + } + } else { + yield return primaryType; + } + } JavaPeerProxy? GetProxyForManagedType (Type managedType) { if (managedType.IsGenericType && !managedType.IsGenericTypeDefinition) { @@ -105,11 +127,21 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] } var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => { - if (self._proxyTypeMap.TryGetValue (type, out var proxyType)) { - return proxyType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; + if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) { + return s_noPeerSentinel; } - return s_noPeerSentinel; + var proxy = proxyType.GetCustomAttribute (inherit: false); + if (proxy is null) { + return s_noPeerSentinel; + } + + // If _proxyTypeMap mapped this type to an alias holder, resolve the actual proxy + if (proxy is IJavaPeerAliases aliases) { + return ResolveAlias (self, aliases, type) ?? s_noPeerSentinel; + } + + return proxy; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } @@ -121,19 +153,29 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] return s_noPeerSentinel; } - var proxy = mappedType.GetCustomAttribute (inherit: false); - if (proxy is null) { - // Alias typemap entries (for example "jni/name[1]") are not implemented yet. - // Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788. - throw new NotImplementedException ( - $"Trimmable typemap alias handling is not implemented yet for '{name}'."); - } - - return proxy; + return mappedType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } + /// + /// Resolves a specific managed type from an alias group by iterating + /// the explicit alias keys and checking TargetType on each proxy. + /// + static JavaPeerProxy? ResolveAlias (TrimmableTypeMap self, IJavaPeerAliases aliases, Type targetType) + { + foreach (var key in aliases.Aliases) { + if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { + continue; + } + var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); + if (aliasProxy is not null && aliasProxy.TargetType == targetType) { + return aliasProxy; + } + } + return null; + } + internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { jniName = GetProxyForManagedType (managedType)?.JniName; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 1bdaf5ee65f..9c808e8f699 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -21,7 +21,7 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return t; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { + foreach (var type in TrimmableTypeMap.Instance.GetAllTypesForJniName (jniSimpleReference)) { yield return type; } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 17952f8e1c4..7a8e77e7669 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -715,4 +715,67 @@ public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () $"Expected fewer RVA fields ({rvaFields.Count}) than total strings ({allStrings.Count}) due to deduplication"); } } + + [Fact] + public void Generate_AliasGroup_ProducesCorrectIndexedEntries () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + Assert.Equal (3, aliasPeers.Count); + + using var stream = GenerateAssembly (aliasPeers, "AliasRoundTrip"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Read all TypeMap attribute blobs + var typeMapBlobs = new List<(string? jniName, string? proxyRef, string? targetRef)> (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + if (attr.Constructor.Kind == HandleKind.MethodDefinition) + continue; + + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + if (prolog != 1) + continue; + + string? val1 = blobReader.ReadSerializedString (); + string? val2 = blobReader.ReadSerializedString (); + + // TypeMap has a jniName string arg; TypeMapAssociation has two Type args. + // We distinguish by checking if val1 looks like a JNI name (contains '/'). + if (val1 is not null && val1.Contains ('/')) { + string? val3 = blobReader.RemainingBytes > 2 ? blobReader.ReadSerializedString () : null; + typeMapBlobs.Add ((val1, val2, val3)); + } + } + + // Verify indexed entries: "test/AliasTarget[0]", "test/AliasTarget[1]", "test/AliasTarget[2]", and base "test/AliasTarget" + var jniNames = typeMapBlobs.Select (b => b.jniName).ToList (); + Assert.Contains ("test/AliasTarget", jniNames); + Assert.Contains ("test/AliasTarget[0]", jniNames); + Assert.Contains ("test/AliasTarget[1]", jniNames); + Assert.Contains ("test/AliasTarget[2]", jniNames); + + // Verify TypeMapAssociationAttribute is referenced (generic version) + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute`1", typeNames); + + // Verify 3 proxy types + 1 alias holder were emitted + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Equal (3, proxyTypes.Count); + + var aliasHolders = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Aliases") + .ToList (); + Assert.Single (aliasHolders); + + // Verify the alias holder implements IJavaPeerAliases + Assert.Contains ("IJavaPeerAliases", typeNames); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9376ec09f05..bd9d1ab6b0d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -63,15 +63,71 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () }; var model = BuildModel (peers); - // Two entries: primary "test/Dup" and alias "test/Dup[1]" - Assert.Equal (2, model.Entries.Count); - Assert.Equal ("test/Dup", model.Entries [0].JniName); + // Three entries: "test/Dup[0]", "test/Dup[1]", and the base "test/Dup" → alias holder + Assert.Equal (3, model.Entries.Count); + Assert.Equal ("test/Dup[0]", model.Entries [0].JniName); Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); + Assert.Equal ("test/Dup", model.Entries [2].JniName); - // No associations when neither peer has a proxy (no activation ctor or invoker) - Assert.Empty (model.Associations); + // Both peers get associations to the alias holder + Assert.Equal (2, model.Associations.Count); + + // One alias holder + Assert.Single (model.AliasHolders); + Assert.Equal (2, model.AliasHolders [0].AliasKeys.Count); + } + + [Fact] + public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () + { + var peers = new List { + MakePeerWithActivation ("test/Triple", "Test.Alpha", "A"), + MakePeerWithActivation ("test/Triple", "Test.Beta", "A"), + MakePeerWithActivation ("test/Triple", "Test.Gamma", "A"), + }; + + var model = BuildModel (peers, "TripleAlias"); + // 3 indexed entries + 1 base entry → alias holder = 4 + Assert.Equal (4, model.Entries.Count); + Assert.Equal ("test/Triple[0]", model.Entries [0].JniName); + Assert.Equal ("test/Triple[1]", model.Entries [1].JniName); + Assert.Equal ("test/Triple[2]", model.Entries [2].JniName); + Assert.Equal ("test/Triple", model.Entries [3].JniName); + + // All three peers get associations to the alias holder + Assert.Equal (3, model.Associations.Count); + + // Three distinct proxy types + Assert.Equal (3, model.ProxyTypes.Count); + + // One alias holder with 3 keys + Assert.Single (model.AliasHolders); + Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); + } + + [Fact] + public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivation () + { + var peers = new List { + MakeMcwPeer ("test/Mixed", "Test.NoAct", "A"), + MakePeerWithActivation ("test/Mixed", "Test.WithAct", "A"), + }; + + var model = BuildModel (peers, "MixedAlias"); + // 2 indexed entries + 1 base entry → alias holder = 3 + Assert.Equal (3, model.Entries.Count); + Assert.Equal ("test/Mixed[0]", model.Entries [0].JniName); + Assert.Equal ("test/Mixed[1]", model.Entries [1].JniName); + Assert.Equal ("test/Mixed", model.Entries [2].JniName); + + // Only the alias peer with activation gets a proxy + Assert.Single (model.ProxyTypes); + Assert.Equal ("Test_WithAct_Proxy", model.ProxyTypes [0].TypeName); + + // Both peers get associations to alias holder + Assert.Equal (2, model.Associations.Count); } } @@ -183,14 +239,13 @@ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string m } [Fact] - public void Build_PeerWithActivation_CreatesAssociation () + public void Build_SinglePeer_NoAssociation () { + // Single peers don't need associations — only alias groups do var peer = MakePeerWithActivation ("my/app/MainActivity", "MyApp.MainActivity", "App"); var model = BuildModel (new [] { peer }, "MyTypeMap"); - var assoc = Assert.Single (model.Associations); - Assert.Equal ("MyApp.MainActivity, App", assoc.SourceTypeReference); - Assert.Equal ("_TypeMap.Proxies.MyApp_MainActivity_Proxy, MyTypeMap", assoc.AliasProxyTypeReference); + Assert.Empty (model.Associations); } [Fact] @@ -438,6 +493,79 @@ public void Build_InvokerType_NoProxyNoEntry () } } + public class FixtureAliases + { + [Fact] + public void Fixture_AliasTarget_ThreeTypesShareJniName () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + Assert.Equal (3, aliasPeers.Count); + } + + [Fact] + public void Fixture_AliasTarget_ProducesIndexedEntries () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + var model = BuildModel (aliasPeers, "AliasFixture"); + + // 3 indexed entries + 1 base entry → alias holder = 4 + Assert.Equal (4, model.Entries.Count); + Assert.Equal ("test/AliasTarget[0]", model.Entries [0].JniName); + Assert.Equal ("test/AliasTarget[1]", model.Entries [1].JniName); + Assert.Equal ("test/AliasTarget[2]", model.Entries [2].JniName); + Assert.Equal ("test/AliasTarget", model.Entries [3].JniName); + } + + [Fact] + public void Fixture_AliasTarget_EachPeerGetsDistinctProxy () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + var model = BuildModel (aliasPeers, "AliasFixture"); + Assert.Equal (3, model.ProxyTypes.Count); + + var proxyNames = model.ProxyTypes.Select (p => p.TypeName).ToList (); + Assert.Equal (proxyNames.Distinct ().Count (), proxyNames.Count); + } + + [Fact] + public void Fixture_AliasTarget_AssociationsLinkToAliasHolder () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + var model = BuildModel (aliasPeers, "AliasFixture"); + // All 3 peers get associations to the alias holder + Assert.Equal (3, model.Associations.Count); + + // All associations point to the same alias holder + var holderRef = model.Associations [0].AliasProxyTypeReference; + Assert.All (model.Associations, a => Assert.Equal (holderRef, a.AliasProxyTypeReference)); + Assert.Contains ("_Aliases", holderRef); + } + + [Fact] + public void Fixture_AliasTarget_GeneratesAliasHolder () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + var model = BuildModel (aliasPeers, "AliasFixture"); + Assert.Single (model.AliasHolders); + + var holder = model.AliasHolders [0]; + Assert.Equal ("_TypeMap.Aliases", holder.Namespace); + Assert.Equal (3, holder.AliasKeys.Count); + Assert.Equal ("test/AliasTarget[0]", holder.AliasKeys [0]); + Assert.Equal ("test/AliasTarget[1]", holder.AliasKeys [1]); + Assert.Equal ("test/AliasTarget[2]", holder.AliasKeys [2]); + } + } + public class FixtureGenericHolder { [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 776ca77c494..68734a21ae2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -914,3 +914,37 @@ public class DotFormatActivity : Android.App.Activity { protected DotFormatActivity (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } } + +// --- Alias test types --- +// Multiple .NET types mapping to the same JNI name (e.g., generic + non-generic collection wrappers). + +namespace MyApp.Aliases +{ + /// + /// Non-generic type registered as "test/AliasTarget" — forms the primary entry. + /// + [Register ("test/AliasTarget", DoNotGenerateAcw = true)] + public class AliasTarget : Java.Lang.Object + { + protected AliasTarget (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Generic type also registered as "test/AliasTarget" — forms an alias entry. + /// Mirrors the real-world pattern of JavaCollection/JavaCollection<T>. + /// + [Register ("test/AliasTarget", DoNotGenerateAcw = true)] + public class AliasTargetGeneric : Java.Lang.Object + { + protected AliasTargetGeneric (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Third type also registered as "test/AliasTarget" — tests 3-way alias groups. + /// + [Register ("test/AliasTarget", DoNotGenerateAcw = true)] + public class AliasTargetExtended : Java.Lang.Object + { + protected AliasTargetExtended (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } + } +} From 725a2f6ea84feaed38dc7e0c7b927b855b169dd7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 16:36:40 +0200 Subject: [PATCH 2/9] Address review: alias-aware resolution in GetProxyForJavaType and hierarchy walking - Rename ResolveAlias -> GetProxyFromAliases, use IsAssignableFrom - GetProxyForJavaType: return sentinel for alias holders (not real proxies) - TryGetProxyFromHierarchy: resolve alias groups when targetType is available - Add GetAliasesForJniName helper - Fix NotSupportedException IL: push string arg before newobj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 3 +- .../TrimmableTypeMap.cs | 44 ++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 78cc9441e8a..276d353c95e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -505,8 +505,9 @@ void EmitAliasHolderType (AliasHolderData holder) var emptyBlob = _pe.BuildAttributeBlob (b => { }); metadata.AddCustomAttribute (typeDefHandle, ctorHandle, emptyBlob); - // CreateInstance → throw new NotSupportedException() + // CreateInstance → throw new NotSupportedException("...") EmitCreateInstanceBody (encoder => { + encoder.LoadString (metadata.GetOrAddUserString ("Alias holders do not support direct activation.")); encoder.OpCode (ILOpCode.Newobj); encoder.Token (_notSupportedExceptionCtorRef); encoder.OpCode (ILOpCode.Throw); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 73f438bb4be..f511e10cf42 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -138,7 +138,7 @@ internal IEnumerable GetAllTypesForJniName (string jniName) // If _proxyTypeMap mapped this type to an alias holder, resolve the actual proxy if (proxy is IJavaPeerAliases aliases) { - return ResolveAlias (self, aliases, type) ?? s_noPeerSentinel; + return GetProxyFromAliases (self, aliases, type) ?? s_noPeerSentinel; } return proxy; @@ -153,29 +153,49 @@ internal IEnumerable GetAllTypesForJniName (string jniName) return s_noPeerSentinel; } - return mappedType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; + var proxy = mappedType.GetCustomAttribute (inherit: false); + + // Alias holders are not real proxies — skip them here. + // Callers that need alias resolution use GetProxyFromAliases directly. + if (proxy is IJavaPeerAliases) { + return s_noPeerSentinel; + } + + return proxy ?? s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } /// - /// Resolves a specific managed type from an alias group by iterating - /// the explicit alias keys and checking TargetType on each proxy. + /// Reads the alias holder's explicit key list and returns the proxy whose + /// TargetType is assignable from . /// - static JavaPeerProxy? ResolveAlias (TrimmableTypeMap self, IJavaPeerAliases aliases, Type targetType) + static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, IJavaPeerAliases aliases, Type targetType) { foreach (var key in aliases.Aliases) { if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { continue; } var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null && aliasProxy.TargetType == targetType) { + if (aliasProxy is not null && targetType.IsAssignableFrom (aliasProxy.TargetType)) { return aliasProxy; } } return null; } + /// + /// Reads the attribute from the TypeMap entry for the given JNI name. + /// Returns null when the entry is not an alias holder. + /// + IJavaPeerAliases? GetAliasesForJniName (string jniName) + { + if (!_typeMap.TryGetValue (jniName, out var mappedType)) { + return null; + } + return mappedType.GetCustomAttribute (inherit: false) as IJavaPeerAliases; + } + internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { jniName = GetProxyForManagedType (managedType)?.JniName; @@ -204,6 +224,18 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) if (proxy != null && (targetType is null || TargetTypeMatches (targetType, proxy.TargetType))) { return proxy; } + + // GetProxyForJavaType returns null for alias holders — + // resolve from the alias group when a targetType is available. + if (proxy is null && targetType is not null) { + var aliases = self.GetAliasesForJniName (className); + if (aliases is not null) { + var aliasProxy = GetProxyFromAliases (self, aliases, targetType); + if (aliasProxy is not null) { + return aliasProxy; + } + } + } } var super = JniEnvironment.Types.GetSuperclass (jniClass); From 8d5559c9c6ec1e53105001e1fd99960bd3033dd6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 16:45:29 +0200 Subject: [PATCH 3/9] Simplify: alias holder is plain class + JavaPeerAliasesAttribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't extend JavaPeerProxy for alias holders — this keeps the fast path (GetCustomAttribute()) clean with zero alias checks. - Replace IJavaPeerAliases interface with JavaPeerAliasesAttribute - Alias holder is now a plain class (extends Object, not JavaPeerProxy) - [JavaPeerAliases] attribute encodes alias keys in the metadata blob - No ctor/CreateInstance/TargetType methods on alias holder (not needed) - Runtime: only checks JavaPeerAliasesAttribute when JavaPeerProxy is null Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 107 +++++++----------- .../Java.Interop/JavaPeerProxy.cs | 17 ++- .../TrimmableTypeMap.cs | 63 ++++++----- .../TypeMapAssemblyGeneratorTests.cs | 4 +- 4 files changed, 90 insertions(+), 101 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 276d353c95e..d21178f0a5a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -81,7 +81,8 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; - TypeReferenceHandle _iJavaPeerAliasesRef; + TypeReferenceHandle _javaPeerAliasesAttrRef; + MemberReferenceHandle _javaPeerAliasesAttrCtorRef; MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; @@ -195,8 +196,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); - _iJavaPeerAliasesRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerAliases")); + _javaPeerAliasesAttrRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerAliasesAttribute")); _jniNativeMethodRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniNativeMethod")); @@ -296,6 +297,7 @@ void EmitMemberReferences () EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); + EmitJavaPeerAliasesAttributeCtorRef (); } void EmitTypeMapAttributeCtorRef () @@ -463,83 +465,54 @@ void EmitAliasHolderType (AliasHolderData holder) { var metadata = _pe.Metadata; - // Alias holder base ctor: JavaPeerProxy(string jniName, Type targetType, Type? invokerType) - var baseCtorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().String (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); + // Alias holders are plain classes (NOT JavaPeerProxy subclasses). + // GetCustomAttribute() returns null for these — the fast path + // stays clean. Aliases are discovered via [JavaPeerAliases] attribute only when needed. + var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); - var typeDefHandle = metadata.AddTypeDefinition ( + metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, metadata.GetOrAddString (holder.Namespace), metadata.GetOrAddString (holder.TypeName), - _javaPeerProxyNonGenericRef, + objectRef, MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - // Implement IJavaPeerAliases - metadata.AddInterfaceImplementation (typeDefHandle, _iJavaPeerAliasesRef); - - // .ctor — call base(jniName: "", targetType: typeof(void), invokerType: null) - var ctorHandle = _pe.EmitBody (".ctor", - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), - encoder => { - encoder.OpCode (ILOpCode.Ldarg_0); - encoder.LoadString (metadata.GetOrAddUserString ("")); - encoder.OpCode (ILOpCode.Ldtoken); - var voidTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Void")); - encoder.Token (voidTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.OpCode (ILOpCode.Ldnull); - encoder.Call (baseCtorRef); - encoder.OpCode (ILOpCode.Ret); - }); + // Apply [JavaPeerAliases("key[0]", "key[1]", ...)] to the type + EmitJavaPeerAliasesAttribute (holder.AliasKeys); + } - // Self-apply the attribute: [AliasHolder] on the alias holder class itself. - var emptyBlob = _pe.BuildAttributeBlob (b => { }); - metadata.AddCustomAttribute (typeDefHandle, ctorHandle, emptyBlob); + void EmitJavaPeerAliasesAttributeCtorRef () + { + // JavaPeerAliasesAttribute(params string[] aliases) — in Mono.Android, Java.Interop namespace + _javaPeerAliasesAttrCtorRef = _pe.AddMemberRef (_javaPeerAliasesAttrRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().SZArray ().String ())); + } - // CreateInstance → throw new NotSupportedException("...") - EmitCreateInstanceBody (encoder => { - encoder.LoadString (metadata.GetOrAddUserString ("Alias holders do not support direct activation.")); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (_notSupportedExceptionCtorRef); - encoder.OpCode (ILOpCode.Throw); - }); + void EmitJavaPeerAliasesAttribute (List aliasKeys) + { + // Encode the attribute blob: prolog (0x0001), then packed string array, then NumNamed (0x0000). + // The params string[] is encoded as: element count (uint32), then each string as SerializedString. + var blobBuilder = new BlobBuilder (); + blobBuilder.WriteUInt16 (1); // prolog + blobBuilder.WriteInt32 (aliasKeys.Count); // array length + foreach (var key in aliasKeys) { + WriteSerializedString (blobBuilder, key); + } + blobBuilder.WriteUInt16 (0); // NumNamed - // get_Aliases → return new string[] { "key[0]", "key[1]", ... } - EmitAliasesGetter (holder.AliasKeys); + var lastTypeDef = MetadataTokens.TypeDefinitionHandle (_pe.Metadata.GetRowCount (TableIndex.TypeDef)); + _pe.Metadata.AddCustomAttribute (lastTypeDef, _javaPeerAliasesAttrCtorRef, _pe.Metadata.GetOrAddBlob (blobBuilder)); } - void EmitAliasesGetter (List aliasKeys) + static void WriteSerializedString (BlobBuilder builder, string value) { - var metadata = _pe.Metadata; - var stringTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("String")); - - _pe.EmitBody ("get_Aliases", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, - rt => rt.Type ().SZArray ().String (), - p => { }), - encoder => { - encoder.LoadConstantI4 (aliasKeys.Count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (stringTypeRef); - for (int i = 0; i < aliasKeys.Count; i++) { - encoder.OpCode (ILOpCode.Dup); - encoder.LoadConstantI4 (i); - encoder.LoadString (metadata.GetOrAddUserString (aliasKeys [i])); - encoder.OpCode (ILOpCode.Stelem_ref); - } - encoder.OpCode (ILOpCode.Ret); - }); + var bytes = System.Text.Encoding.UTF8.GetBytes (value); + builder.WriteCompressedInteger (bytes.Length); + builder.WriteBytes (bytes); } void EmitCreateInstance (JavaPeerProxyData proxy) diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index b61d8b669e9..cfe3611936c 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -7,18 +7,27 @@ namespace Java.Interop { /// - /// Implemented by generated alias holder proxy types. When multiple .NET types + /// Attribute applied to generated alias holder types. When multiple .NET types /// map to the same JNI name (e.g., JavaCollection and JavaCollection<T> /// both map to "java/util/Collection"), the base JNI name entry points to - /// an alias holder that lists the indexed TypeMap keys for each alias type. + /// a plain holder class annotated with this attribute, which lists the indexed + /// TypeMap keys for each alias type. /// - public interface IJavaPeerAliases + /// + /// The alias holder is NOT a subclass — this ensures + /// GetCustomAttribute<JavaPeerProxy>() returns null for alias entries, + /// keeping the fast path (non-alias types) free of alias checks. + /// + [AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class JavaPeerAliasesAttribute : Attribute { /// /// Gets the indexed TypeMap keys for this alias group (e.g., "java/util/Collection[0]", /// "java/util/Collection[1]"). /// - string[] Aliases { get; } + public string[] Aliases { get; } + + public JavaPeerAliasesAttribute (params string[] aliases) => Aliases = aliases; } /// diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f511e10cf42..e5de4d3170f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -100,24 +100,35 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] /// /// /// Enumerates all proxy types for a JNI name, including alias entries. - /// If the base JNI name maps to an alias holder (implementing ), + /// If the base JNI name maps to an alias holder (annotated with ), /// enumerates the explicit alias keys. Otherwise yields only the primary type. /// internal IEnumerable GetAllTypesForJniName (string jniName) { if (!_typeMap.TryGetValue (jniName, out var primaryType)) { - yield break; + return []; } - var proxy = primaryType.GetCustomAttribute (inherit: false); - if (proxy is IJavaPeerAliases aliases) { - foreach (var key in aliases.Aliases) { - if (_typeMap.TryGetValue (key, out var aliasType)) { - yield return aliasType; - } + // Fast path: non-alias entry — most JNI names map directly to a proxy + if (primaryType.GetCustomAttribute (inherit: false) is not null) { + return [primaryType]; + } + + // Slow path: alias holder — enumerate the explicit alias keys + var aliases = primaryType.GetCustomAttribute (inherit: false); + if (aliases is null) { + return []; + } + + return GetAliasedProxyTypes (aliases); + } + + IEnumerable GetAliasedProxyTypes (JavaPeerAliasesAttribute aliases) + { + foreach (var key in aliases.Aliases) { + if (_typeMap.TryGetValue (key, out var aliasType)) { + yield return aliasType; } - } else { - yield return primaryType; } } JavaPeerProxy? GetProxyForManagedType (Type managedType) @@ -131,17 +142,19 @@ internal IEnumerable GetAllTypesForJniName (string jniName) return s_noPeerSentinel; } + // Fast path: direct proxy lookup (non-alias types) var proxy = proxyType.GetCustomAttribute (inherit: false); - if (proxy is null) { - return s_noPeerSentinel; + if (proxy is not null) { + return proxy; } - // If _proxyTypeMap mapped this type to an alias holder, resolve the actual proxy - if (proxy is IJavaPeerAliases aliases) { + // Slow path: _proxyTypeMap mapped this type to an alias holder — resolve from aliases + var aliases = proxyType.GetCustomAttribute (inherit: false); + if (aliases is not null) { return GetProxyFromAliases (self, aliases, type) ?? s_noPeerSentinel; } - return proxy; + return s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } @@ -153,15 +166,9 @@ internal IEnumerable GetAllTypesForJniName (string jniName) return s_noPeerSentinel; } - var proxy = mappedType.GetCustomAttribute (inherit: false); - - // Alias holders are not real proxies — skip them here. - // Callers that need alias resolution use GetProxyFromAliases directly. - if (proxy is IJavaPeerAliases) { - return s_noPeerSentinel; - } - - return proxy ?? s_noPeerSentinel; + // Alias holders don't have JavaPeerProxy — GetCustomAttribute returns null naturally. + // No alias check needed here; callers use GetAliasesForJniName when proxy is null. + return mappedType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } @@ -170,7 +177,7 @@ internal IEnumerable GetAllTypesForJniName (string jniName) /// Reads the alias holder's explicit key list and returns the proxy whose /// TargetType is assignable from . /// - static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, IJavaPeerAliases aliases, Type targetType) + static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases, Type targetType) { foreach (var key in aliases.Aliases) { if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { @@ -185,15 +192,15 @@ internal IEnumerable GetAllTypesForJniName (string jniName) } /// - /// Reads the attribute from the TypeMap entry for the given JNI name. + /// Reads the from the TypeMap entry for the given JNI name. /// Returns null when the entry is not an alias holder. /// - IJavaPeerAliases? GetAliasesForJniName (string jniName) + JavaPeerAliasesAttribute? GetAliasesForJniName (string jniName) { if (!_typeMap.TryGetValue (jniName, out var mappedType)) { return null; } - return mappedType.GetCustomAttribute (inherit: false) as IJavaPeerAliases; + return mappedType.GetCustomAttribute (inherit: false); } internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 7a8e77e7669..ddcd2e50475 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -775,7 +775,7 @@ public void Generate_AliasGroup_ProducesCorrectIndexedEntries () .ToList (); Assert.Single (aliasHolders); - // Verify the alias holder implements IJavaPeerAliases - Assert.Contains ("IJavaPeerAliases", typeNames); + // Verify the alias holder has JavaPeerAliasesAttribute + Assert.Contains ("JavaPeerAliasesAttribute", typeNames); } } From f80f09de8f6c15be76380a6d4c638f25ede2a341 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 17:09:24 +0200 Subject: [PATCH 4/9] Fix runtime alias API: TryGetTargetTypes, exact match, tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace TryGetTargetType(string, out Type?) with TryGetTargetTypes(string, out Type[]?) — returns all surviving target types for alias groups, false when no mapping or all trimmed - GetProxyFromAliases: exact type equality (==) not IsAssignableFrom - TryGetProxyFromHierarchy: resolve aliases even without targetType (returns first surviving proxy) - TypeManager: single TryGetTargetTypes call, no alias awareness - Remove dead methods: GetTargetTypesForAliasedJniName, GetAliasedTargetTypes - Add GetFirstProxyFromAliases for targetType-less resolution - 3 new integration tests: - alias holder extends Object not JavaPeerProxy - JavaPeerAliasesAttribute deserializes to correct keys - proxy types have self-applied attribute (MethodDef ctor) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 102 +++++++++--------- .../TrimmableTypeMapTypeManager.cs | 6 +- .../TypeMapAssemblyGeneratorTests.cs | 94 ++++++++++++++++ 3 files changed, 150 insertions(+), 52 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index e5de4d3170f..02d381f6902 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -76,60 +76,47 @@ unsafe void RegisterNatives () } } - internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) - { - type = GetProxyForJavaType (jniSimpleReference)?.TargetType; - return type is not null; - } - - /// - /// Resolves the for a managed type via the CLR - /// TypeMapping proxy dictionary. - /// - /// - /// The generator emits exactly one TypeMapAssociation per generic peer, - /// keyed by the open generic definition (Java erases generics, so one proxy - /// fits every closed instantiation). Closed instantiations are normalised to - /// their generic type definition before the lookup because the CLR lazy - /// dictionary does identity-based key matching - /// (see dotnet/runtime TypeMapLazyDictionary.cs). - /// is safe under full AOT + trim - /// (it is not RequiresDynamicCode). Java→managed construction of a - /// closed generic peer still requires a closed at the call - /// site and is tracked separately. - /// /// - /// Enumerates all proxy types for a JNI name, including alias entries. - /// If the base JNI name maps to an alias holder (annotated with ), - /// enumerates the explicit alias keys. Otherwise yields only the primary type. + /// Returns all target types mapped to a JNI name. For non-alias entries, returns a + /// single-element array. For alias groups, returns the surviving target types from + /// each alias key. Returns false when no mapping exists or all aliases were trimmed. /// - internal IEnumerable GetAllTypesForJniName (string jniName) + internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[]? types) { - if (!_typeMap.TryGetValue (jniName, out var primaryType)) { - return []; + if (!_typeMap.TryGetValue (jniName, out var mappedType)) { + types = null; + return false; } - // Fast path: non-alias entry — most JNI names map directly to a proxy - if (primaryType.GetCustomAttribute (inherit: false) is not null) { - return [primaryType]; + // Fast path: non-alias entry (the vast majority of JNI names) + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is not null) { + types = [proxy.TargetType]; + return true; } - // Slow path: alias holder — enumerate the explicit alias keys - var aliases = primaryType.GetCustomAttribute (inherit: false); + // Slow path: alias holder — collect surviving target types + var aliases = mappedType.GetCustomAttribute (inherit: false); if (aliases is null) { - return []; + types = null; + return false; } - return GetAliasedProxyTypes (aliases); - } - - IEnumerable GetAliasedProxyTypes (JavaPeerAliasesAttribute aliases) - { + var result = new List (); foreach (var key in aliases.Aliases) { - if (_typeMap.TryGetValue (key, out var aliasType)) { - yield return aliasType; + var aliasProxy = GetProxyForJavaType (key); + if (aliasProxy is not null) { + result.Add (aliasProxy.TargetType); } } + + if (result.Count == 0) { + types = null; + return false; + } + + types = result.ToArray (); + return true; } JavaPeerProxy? GetProxyForManagedType (Type managedType) { @@ -167,15 +154,14 @@ IEnumerable GetAliasedProxyTypes (JavaPeerAliasesAttribute aliases) } // Alias holders don't have JavaPeerProxy — GetCustomAttribute returns null naturally. - // No alias check needed here; callers use GetAliasesForJniName when proxy is null. return mappedType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } /// - /// Reads the alias holder's explicit key list and returns the proxy whose - /// TargetType is assignable from . + /// Returns the proxy whose TargetType exactly matches + /// from an alias group's explicit key list. /// static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases, Type targetType) { @@ -184,7 +170,7 @@ IEnumerable GetAliasedProxyTypes (JavaPeerAliasesAttribute aliases) continue; } var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null && targetType.IsAssignableFrom (aliasProxy.TargetType)) { + if (aliasProxy is not null && aliasProxy.TargetType == targetType) { return aliasProxy; } } @@ -192,9 +178,23 @@ IEnumerable GetAliasedProxyTypes (JavaPeerAliasesAttribute aliases) } /// - /// Reads the from the TypeMap entry for the given JNI name. - /// Returns null when the entry is not an alias holder. + /// Returns the first surviving proxy from an alias group's key list. + /// Used when no specific targetType is available. /// + static JavaPeerProxy? GetFirstProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases) + { + foreach (var key in aliases.Aliases) { + if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { + continue; + } + var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); + if (aliasProxy is not null) { + return aliasProxy; + } + } + return null; + } + JavaPeerAliasesAttribute? GetAliasesForJniName (string jniName) { if (!_typeMap.TryGetValue (jniName, out var mappedType)) { @@ -233,11 +233,13 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } // GetProxyForJavaType returns null for alias holders — - // resolve from the alias group when a targetType is available. - if (proxy is null && targetType is not null) { + // resolve from the alias group. + if (proxy is null) { var aliases = self.GetAliasesForJniName (className); if (aliases is not null) { - var aliasProxy = GetProxyFromAliases (self, aliases, targetType); + var aliasProxy = targetType is not null + ? GetProxyFromAliases (self, aliases, targetType) + : GetFirstProxyFromAliases (self, aliases); if (aliasProxy is not null) { return aliasProxy; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 9c808e8f699..ffa3d509f8b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -21,8 +21,10 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return t; } - foreach (var type in TrimmableTypeMap.Instance.GetAllTypesForJniName (jniSimpleReference)) { - yield return type; + if (TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types)) { + foreach (var type in types) { + yield return type; + } } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index ddcd2e50475..a1b78a23b1b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -778,4 +778,98 @@ public void Generate_AliasGroup_ProducesCorrectIndexedEntries () // Verify the alias holder has JavaPeerAliasesAttribute Assert.Contains ("JavaPeerAliasesAttribute", typeNames); } + + [Fact] + public void Generate_AliasHolder_ExtendsObjectNotJavaPeerProxy () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + using var stream = GenerateAssembly (aliasPeers, "AliasBaseType"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var aliasHolder = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Namespace) == "_TypeMap.Aliases"); + + var baseTypeHandle = aliasHolder.BaseType; + Assert.Equal (HandleKind.TypeReference, baseTypeHandle.Kind); + var baseType = reader.GetTypeReference ((TypeReferenceHandle) baseTypeHandle); + Assert.Equal ("Object", reader.GetString (baseType.Name)); + Assert.Equal ("System", reader.GetString (baseType.Namespace)); + } + + [Fact] + public void Generate_AliasHolder_HasDeserializableAliasKeys () + { + var peers = ScanFixtures (); + var aliasPeers = peers.Where (p => p.JavaName == "test/AliasTarget").ToList (); + + using var stream = GenerateAssembly (aliasPeers, "AliasAttrBlob"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var aliasHolder = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Namespace) == "_TypeMap.Aliases"); + + var aliasHolderHandle = reader.TypeDefinitions + .First (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Aliases"); + + // Read the JavaPeerAliasesAttribute blob from the alias holder's custom attributes + var attrs = reader.GetCustomAttributes (aliasHolderHandle); + Assert.NotEmpty (attrs); + + // Find the attribute blob and parse it + foreach (var attrHandle in attrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + Assert.Equal (1, prolog); + + // Read the params string[] — encoded as int32 count + serialized strings + int count = blobReader.ReadInt32 (); + Assert.Equal (3, count); + + var keys = new List (); + for (int i = 0; i < count; i++) { + keys.Add (blobReader.ReadSerializedString ()!); + } + + Assert.Contains ("test/AliasTarget[0]", keys); + Assert.Contains ("test/AliasTarget[1]", keys); + Assert.Contains ("test/AliasTarget[2]", keys); + } + } + + [Fact] + public void Generate_ProxyTypes_HaveSelfAppliedAttribute () + { + var peers = ScanFixtures (); + var activityPeer = peers.First (p => p.JavaName == "android/app/Activity"); + + using var stream = GenerateAssembly (new [] { activityPeer }, "SelfApply"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var proxyTypeDef = reader.TypeDefinitions + .First (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Proxies"); + + // The proxy type should have a custom attribute applied to itself (self-application) + var attrs = reader.GetCustomAttributes (proxyTypeDef); + Assert.NotEmpty (attrs); + + // Verify the attribute's constructor is a MethodDef (i.e., defined in this assembly, + // meaning it's the proxy's own .ctor — self-application) + bool hasSelfApplied = false; + foreach (var attrHandle in attrs) { + var attr = reader.GetCustomAttribute (attrHandle); + if (attr.Constructor.Kind == HandleKind.MethodDefinition) { + hasSelfApplied = true; + break; + } + } + Assert.True (hasSelfApplied, "Proxy type should have a self-applied attribute (ctor is MethodDefinition)"); + } } From fc7dbb68217d7c000ec3366ae5f8d62894ce7ca3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 17:17:55 +0200 Subject: [PATCH 5/9] Remove GetProxyForJavaType and _peerProxyCache A single-proxy-per-JNI-name cache violates the principle that one JNI name can map to multiple types. Replace with GetProxyForJniClass(string, Type?) which handles both direct entries and alias groups in a single call, with no per-JNI-name caching. - Remove _peerProxyCache (cached single proxy per JNI name) - Remove GetProxyForJavaType (returned single proxy) - Remove GetAliasesForJniName (no longer needed as separate method) - Add GetProxyForJniClass(className, targetType) that resolves direct proxies or alias groups based on targetType - Simplify TryGetProxyFromHierarchy to single GetProxyForJniClass call Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/xamarin-android-tools | 2 +- .../TrimmableTypeMap.cs | 145 ++++++++---------- 2 files changed, 63 insertions(+), 84 deletions(-) diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index d222cfe45c3..a4281785292 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit d222cfe45c30c4158211760b0f6df31c064165f5 +Subproject commit a4281785292065ab7f5c0298f0f88f51b73ffb2c diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 02d381f6902..4fb81ab422f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -30,7 +30,7 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; readonly ConcurrentDictionary _proxyCache = new (); - readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); + readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () { @@ -83,40 +83,76 @@ unsafe void RegisterNatives () /// internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[]? types) { - if (!_typeMap.TryGetValue (jniName, out var mappedType)) { + var proxies = GetProxiesForJniName (jniName); + if (proxies.Length == 0) { types = null; return false; } - // Fast path: non-alias entry (the vast majority of JNI names) - var proxy = mappedType.GetCustomAttribute (inherit: false); - if (proxy is not null) { - types = [proxy.TargetType]; - return true; + types = new Type [proxies.Length]; + for (int i = 0; i < proxies.Length; i++) { + types [i] = proxies [i].TargetType; } + return true; + } - // Slow path: alias holder — collect surviving target types - var aliases = mappedType.GetCustomAttribute (inherit: false); - if (aliases is null) { - types = null; - return false; - } + /// + /// Resolves and caches all proxies for a JNI name. For non-alias entries, returns a + /// single-element array. For alias groups, resolves each alias key and returns the + /// surviving proxies. Returns an empty array when no mapping exists or all aliases were trimmed. + /// + JavaPeerProxy[] GetProxiesForJniName (string jniName) + { + return _jniProxyCache.GetOrAdd (jniName, static (name, self) => { + if (!self._typeMap.TryGetValue (name, out var mappedType)) { + return []; + } - var result = new List (); - foreach (var key in aliases.Aliases) { - var aliasProxy = GetProxyForJavaType (key); - if (aliasProxy is not null) { - result.Add (aliasProxy.TargetType); + // Fast path: non-alias entry + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is not null) { + return [proxy]; } - } - if (result.Count == 0) { - types = null; - return false; - } + // Slow path: alias holder — resolve each alias key + var aliases = mappedType.GetCustomAttribute (inherit: false); + if (aliases is null) { + return []; + } - types = result.ToArray (); - return true; + var result = new List (); + foreach (var key in aliases.Aliases) { + if (self._typeMap.TryGetValue (key, out var aliasEntryType)) { + var aliasProxy = aliasEntryType.GetCustomAttribute (inherit: false); + if (aliasProxy is not null) { + result.Add (aliasProxy); + } + } + } + return result.Count > 0 ? result.ToArray () : []; + }, this); + } + + /// + /// Resolves the best proxy for a JNI class name, handling both direct entries and alias groups. + /// When targetType is available, finds the proxy whose TargetType matches. + /// When targetType is null, returns the first available proxy. + /// + JavaPeerProxy? GetProxyForJniClass (string className, Type? targetType) + { + var proxies = GetProxiesForJniName (className); + if (proxies.Length == 0) { + return null; + } + if (proxies.Length == 1 || targetType is null) { + return proxies [0]; + } + foreach (var proxy in proxies) { + if (proxy.TargetType == targetType) { + return proxy; + } + } + return null; } JavaPeerProxy? GetProxyForManagedType (Type managedType) { @@ -146,23 +182,6 @@ internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[] return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } - JavaPeerProxy? GetProxyForJavaType (string className) - { - var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => { - if (!self._typeMap.TryGetValue (name, out var mappedType)) { - return s_noPeerSentinel; - } - - // Alias holders don't have JavaPeerProxy — GetCustomAttribute returns null naturally. - return mappedType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; - }, this); - return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; - } - - /// - /// Returns the proxy whose TargetType exactly matches - /// from an alias group's explicit key list. - /// static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases, Type targetType) { foreach (var key in aliases.Aliases) { @@ -177,32 +196,6 @@ internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[] return null; } - /// - /// Returns the first surviving proxy from an alias group's key list. - /// Used when no specific targetType is available. - /// - static JavaPeerProxy? GetFirstProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases) - { - foreach (var key in aliases.Aliases) { - if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { - continue; - } - var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null) { - return aliasProxy; - } - } - return null; - } - - JavaPeerAliasesAttribute? GetAliasesForJniName (string jniName) - { - if (!_typeMap.TryGetValue (jniName, out var mappedType)) { - return null; - } - return mappedType.GetCustomAttribute (inherit: false); - } - internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { jniName = GetProxyForManagedType (managedType)?.JniName; @@ -227,24 +220,10 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - var proxy = self.GetProxyForJavaType (className); + var proxy = self.GetProxyForJniClass (className, targetType); if (proxy != null && (targetType is null || TargetTypeMatches (targetType, proxy.TargetType))) { return proxy; } - - // GetProxyForJavaType returns null for alias holders — - // resolve from the alias group. - if (proxy is null) { - var aliases = self.GetAliasesForJniName (className); - if (aliases is not null) { - var aliasProxy = targetType is not null - ? GetProxyFromAliases (self, aliases, targetType) - : GetFirstProxyFromAliases (self, aliases); - if (aliasProxy is not null) { - return aliasProxy; - } - } - } } var super = JniEnvironment.Types.GetSuperclass (jniClass); From d6fee8bd249c68a14866f5086b9032fe803c19c9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 16 Apr 2026 17:47:21 +0200 Subject: [PATCH 6/9] Fix OnRegisterNatives for alias groups Use GetProxiesForJniName to resolve all proxies (including aliases) and register natives for each ACW proxy. Previously, alias holders returned null from GetCustomAttribute() and native registration was silently skipped for aliased types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 4fb81ab422f..7f74df1f91c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -341,16 +341,19 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa return; } - if (!s_instance._typeMap.TryGetValue (className, out var type)) { + var proxies = s_instance.GetProxiesForJniName (className); + if (proxies.Length == 0) { return; } - var proxy = type.GetCustomAttribute (inherit: false); - if (proxy is IAndroidCallableWrapper acw) { - // Use the class reference passed from Java (via C++) — not JniType(className) - // which resolves via FindClass and may get a different class from a different ClassLoader. - using var jniType = new JniType (ref classRef, JniObjectReferenceOptions.Copy); - acw.RegisterNatives (jniType); + // Use the class reference passed from Java (via C++) — not JniType(className) + // which resolves via FindClass and may get a different class from a different ClassLoader. + // Registering natives on that other instance is silently wrong. + using var jniType = new JniType (ref classRef, JniObjectReferenceOptions.Copy); + foreach (var proxy in proxies) { + if (proxy is IAndroidCallableWrapper acw) { + acw.RegisterNatives (jniType); + } } } catch (Exception ex) { Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); From d02ec92ad29859e75f281295688f701e48a14305 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Apr 2026 09:23:28 +0200 Subject: [PATCH 7/9] Reset accidental submodule bumps to main Commit 64d3228e2 accidentally bumped external/Java.Interop and external/xamarin-android-tools submodule pointers. The newer xamarin-android-tools commit removed the 'log' parameter from Files.ExtractAll, causing CI build failures: ResolveLibraryProjectImports.cs(300,13): error CS1739: The best overload for 'ExtractAll' does not have a parameter named 'log' Reset both submodules back to origin/main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/xamarin-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index a4281785292..d222cfe45c3 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit a4281785292065ab7f5c0298f0f88f51b73ffb2c +Subproject commit d222cfe45c30c4158211760b0f6df31c064165f5 From 2b159e5d0148886016801f709319cad415cdb573 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 18 Apr 2026 22:34:31 +0200 Subject: [PATCH 8/9] Fix typemap merge regressions after rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 6 ++++++ .../Generator/TypeMapAssemblyEmitter.cs | 16 +++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 19d16c1d329..6a691882d6e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -123,6 +123,12 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, } model.Entries.Add (BuildEntry (peer, proxy, assemblyName, jniName)); + if (proxy != null && peer.IsGenericDefinition) { + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), + AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName), + }); + } return; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index d21178f0a5a..1ce5a33310c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -407,17 +407,9 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary() to instantiate the proxy - // at runtime for AOT-safe type resolution. - var selfAttrCtorRef = _pe.AddMemberRef (typeDefHandle, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - var selfAttrBlob = _pe.BuildAttributeBlob (b => { }); - metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorRef, selfAttrBlob); - // .ctor — pass the resolved JNI name, (for generic-definition base) target type, and // optional invoker type to the base proxy constructor. - _pe.EmitBody (".ctor", + var selfAttrCtorDef = _pe.EmitBody (".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { @@ -441,6 +433,12 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary() to instantiate the proxy + // at runtime for AOT-safe type resolution. + var selfAttrBlob = _pe.BuildAttributeBlob (b => { }); + metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorDef, selfAttrBlob); + // CreateInstance EmitCreateInstance (proxy); From f4a3a698df5a400342e1625f9e92f881a8b9ce94 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 21 Apr 2026 17:15:16 +0200 Subject: [PATCH 9/9] Address PR review findings: fix fragile handle resolution and generic alias matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmitAliasHolderType: capture TypeDefinitionHandle from AddTypeDefinition and pass it explicitly to EmitJavaPeerAliasesAttribute instead of re-deriving via GetRowCount(TableIndex.TypeDef) - GetProxyForJniClass/GetProxyFromAliases: use TargetTypeMatches() to handle open generic → closed generic matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 9 ++++----- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 1ce5a33310c..b17ca32770a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -469,7 +469,7 @@ void EmitAliasHolderType (AliasHolderData holder) var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); - metadata.AddTypeDefinition ( + var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, metadata.GetOrAddString (holder.Namespace), metadata.GetOrAddString (holder.TypeName), @@ -478,7 +478,7 @@ void EmitAliasHolderType (AliasHolderData holder) MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); // Apply [JavaPeerAliases("key[0]", "key[1]", ...)] to the type - EmitJavaPeerAliasesAttribute (holder.AliasKeys); + EmitJavaPeerAliasesAttribute (typeDefHandle, holder.AliasKeys); } void EmitJavaPeerAliasesAttributeCtorRef () @@ -490,7 +490,7 @@ void EmitJavaPeerAliasesAttributeCtorRef () p => p.AddParameter ().Type ().SZArray ().String ())); } - void EmitJavaPeerAliasesAttribute (List aliasKeys) + void EmitJavaPeerAliasesAttribute (TypeDefinitionHandle typeDefHandle, List aliasKeys) { // Encode the attribute blob: prolog (0x0001), then packed string array, then NumNamed (0x0000). // The params string[] is encoded as: element count (uint32), then each string as SerializedString. @@ -502,8 +502,7 @@ void EmitJavaPeerAliasesAttribute (List aliasKeys) } blobBuilder.WriteUInt16 (0); // NumNamed - var lastTypeDef = MetadataTokens.TypeDefinitionHandle (_pe.Metadata.GetRowCount (TableIndex.TypeDef)); - _pe.Metadata.AddCustomAttribute (lastTypeDef, _javaPeerAliasesAttrCtorRef, _pe.Metadata.GetOrAddBlob (blobBuilder)); + _pe.Metadata.AddCustomAttribute (typeDefHandle, _javaPeerAliasesAttrCtorRef, _pe.Metadata.GetOrAddBlob (blobBuilder)); } static void WriteSerializedString (BlobBuilder builder, string value) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 7f74df1f91c..5a1a26cf33e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -148,7 +148,7 @@ JavaPeerProxy[] GetProxiesForJniName (string jniName) return proxies [0]; } foreach (var proxy in proxies) { - if (proxy.TargetType == targetType) { + if (TargetTypeMatches (targetType, proxy.TargetType)) { return proxy; } } @@ -189,7 +189,7 @@ JavaPeerProxy[] GetProxiesForJniName (string jniName) continue; } var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null && aliasProxy.TargetType == targetType) { + if (aliasProxy is not null && TargetTypeMatches (targetType, aliasProxy.TargetType)) { return aliasProxy; } }