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..6a691882d6e 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,58 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - if (i == 0) { - primaryProxy = 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.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 ($"{assocProxy.Namespace}.{assocProxy.TypeName}", assemblyName), + AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName), }); } + 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)); + + // 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..b17ca32770a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -81,6 +81,8 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; + TypeReferenceHandle _javaPeerAliasesAttrRef; + MemberReferenceHandle _javaPeerAliasesAttrCtorRef; MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; @@ -150,6 +152,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 +196,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + _javaPeerAliasesAttrRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerAliasesAttribute")); _jniNativeMethodRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniNativeMethod")); @@ -289,6 +297,7 @@ void EmitMemberReferences () EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); + EmitJavaPeerAliasesAttributeCtorRef (); } void EmitTypeMapAttributeCtorRef () @@ -398,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 => { @@ -432,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); @@ -452,6 +459,59 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary() 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 ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (holder.Namespace), + metadata.GetOrAddString (holder.TypeName), + objectRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // Apply [JavaPeerAliases("key[0]", "key[1]", ...)] to the type + EmitJavaPeerAliasesAttribute (typeDefHandle, holder.AliasKeys); + } + + 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 ())); + } + + 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. + 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 + + _pe.Metadata.AddCustomAttribute (typeDefHandle, _javaPeerAliasesAttrCtorRef, _pe.Metadata.GetOrAddBlob (blobBuilder)); + } + + static void WriteSerializedString (BlobBuilder builder, string value) + { + var bytes = System.Text.Encoding.UTF8.GetBytes (value); + builder.WriteCompressedInteger (bytes.Length); + builder.WriteBytes (bytes); + } + 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..cfe3611936c 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -6,6 +6,30 @@ namespace Java.Interop { + /// + /// 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 + /// a plain holder class annotated with this attribute, which lists the indexed + /// TypeMap keys for each alias type. + /// + /// + /// 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]"). + /// + public string[] Aliases { get; } + + public JavaPeerAliasesAttribute (params string[] aliases) => Aliases = aliases; + } + /// /// 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..5a1a26cf33e 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 () { @@ -76,28 +76,84 @@ unsafe void RegisterNatives () } } - internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? 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 bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[]? types) { - type = GetProxyForJavaType (jniSimpleReference)?.TargetType; - return type is not null; + var proxies = GetProxiesForJniName (jniName); + if (proxies.Length == 0) { + types = null; + return false; + } + + types = new Type [proxies.Length]; + for (int i = 0; i < proxies.Length; i++) { + types [i] = proxies [i].TargetType; + } + return true; } /// - /// Resolves the for a managed type via the CLR - /// TypeMapping proxy dictionary. + /// 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. /// - /// - /// 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. - /// + JavaPeerProxy[] GetProxiesForJniName (string jniName) + { + return _jniProxyCache.GetOrAdd (jniName, static (name, self) => { + if (!self._typeMap.TryGetValue (name, out var mappedType)) { + return []; + } + + // Fast path: non-alias entry + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is not null) { + return [proxy]; + } + + // Slow path: alias holder — resolve each alias key + var aliases = mappedType.GetCustomAttribute (inherit: false); + if (aliases is null) { + return []; + } + + 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 (TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; + } + } + return null; + } JavaPeerProxy? GetProxyForManagedType (Type managedType) { if (managedType.IsGenericType && !managedType.IsGenericTypeDefinition) { @@ -105,8 +161,20 @@ 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; + } + + // Fast path: direct proxy lookup (non-alias types) + var proxy = proxyType.GetCustomAttribute (inherit: false); + if (proxy is not null) { + return proxy; + } + + // 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 s_noPeerSentinel; @@ -114,24 +182,18 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } - JavaPeerProxy? GetProxyForJavaType (string className) + static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases, Type targetType) { - var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => { - if (!self._typeMap.TryGetValue (name, out var mappedType)) { - return s_noPeerSentinel; + foreach (var key in aliases.Aliases) { + if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { + continue; } - - 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}'."); + var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); + if (aliasProxy is not null && TargetTypeMatches (targetType, aliasProxy.TargetType)) { + return aliasProxy; } - - return proxy; - }, this); - return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; + } + return null; } internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) @@ -158,7 +220,7 @@ 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; } @@ -279,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); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 1bdaf5ee65f..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; } - if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { - 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 17952f8e1c4..a1b78a23b1b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -715,4 +715,161 @@ 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 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)"); + } } 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) { } + } +}