diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index d629fc83d2c..0d2b15f803c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -125,9 +125,15 @@ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); // Application and Instrumentation types cannot call registerNatives in their - // static initializer — the runtime isn't ready yet at that point. Emit a - // lazy one-time helper instead so the first managed callback can register - // the class just before invoking its native method. + // static initializer — the runtime isn't ready yet at that point. Instead we + // rely on ApplicationRegistration.registerApplications() (invoked from + // MonoPackageManager.LoadApplication, which runs from MonoRuntimeProvider.attachInfo) + // to register natives for these classes. However, Application.attachBaseContext + // fires *before* ContentProvider.attachInfo, so an override of attachBaseContext + // on a user Application subclass can invoke a native callback before + // registerApplications() has run. To cover that early-callback window we also + // emit a lazy one-time __md_registerNatives() helper and call it at the top of + // each generated native-method wrapper. if (type.CannotRegisterInStaticConstructor) { writer.Write ($$""" private static boolean __md_natives_registered; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index d61d716cbb3..b46f27b8ff3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -132,6 +132,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // 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 { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 70521a1473f..423b27102a8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -69,6 +69,7 @@ sealed class TypeMapAssemblyEmitter AssemblyReferenceHandle _javaInteropRef; TypeReferenceHandle _javaPeerProxyRef; + TypeReferenceHandle _javaPeerProxyNonGenericRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; TypeReferenceHandle _jniObjectReferenceRef; @@ -165,6 +166,8 @@ void EmitTypeReferences () var metadata = _pe.Metadata; _javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy`1")); + _javaPeerProxyNonGenericRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, @@ -352,14 +355,36 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().String (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); + + // Open generic definitions derive from the non-generic `JavaPeerProxy` abstract base. + // Using `JavaPeerProxy` with an open T would force the CLR to resolve a generic + // argument that isn't available via the TypeMapLazyDictionary loader, and using a + // placeholder like `Java.Lang.Object` leaks an incorrect TargetType into the typemap. + // The non-generic base takes `targetType` as a ctor parameter, so we can pass the real + // open-generic type token (a TypeRef, not a closed TypeSpec) and keep TargetType correct. + EntityHandle proxyBaseType; + MemberReferenceHandle baseCtorRef; + if (proxy.IsGenericDefinition) { + proxyBaseType = _javaPeerProxyNonGenericRef; + 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); + })); + } else { + var genericProxyBase = _pe.MakeGenericTypeSpec (_javaPeerProxyRef, targetTypeRef); + proxyBaseType = genericProxyBase; + baseCtorRef = _pe.AddMemberRef (genericProxyBase, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, @@ -373,13 +398,29 @@ 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", 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 (proxy.JniName)); + if (proxy.IsGenericDefinition) { + // Non-generic base ctor signature: (string, Type, Type?). Push the open-generic + // target type as the second argument. + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + } if (proxy.InvokerType != null) { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (_pe.ResolveTypeRef (proxy.InvokerType)); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 78ae91c7cfb..5a2e36bce10 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -50,7 +50,7 @@ public TrimmableTypeMapResult Execute ( // Collect Application/Instrumentation types that need deferred registerNatives var appRegTypes = allPeers - .Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract) + .Where (p => p.CannotRegisterInStaticConstructor && !p.DoNotGenerateAcw) .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) .ToList (); if (appRegTypes.Count > 0) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 6c5b51fef0d..58491410db0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -59,7 +59,16 @@ internal static void Initialize () unsafe void RegisterNatives () { - using var runtimeClass = new JniType ("mono/android/Runtime"u8); + // Use the `string` overload of `JniType` deliberately. Its underlying + // `JniEnvironment.Types.TryFindClass(string, bool)` tries raw JNI `FindClass` + // first and, if that fails, falls back to `Class.forName(name, true, info.Runtime.ClassLoader)`, + // which resolves via the runtime's app ClassLoader — the same one that loads + // `mono.android.Runtime` from the APK. + // The `ReadOnlySpan` overload (see external/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs) + // only calls raw JNI `FindClass`, which resolves via the system ClassLoader on + // Android and returns a different `Class` instance from the one JCWs reference. + // Registering natives on that other instance is silently wrong. + using var runtimeClass = new JniType ("mono/android/Runtime"); fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; var method = new JniNativeMethod (name, sig, onRegisterNatives); @@ -73,14 +82,34 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] 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. + /// JavaPeerProxy? GetProxyForManagedType (Type managedType) { + if (managedType.IsGenericType && !managedType.IsGenericTypeDefinition) { + managedType = managedType.GetGenericTypeDefinition (); + } + var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => { - if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) { - return s_noPeerSentinel; + if (self._proxyTypeMap.TryGetValue (type, out var proxyType)) { + return proxyType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; } - return proxyType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; + return s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } @@ -130,7 +159,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { var proxy = self.GetProxyForJavaType (className); - if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + if (proxy != null && (targetType is null || TargetTypeMatches (targetType, proxy.TargetType))) { return proxy; } } @@ -176,6 +205,47 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + /// + /// Match the proxy's stored target type against a hint from the caller. + /// The proxy's target type is the open generic definition for generic peers + /// (Java erases generics, so one proxy fits every closed instantiation), + /// so a plain check misses when the hint + /// is a closed instantiation. Walk the hint's base chain to find a generic + /// type whose definition equals the proxy's open target type. This covers + /// closed subclasses of an open generic class peer. + /// + /// + /// Implementers of an open generic interface peer are intentionally + /// not matched here: walks only the + /// JNI class chain (getSuperclass), never JNI interfaces, so the + /// proxy returned from that walk is always a class peer. Matching on + /// Type.GetInterfaces() would also force a trimmer + /// DynamicallyAccessedMembers(Interfaces) annotation up the chain + /// (ultimately into Java.Interop's CreatePeer API). If we ever need + /// to discover interface peers, the generator should emit an explicit + /// implementer→interface map so runtime can avoid reflection over + /// interface lists. + /// + internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) + { + if (targetType.IsAssignableFrom (proxyTargetType)) { + return true; + } + + if (!proxyTargetType.IsGenericTypeDefinition) { + return false; + } + + for (Type? t = targetType; t is not null; t = t.BaseType) { + if (t.IsGenericType && !t.IsGenericTypeDefinition && + t.GetGenericTypeDefinition () == proxyTargetType) { + return true; + } + } + + return false; + } + /// /// Gets the invoker type for an interface or abstract class from the proxy attribute. /// @@ -215,7 +285,9 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa var proxy = type.GetCustomAttribute (inherit: false); if (proxy is IAndroidCallableWrapper acw) { - using var jniType = new JniType (className); + // 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); } } catch (Exception ex) { diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index d38aad44fb9..ea1a6a2746b 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -172,11 +172,4 @@ - - - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs index bd600efc48c..5c166e75935 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs @@ -68,7 +68,7 @@ public override bool RunTask () ResolvedSymbols = symbols.Values.ToArray (); // Set ShrunkAssemblies for _RemoveRegisterAttribute and - // This should match the Condition on the _RemoveRegisterAttribute target + // This should match the Condition on the _RemoveRegisterAttribute target. if (PublishTrimmed) { if (!AndroidIncludeDebugSymbols) { var shrunkAssemblies = new List (OutputAssemblies.Length); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c80fdbb6936..8412d665597 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -73,16 +73,19 @@ public void Execute_WithTestFixtures_ProducesOutputs () } [Fact] - public void Execute_CollectsDeferredRegistrationTypes_ForConcreteApplicationAndInstrumentation () + public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + // Abstract Instrumentation/Application subtypes are included too: their native + // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class + // and must be registered via ApplicationRegistration.registerApplications (). Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes); Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes); - Assert.DoesNotContain ("my.app.BaseApplication", result.ApplicationRegistrationTypes); - Assert.DoesNotContain ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); - Assert.DoesNotContain ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.BaseApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index a99d95bfca0..17952f8e1c4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -98,12 +98,23 @@ public void Generate_ProxyType_UsesGenericJavaPeerProxyBase () Assert.NotEmpty (proxyTypes); Assert.All (proxyTypes, proxyType => { - Assert.Equal (HandleKind.TypeSpecification, proxyType.BaseType.Kind); - - var baseTypeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) proxyType.BaseType); - var baseTypeName = baseTypeSpec.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); - - Assert.StartsWith ("Java.Interop.JavaPeerProxy`1<", baseTypeName, StringComparison.Ordinal); + switch (proxyType.BaseType.Kind) { + case HandleKind.TypeSpecification: + // Non-generic target types derive from the closed `JavaPeerProxy`. + var baseTypeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) proxyType.BaseType); + var baseTypeName = baseTypeSpec.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + Assert.StartsWith ("Java.Interop.JavaPeerProxy`1<", baseTypeName, StringComparison.Ordinal); + break; + case HandleKind.TypeReference: + // Open generic target types derive from the non-generic `JavaPeerProxy`. + var baseTypeRef = reader.GetTypeReference ((TypeReferenceHandle) proxyType.BaseType); + Assert.Equal ("Java.Interop", reader.GetString (baseTypeRef.Namespace)); + Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name)); + break; + default: + Assert.Fail ($"Unexpected BaseType handle kind: {proxyType.BaseType.Kind}"); + break; + } }); var objectProxy = proxyTypes.First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); @@ -112,6 +123,62 @@ public void Generate_ProxyType_UsesGenericJavaPeerProxyBase () objectProxyBaseType.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null)); } + // Regression test: every generated proxy type must carry a custom attribute whose + // constructor points at the proxy's own TypeDefinitionHandle (either as a MemberRef + // parented on the TypeDef, or as a MethodDefinition on the TypeDef). This is how + // JavaPeerProxy instances are resolved at runtime via + // type.GetCustomAttribute() — losing the self-application means the + // runtime can't construct the proxy. This has regressed twice; keep it covered. + [Fact] + public void Generate_ProxyType_IsSelfAppliedAsCustomAttribute () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var proxyTypeHandles = reader.TypeDefinitions + .Where (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Proxies") + .ToList (); + + Assert.NotEmpty (proxyTypeHandles); + + foreach (var proxyHandle in proxyTypeHandles) { + var proxy = reader.GetTypeDefinition (proxyHandle); + var proxyName = reader.GetString (proxy.Name); + + bool selfApplied = false; + foreach (var caHandle in proxy.GetCustomAttributes ()) { + var ca = reader.GetCustomAttribute (caHandle); + + switch (ca.Constructor.Kind) { + case HandleKind.MemberReference: + var ctorRef = reader.GetMemberReference ((MemberReferenceHandle) ca.Constructor); + if (ctorRef.Parent.Kind == HandleKind.TypeDefinition && + (TypeDefinitionHandle) ctorRef.Parent == proxyHandle) { + selfApplied = true; + } + break; + case HandleKind.MethodDefinition: + var ctorDef = reader.GetMethodDefinition ((MethodDefinitionHandle) ca.Constructor); + if (ctorDef.GetDeclaringType () == proxyHandle) { + selfApplied = true; + } + break; + } + + if (selfApplied) { + break; + } + } + + Assert.True (selfApplied, + $"Proxy type '{proxyName}' is missing its self-applied custom attribute. " + + "Every proxy must carry itself as a [JavaPeerProxy] attribute so the runtime " + + "can instantiate it via Type.GetCustomAttribute ()."); + } + } + [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index df3a1256763..9376ec09f05 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -450,6 +450,21 @@ public void Fixture_GenericHolder_Entry () var entry = FindEntry (model, "my/app/GenericHolder"); Assert.NotNull (entry); } + + [Fact] + public void Fixture_GenericHolder_HasAssociation () + { + // Generic definitions must still get a TypeMapAssociation entry so managed→proxy + // lookup works for the open generic definition. Their proxy derives from the + // non-generic `JavaPeerProxy` base, so the CLR can load the proxy without + // resolving an open generic argument. + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + Assert.True (peer.IsGenericDefinition); + + var model = BuildModel (new [] { peer }, "TypeMap"); + Assert.Contains (model.Associations, + a => a.SourceTypeReference.StartsWith ("MyApp.Generic.GenericHolder`1", StringComparison.Ordinal)); + } } public class FixtureAcwTypeHasProxy diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index e5e7f9e134c..233cc67fa23 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using Android.Runtime; using Java.Interop; using Microsoft.Android.Runtime; using NUnit.Framework; @@ -46,5 +50,161 @@ public void GetStaticMethodFallbackTypes_WithDeepPackageName_ReturnsDesugarFallb Assert.AreEqual ("com/example/package/DesugarMyInterface$_CC", fallbacks [0]); Assert.AreEqual ("com/example/package/MyInterface$-CC", fallbacks [1]); } + + // Verifies the generic-type-definition fallback in GetProxyForManagedType: + // the generator emits one TypeMapAssociation per open generic peer, so a + // closed instantiation like JavaList must resolve through its GTD. + [Test] + public void TryGetJniNameForManagedType_ClosedGeneric_ResolvesViaGenericTypeDefinition () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + + var instance = TrimmableTypeMap.Instance; + + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList<>), out var openJniName), + "Open generic definition should resolve directly."); + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out var closedStringJniName), + "Closed instantiation should resolve via GTD fallback."); + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out var closedIntJniName), + "A second closed instantiation should also resolve via GTD fallback."); + + Assert.AreEqual (openJniName, closedStringJniName, + "Closed instantiation must share the open GTD's JNI name (Java erases generics)."); + Assert.AreEqual (openJniName, closedIntJniName, + "Different closed instantiations must map to the same JNI name."); + } + + [Test] + public void TryGetJniNameForManagedType_NonGenericType_ResolvesDirectly () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + + // Regression: the GTD fallback must not disturb the non-generic hot path. + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (typeof (JavaList), out var jniName)); + Assert.IsFalse (string.IsNullOrEmpty (jniName)); + } + + [Test] + public void TryGetJniNameForManagedType_UnknownClosedGeneric_ReturnsFalse () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + + // System.Collections.Generic.List has no TypeMapAssociation — both the + // direct lookup AND the GTD fallback must miss, and the API must return false. + Assert.IsFalse (TrimmableTypeMap.Instance.TryGetJniNameForManagedType ( + typeof (System.Collections.Generic.List), out var jniName)); + Assert.IsNull (jniName); + } + + [Test] + public void TryGetJniNameForManagedType_RepeatedClosedGenericLookup_IsCached () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + + // Closed generic peers normalize to their open generic definition, so + // repeated lookups reuse the same cached proxy. + var instance = TrimmableTypeMap.Instance; + + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out var first)); + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out var second)); + Assert.AreEqual (first, second); + } + + [Test] + public void TryGetJniNameForManagedType_DifferentClosedGenerics_UseGenericDefinitionCacheKey () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + + var instance = TrimmableTypeMap.Instance; + var cache = GetProxyCache (instance); + + cache.TryRemove (typeof (JavaList<>), out _); + cache.TryRemove (typeof (JavaList), out _); + cache.TryRemove (typeof (JavaList), out _); + + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out _)); + Assert.IsTrue (instance.TryGetJniNameForManagedType (typeof (JavaList), out _)); + + Assert.IsTrue (cache.ContainsKey (typeof (JavaList<>))); + Assert.IsFalse (cache.ContainsKey (typeof (JavaList))); + Assert.IsFalse (cache.ContainsKey (typeof (JavaList))); + } + + static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) + { + var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull (field); + + var value = field.GetValue (instance); + Assert.IsNotNull (value); + + if (value is ConcurrentDictionary cache) { + return cache; + } + + Assert.Fail ("Unable to access TrimmableTypeMap proxy cache."); + throw new InvalidOperationException ("Unable to access TrimmableTypeMap proxy cache."); + } + + // Pure-function tests for the TargetTypeMatches helper used by + // TryGetProxyFromHierarchy when the hierarchy lookup finds a proxy whose + // stored TargetType is an open generic definition. + + class OpenT { } + class OpenT2 { } + class ClosedOfIntOpenT : OpenT { } + class DeepClosedOfOpenT : ClosedOfIntOpenT { } + + [Test] + public void TargetTypeMatches_DirectAssignable_ReturnsTrue () + { + // Non-generic direct match: proxy target IS-A hint. + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (object), typeof (string))); + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (string), typeof (string))); + } + + [Test] + public void TargetTypeMatches_ClosedHint_OpenGenericProxy_SelfMatch_ReturnsTrue () + { + // Hint is OpenT; proxy's target is the open GTD OpenT<>. + // IsAssignableFrom(OpenT<>) against OpenT is false, so this exercises + // the new GTD base-walk branch (self match on first iteration). + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (OpenT), typeof (OpenT<>))); + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (OpenT), typeof (OpenT<>))); + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (OpenT2), typeof (OpenT2<,>))); + } + + [Test] + public void TargetTypeMatches_ClosedSubclassHint_OpenGenericProxy_ReturnsTrue () + { + // Hint is a closed subclass of the open generic; the base-walk finds + // the generic base type whose definition equals the proxy's open target. + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (ClosedOfIntOpenT), typeof (OpenT<>))); + Assert.IsTrue (TrimmableTypeMap.TargetTypeMatches (typeof (DeepClosedOfOpenT), typeof (OpenT<>))); + } + + [Test] + public void TargetTypeMatches_MismatchedOpenGeneric_ReturnsFalse () + { + // Different open generic definitions must NOT be treated as matching. + Assert.IsFalse (TrimmableTypeMap.TargetTypeMatches (typeof (OpenT), typeof (OpenT2<,>))); + Assert.IsFalse (TrimmableTypeMap.TargetTypeMatches (typeof (string), typeof (OpenT<>))); + } + + [Test] + public void TargetTypeMatches_UnrelatedNonGeneric_ReturnsFalse () + { + Assert.IsFalse (TrimmableTypeMap.TargetTypeMatches (typeof (string), typeof (int))); + } } }