From 5c2760d052425190378957d47d7440da25c6fb43 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 10:56:12 +0200 Subject: [PATCH 01/14] Remove ForceUnconditionalEntries workaround Restore conditional TypeMap entries for non-essential MCW bindings now that the runtime trimmer issue has been fixed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 19 ++---------- .../Generator/TypeMapModelBuilderTests.cs | 29 ++++++++----------- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 21ce7d7d66d..25e9db22008 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,13 +16,6 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; - // Workaround for https://github.com/dotnet/runtime/issues/127004 - // When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the - // trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute - // references the same type. Set to false once the runtime bug is fixed to re-enable - // 3-arg conditional entries that allow unused framework bindings to be trimmed away. - const bool ForceUnconditionalEntries = true; - static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -189,13 +182,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, } // Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations) - // When ForceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just - // like BuildEntry does: dotnet/runtime#127004 strips the TypeMapAssociation that keeps the - // holder alive when a TypeMap entry references the same type, leaving the dictionary key - // missing at runtime and breaking hierarchy lookups for essential types like - // java/lang/String and java/lang/Object. - bool aliasBaseUnconditional = ForceUnconditionalEntries - || EssentialRuntimeTypes.Contains (jniName) + bool aliasBaseUnconditional = EssentialRuntimeTypes.Contains (jniName) || peersForName.Any (IsUnconditionalEntry); model.Entries.Add (new TypeMapAttributeData { JniName = jniName, @@ -406,9 +393,7 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } - // When ForceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap - // attributes to work around https://github.com/dotnet/runtime/issues/127004. - bool isUnconditional = ForceUnconditionalEntries || IsUnconditionalEntry (peer); + bool isUnconditional = IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 55ba4a3e9f9..7a34fc345fe 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -172,14 +172,12 @@ public void Build_UserAcwType_IsUnconditional () public void Build_McwBinding_IsTrimmable () { // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential. - // When ForceUnconditionalEntries is enabled (workaround for dotnet/runtime#127004), - // all entries become unconditional. var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); - Assert.True (model.Entries [0].IsUnconditional); - Assert.Null (model.Entries [0].TargetTypeReference); + Assert.False (model.Entries [0].IsUnconditional); + Assert.Equal ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference); } [Fact] @@ -248,8 +246,8 @@ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string m [Fact] public void Build_SinglePeer_HasAssociation () { - // When ForceUnconditionalEntries is enabled, single peers emit associations - // so the runtime proxy type map is populated. + // Single peers with generated proxies emit associations so the runtime proxy + // type map is populated. var peer = MakePeerWithActivation ("my/app/MainActivity", "MyApp.MainActivity", "App"); var model = BuildModel (new [] { peer }, "MyTypeMap"); @@ -338,8 +336,8 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) var peer = FindFixtureByJavaName (javaName); Assert.True (peer.DoNotGenerateAcw); var model = BuildModel (new [] { peer }); - // ForceUnconditionalEntries workaround makes all entries unconditional - Assert.True (model.Entries [0].IsUnconditional); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); } } @@ -776,7 +774,6 @@ public class PeBlobValidation [Fact] public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () { - // With ForceUnconditionalEntries, both are emitted as 2-arg unconditional var objectPeer = FindFixtureByJavaName ("java/lang/Object"); var activityPeer = FindFixtureByJavaName ("android/app/Activity"); @@ -793,7 +790,7 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () var activityEntry = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); Assert.NotNull (activityEntry.jniName); - Assert.Null (activityEntry.targetRef); // unconditional due to ForceUnconditionalEntries + Assert.Equal ("Android.App.Activity, TestFixtures", activityEntry.targetRef); }); } @@ -818,22 +815,20 @@ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, } [Fact] - public void FullPipeline_McwBinding_Emits2ArgAttribute_WithWorkaround () + public void FullPipeline_McwBinding_Emits3ArgAttribute () { - // With ForceUnconditionalEntries workaround for dotnet/runtime#127004, - // MCW bindings are emitted as 2-arg unconditional. var peer = FindFixtureByJavaName ("android/app/Activity"); - var model = BuildModel (new [] { peer }, "Blob2ArgWorkaround"); + var model = BuildModel (new [] { peer }, "Blob3ArgConditional"); Assert.Single (model.Entries); - Assert.True (model.Entries [0].IsUnconditional); + Assert.False (model.Entries [0].IsUnconditional); - EmitAndVerify (model, "Blob2ArgWorkaround", (pe, reader) => { + EmitAndVerify (model, "Blob3ArgConditional", (pe, reader) => { var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); Assert.Equal ("android/app/Activity", jniName); Assert.NotNull (proxyRef); Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); - Assert.Null (targetRef); // unconditional due to ForceUnconditionalEntries + Assert.Equal ("Android.App.Activity, TestFixtures", targetRef); }); } } From cb4f4ed36a56c5c5645b4c8fb3a259a434db6899 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 15 May 2026 15:30:59 +0200 Subject: [PATCH 02/14] TMP: test --- .../ServerCertificateCustomValidator.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs index f1508cd11e0..7d29836a188 100644 --- a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs +++ b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs @@ -170,35 +170,33 @@ private sealed class AlwaysAcceptingHostnameVerifier : Java.Lang.Object, IHostna public bool Verify (string? hostname, ISSLSession? session) => true; } - [DynamicDependency(nameof(IX509TrustManager.CheckServerTrusted), typeof(IX509TrustManagerInvoker))] - [DynamicDependency(nameof(IX509TrustManager.CheckServerTrusted), typeof(X509ExtendedTrustManagerInvoker))] private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManagers, out int index) { + index = -1; + + for (int i = 0; i < trustManagers.Length; i++) { + var trustManager = trustManagers [i]; + Console.WriteLine ($"TrustManager class: {trustManager.GetType().FullName}"); + } + for (int i = 0; i < trustManagers.Length; i++) { var trustManager = trustManagers [i]; if (trustManager is IX509TrustManager x509TrustManager) { index = i; return x509TrustManager; } + } - // On API 21-23, the default Java trust manager is TrustManagerImpl from Conscrypt. The class implements X509TrustManager - // but the .NET pattern matching will fail in this case and we need to cast it explicitly. - int apiLevel = (int)Build.VERSION.SdkInt; - if (apiLevel <= 23) { - if (IsTrustManagerImpl (trustManager)) { - index = i; - return trustManager.JavaCast (); - } - } + // HACK - make IX509TrustManagerInvoker visible to the linker so that it doesn't get trimmed out + if (trustManagers.Length > 1_000_000) { + // this is unreachable, but the linker doesn't know that + return new IX509TrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); + } else if (trustManagers.Length > 2_000_000) { + // this is unreachable, but the linker doesn't know that + return new X509ExtendedTrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); } throw new InvalidOperationException($"Could not find {nameof(IX509TrustManager)} in {nameof(ITrustManager)} array."); - - static bool IsTrustManagerImpl (ITrustManager trustManager) - { - var javaClassName = JNIEnv.GetClassNameFromInstance (trustManager.Handle); - return javaClassName.Equals ("com/android/org/conscrypt/TrustManagerImpl", StringComparison.Ordinal); - } } private static ITrustManager[] ModifyTrustManagersArray (ITrustManager[] trustManagers, int originalTrustManagerIndex, IX509TrustManager replacement) From b30dd88f2d630cd9fa7ed913d7b26ba9e66243b7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 16 May 2026 09:39:08 +0200 Subject: [PATCH 03/14] Replace [DynamicDependency] with NoInlining hack, fix AppFunctionState name collision - Replace [DynamicDependency] attributes on FindX509TrustManager with a HackToPreserveInvokers method using [MethodImpl(NoInlining)] to prevent the linker from trimming IX509TrustManagerInvoker and X509ExtendedTrustManagerInvoker. - Remove obsolete API 21-23 Conscrypt TrustManagerImpl workaround. - Rename generated enum from AppFunctionState to AppFunctionEnabledState to avoid name collision with the new Android API 37 android.app.appfunctions.AppFunctionState class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServerCertificateCustomValidator.cs | 27 ++++++++++--------- src/Mono.Android/map.csv | 6 ++--- src/Mono.Android/methodmap.csv | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs index 7d29836a188..da37a6aa1b8 100644 --- a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs +++ b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Net.Security; +using System.Runtime.CompilerServices; using System.Security.Cryptography.X509Certificates; using Android.OS; @@ -174,11 +175,6 @@ private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManag { index = -1; - for (int i = 0; i < trustManagers.Length; i++) { - var trustManager = trustManagers [i]; - Console.WriteLine ($"TrustManager class: {trustManager.GetType().FullName}"); - } - for (int i = 0; i < trustManagers.Length; i++) { var trustManager = trustManagers [i]; if (trustManager is IX509TrustManager x509TrustManager) { @@ -187,18 +183,25 @@ private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManag } } - // HACK - make IX509TrustManagerInvoker visible to the linker so that it doesn't get trimmed out - if (trustManagers.Length > 1_000_000) { - // this is unreachable, but the linker doesn't know that - return new IX509TrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); - } else if (trustManagers.Length > 2_000_000) { - // this is unreachable, but the linker doesn't know that - return new X509ExtendedTrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); + if (trustManagers.Length > 10_000) { + HackToPreserveInvokers(trustManagers); } throw new InvalidOperationException($"Could not find {nameof(IX509TrustManager)} in {nameof(ITrustManager)} array."); } + [MethodImpl (MethodImplOptions.NoInlining)] + static void HackToPreserveInvokers (ITrustManager[] trustManagers) + { + // HACK - make IX509TrustManagerInvoker visible to the linker so that it doesn't get trimmed out. + // These branches are unreachable, but the linker doesn't know that. + if (trustManagers.Length > 1_000_000) { + _ = new IX509TrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); + } else if (trustManagers.Length > 2_000_000) { + _ = new X509ExtendedTrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); + } + } + private static ITrustManager[] ModifyTrustManagersArray (ITrustManager[] trustManagers, int originalTrustManagerIndex, IX509TrustManager replacement) { var modifiedTrustManagersArray = new ITrustManager [trustManagers.Length]; diff --git a/src/Mono.Android/map.csv b/src/Mono.Android/map.csv index 9288c1f7144..ad20547d9b8 100644 --- a/src/Mono.Android/map.csv +++ b/src/Mono.Android/map.csv @@ -478,9 +478,9 @@ E,36,android/app/appfunctions/AppFunctionException.ERROR_ENTERPRISE_POLICY_DISAL E,36,android/app/appfunctions/AppFunctionException.ERROR_FUNCTION_NOT_FOUND,1003,Android.App.AppFunctions.AppFunctionError,FunctionNotFound,remove, E,36,android/app/appfunctions/AppFunctionException.ERROR_INVALID_ARGUMENT,1001,Android.App.AppFunctions.AppFunctionError,InvalidArgument,remove, E,36,android/app/appfunctions/AppFunctionException.ERROR_SYSTEM_ERROR,2000,Android.App.AppFunctions.AppFunctionError,SystemError,remove, -E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DEFAULT,0,Android.App.AppFunctions.AppFunctionState,Default,remove, -E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DISABLED,2,Android.App.AppFunctions.AppFunctionState,Disabled,remove, -E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_ENABLED,1,Android.App.AppFunctions.AppFunctionState,Enabled,remove, +E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DEFAULT,0,Android.App.AppFunctions.AppFunctionEnabledState,Default,remove, +E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_DISABLED,2,Android.App.AppFunctions.AppFunctionEnabledState,Disabled,remove, +E,36,android/app/appfunctions/AppFunctionManager.APP_FUNCTION_STATE_ENABLED,1,Android.App.AppFunctions.AppFunctionEnabledState,Enabled,remove, E,37,android/app/appfunctions/AppFunctionMetadata.SCOPE_ACTIVITY,1,Android.App.AppFunctions.AppFunctionMetadataScope,Activity,remove, E,37,android/app/appfunctions/AppFunctionMetadata.SCOPE_GLOBAL,0,Android.App.AppFunctions.AppFunctionMetadataScope,Global,remove, E,37,android/app/AppInteractionAttribution.INTERACTION_TYPE_OTHER,0,Android.App.AppInteractionAttributionInteractionType,Other,remove, diff --git a/src/Mono.Android/methodmap.csv b/src/Mono.Android/methodmap.csv index 2b452d40b49..bab127e450d 100644 --- a/src/Mono.Android/methodmap.csv +++ b/src/Mono.Android/methodmap.csv @@ -4100,7 +4100,7 @@ 36,android.app.appfunctions,AppFunctionException,getErrorCategory,return,Android.App.AppFunctions.AppFunctionErrorCategory 36,android.app.appfunctions,AppFunctionException,getErrorCode,return,Android.App.AppFunctions.AppFunctionError 36,android.app.appfunctions,AppFunctionException,writeToParcel,flags,Android.OS.ParcelableWriteFlags -36,android.app.appfunctions,AppFunctionManager,setAppFunctionEnabled,newEnabledState,Android.App.AppFunctions.AppFunctionState +36,android.app.appfunctions,AppFunctionManager,setAppFunctionEnabled,newEnabledState,Android.App.AppFunctions.AppFunctionEnabledState 36,android.app.appfunctions,ExecuteAppFunctionRequest,writeToParcel,flags,Android.OS.ParcelableWriteFlags 36,android.app.appfunctions,ExecuteAppFunctionResponse,writeToParcel,flags,Android.OS.ParcelableWriteFlags 36,android.app,ApplicationStartInfo,getStartComponent,return,Android.App.ApplicationStartInfoStartComponent From a9bd0c102c896ae45610b86bfbb8ed199756a7d9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 05:22:55 +0200 Subject: [PATCH 04/14] Revert unnecessary changes to ServerCertificateCustomValidator The isinst instruction properly preserves conditional TypeMap entries for interfaces through the MarkType -> MarkRequirementsForInstantiatedTypes -> ProcessType chain in ILLink. The typeof() and [DynamicDependency] additions were based on an incorrect analysis that ProcessType was skipped for interfaces - while true in the isinst handler's inner switch, MarkType itself calls MarkRequirementsForInstantiatedTypes for interfaces which calls ProcessType without any interface skip. Verified with both ILLink and NativeAOT using a multi-assembly repro that mirrors the Android typemap assembly layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServerCertificateCustomValidator.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs index da37a6aa1b8..f1508cd11e0 100644 --- a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs +++ b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Net.Security; -using System.Runtime.CompilerServices; using System.Security.Cryptography.X509Certificates; using Android.OS; @@ -171,34 +170,34 @@ private sealed class AlwaysAcceptingHostnameVerifier : Java.Lang.Object, IHostna public bool Verify (string? hostname, ISSLSession? session) => true; } + [DynamicDependency(nameof(IX509TrustManager.CheckServerTrusted), typeof(IX509TrustManagerInvoker))] + [DynamicDependency(nameof(IX509TrustManager.CheckServerTrusted), typeof(X509ExtendedTrustManagerInvoker))] private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManagers, out int index) { - index = -1; - for (int i = 0; i < trustManagers.Length; i++) { var trustManager = trustManagers [i]; if (trustManager is IX509TrustManager x509TrustManager) { index = i; return x509TrustManager; } - } - if (trustManagers.Length > 10_000) { - HackToPreserveInvokers(trustManagers); + // On API 21-23, the default Java trust manager is TrustManagerImpl from Conscrypt. The class implements X509TrustManager + // but the .NET pattern matching will fail in this case and we need to cast it explicitly. + int apiLevel = (int)Build.VERSION.SdkInt; + if (apiLevel <= 23) { + if (IsTrustManagerImpl (trustManager)) { + index = i; + return trustManager.JavaCast (); + } + } } throw new InvalidOperationException($"Could not find {nameof(IX509TrustManager)} in {nameof(ITrustManager)} array."); - } - [MethodImpl (MethodImplOptions.NoInlining)] - static void HackToPreserveInvokers (ITrustManager[] trustManagers) - { - // HACK - make IX509TrustManagerInvoker visible to the linker so that it doesn't get trimmed out. - // These branches are unreachable, but the linker doesn't know that. - if (trustManagers.Length > 1_000_000) { - _ = new IX509TrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); - } else if (trustManagers.Length > 2_000_000) { - _ = new X509ExtendedTrustManagerInvoker (IntPtr.Zero, JniHandleOwnership.DoNotTransfer); + static bool IsTrustManagerImpl (ITrustManager trustManager) + { + var javaClassName = JNIEnv.GetClassNameFromInstance (trustManager.Handle); + return javaClassName.Equals ("com/android/org/conscrypt/TrustManagerImpl", StringComparison.Ordinal); } } From 1ccae1f855fcc2c86a0f9e486027227a2686129b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 12:55:06 +0200 Subject: [PATCH 05/14] Add Java interface walk fallback to trimmable typemap resolution When the getSuperclass() class hierarchy walk fails to find a targetType-compatible TypeMap proxy (e.g., because an intermediate class entry like X509ExtendedTrustManager was trimmed), fall back to walking Java interfaces via getInterfaces() at each level of the class hierarchy. This allows the runtime to find TypeMap entries for Java interfaces (e.g., javax/net/ssl/X509TrustManager) that are preserved by the trimmer but unreachable via getSuperclass() alone. The interface walk only runs when targetType is an interface, keeping the common path (class-based resolution) unchanged. Also adds a device test TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager that verifies TrustManagerFactory.GetTrustManagers() returns elements that can be cast to IX509TrustManager. This test runs on both llvm-ir and trimmable typemap paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 85 +++++++++++++++++++ .../TrimmableTypeMapTypeManagerTests.cs | 58 +++++++++++++ 2 files changed, 143 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 58455d8ea79..41a642ea6b0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -238,6 +238,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } return TryGetProxyFromHierarchy (this, handle, targetType) ?? + TryGetProxyFromInterfaces (this, handle, targetType) ?? TryGetProxyFromTargetType (this, handle, targetType); static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType) @@ -266,6 +267,90 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } + // When the class hierarchy walk fails and the targetType is an interface, + // walk the Java interfaces of the object's class. This handles the case + // where the intermediate class entry (e.g., X509ExtendedTrustManager) was + // trimmed but the Java interface entry (e.g., X509TrustManager) survives. + static JavaPeerProxy? TryGetProxyFromInterfaces (TrimmableTypeMap self, IntPtr handle, Type? targetType) + { + if (targetType is null || !targetType.IsInterface) { + return null; + } + + var selfRef = new JniObjectReference (handle); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + + try { + // Walk the class hierarchy and at each level check the interfaces + while (jniClass.IsValid) { + var result = TryGetProxyFromInterfacesOfClass (self, jniClass, targetType); + if (result != null) { + return result; + } + + var super = JniEnvironment.Types.GetSuperclass (jniClass); + JniObjectReference.Dispose (ref jniClass); + jniClass = super; + } + } finally { + JniObjectReference.Dispose (ref jniClass); + } + + return null; + } + + static JavaPeerProxy? TryGetProxyFromInterfacesOfClass (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) + { + IntPtr interfacesArray = GetJavaInterfaces (jniClass.Handle); + if (interfacesArray == IntPtr.Zero) { + return null; + } + + try { + int length = JNIEnv.GetArrayLength (interfacesArray); + for (int i = 0; i < length; i++) { + IntPtr ifaceClass = JNIEnv.GetObjectArrayElement (interfacesArray, i); + if (ifaceClass == IntPtr.Zero) { + continue; + } + + try { + var ifaceRef = new JniObjectReference (ifaceClass); + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef); + if (ifaceName != null) { + var proxy = self.GetProxyForJniClass (ifaceName, targetType); + if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; + } + } + + // Recurse into super-interfaces + var result = TryGetProxyFromInterfacesOfClass (self, ifaceRef, targetType); + if (result != null) { + return result; + } + } finally { + JNIEnv.DeleteLocalRef (ifaceClass); + } + } + } finally { + JNIEnv.DeleteLocalRef (interfacesArray); + } + + return null; + } + + static IntPtr GetJavaInterfaces (IntPtr jniClass) + { + IntPtr classClass = JNIEnv.GetObjectClass (jniClass); + try { + IntPtr getInterfacesMethod = JNIEnv.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + return JNIEnv.CallObjectMethod (jniClass, getInterfacesMethod); + } finally { + JNIEnv.DeleteLocalRef (classClass); + } + } + static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) { if (targetType is null) { 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 3ccae0da3ad..068eafa992a 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 @@ -435,3 +435,61 @@ class TrimmableRegisteredGenericHolder : Java.Lang.Object public T Value { get; set; } } } + +namespace Xamarin.Android.NetTests +{ + using System.Linq; + using Java.Security; + using Javax.Net.Ssl; + using NUnit.Framework; + using Log = global::Android.Util.Log; + + [TestFixture] + public class TrimmableTypeMapTrustManagerTests + { + [Test] + public void TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager () + { + var tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); + tmf.Init ((KeyStore?) null); + + var trustManagers = tmf.GetTrustManagers (); + Assert.IsNotNull (trustManagers, "GetTrustManagers returned null"); + Assert.IsTrue (trustManagers.Length > 0, "GetTrustManagers returned empty array"); + + bool foundX509 = false; + foreach (var tm in trustManagers) { + string javaClass = JNIEnv.GetClassNameFromInstance (tm.Handle); + Log.Info ("TypeMapTest", $"--- TrustManager element ---"); + Log.Info ("TypeMapTest", $" Managed type: {tm.GetType ().FullName}"); + Log.Info ("TypeMapTest", $" Java class: {javaClass}"); + Log.Info ("TypeMapTest", $" is IX509TrustManager: {tm is IX509TrustManager}"); + + // Walk the Java class hierarchy to see what the typemap would encounter + var selfRef = new Java.Interop.JniObjectReference (tm.Handle); + var jniClass = Java.Interop.JniEnvironment.Types.GetObjectClass (selfRef); + try { + int depth = 0; + while (jniClass.IsValid) { + var name = Java.Interop.JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + Log.Info ("TypeMapTest", $" hierarchy[{depth}]: {name}"); + var super = Java.Interop.JniEnvironment.Types.GetSuperclass (jniClass); + Java.Interop.JniObjectReference.Dispose (ref jniClass); + jniClass = super; + depth++; + } + } finally { + Java.Interop.JniObjectReference.Dispose (ref jniClass); + } + + if (tm is IX509TrustManager) { + foundX509 = true; + } + } + + Assert.IsTrue (foundX509, + $"No ITrustManager element was marshalled as IX509TrustManager. " + + $"Types found: {string.Join (", ", trustManagers.Select (t => t.GetType ().FullName))}"); + } + } +} From cf64a40b06ea24419f2db8c5e83f0d0e67313a42 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 13:19:19 +0200 Subject: [PATCH 06/14] Optimize: merge interface walk into hierarchy loop, cache getInterfaces method ID - Check interfaces inline during the getSuperclass() walk instead of a separate second pass, avoiding redundant GetObjectClass/getSuperclass calls - Cache the JNI method ID for getInterfaces() in a static field - Rename TryGetProxyFromInterfacesOfClass to TryMatchInterfaces Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 61 +++++++------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 41a642ea6b0..81ff30ffc6e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -231,6 +231,8 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } + static IntPtr s_getInterfacesMethod; + internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { @@ -238,11 +240,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } return TryGetProxyFromHierarchy (this, handle, targetType) ?? - TryGetProxyFromInterfaces (this, handle, targetType) ?? TryGetProxyFromTargetType (this, handle, targetType); static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType) { + bool checkInterfaces = targetType is not null && targetType.IsInterface; var selfRef = new JniObjectReference (handle); var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); @@ -256,36 +258,15 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } } - var super = JniEnvironment.Types.GetSuperclass (jniClass); - JniObjectReference.Dispose (ref jniClass); - jniClass = super; - } - } finally { - JniObjectReference.Dispose (ref jniClass); - } - - return null; - } - - // When the class hierarchy walk fails and the targetType is an interface, - // walk the Java interfaces of the object's class. This handles the case - // where the intermediate class entry (e.g., X509ExtendedTrustManager) was - // trimmed but the Java interface entry (e.g., X509TrustManager) survives. - static JavaPeerProxy? TryGetProxyFromInterfaces (TrimmableTypeMap self, IntPtr handle, Type? targetType) - { - if (targetType is null || !targetType.IsInterface) { - return null; - } - - var selfRef = new JniObjectReference (handle); - var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); - - try { - // Walk the class hierarchy and at each level check the interfaces - while (jniClass.IsValid) { - var result = TryGetProxyFromInterfacesOfClass (self, jniClass, targetType); - if (result != null) { - return result; + // When targetType is an interface, also check the Java interfaces + // at each level. This handles the case where an intermediate class + // entry (e.g., X509ExtendedTrustManager) was trimmed but the Java + // interface entry (e.g., X509TrustManager) survives. + if (checkInterfaces) { + var result = TryMatchInterfaces (self, jniClass, targetType!); + if (result != null) { + return result; + } } var super = JniEnvironment.Types.GetSuperclass (jniClass); @@ -299,7 +280,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - static JavaPeerProxy? TryGetProxyFromInterfacesOfClass (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) + static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { IntPtr interfacesArray = GetJavaInterfaces (jniClass.Handle); if (interfacesArray == IntPtr.Zero) { @@ -325,7 +306,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } // Recurse into super-interfaces - var result = TryGetProxyFromInterfacesOfClass (self, ifaceRef, targetType); + var result = TryMatchInterfaces (self, ifaceRef, targetType); if (result != null) { return result; } @@ -342,13 +323,15 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) static IntPtr GetJavaInterfaces (IntPtr jniClass) { - IntPtr classClass = JNIEnv.GetObjectClass (jniClass); - try { - IntPtr getInterfacesMethod = JNIEnv.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); - return JNIEnv.CallObjectMethod (jniClass, getInterfacesMethod); - } finally { - JNIEnv.DeleteLocalRef (classClass); + if (s_getInterfacesMethod == IntPtr.Zero) { + IntPtr classClass = JNIEnv.GetObjectClass (jniClass); + try { + s_getInterfacesMethod = JNIEnv.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + } finally { + JNIEnv.DeleteLocalRef (classClass); + } } + return JNIEnv.CallObjectMethod (jniClass, s_getInterfacesMethod); } static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) From 55706eea8cddac610933fab41cbb2356b4ff67d0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 13:30:42 +0200 Subject: [PATCH 07/14] Use pattern matching for interface guard, add comment about getInterfaces scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 81ff30ffc6e..3462a2a7ed0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -244,7 +244,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType) { - bool checkInterfaces = targetType is not null && targetType.IsInterface; var selfRef = new JniObjectReference (handle); var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); @@ -259,11 +258,13 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } // When targetType is an interface, also check the Java interfaces - // at each level. This handles the case where an intermediate class - // entry (e.g., X509ExtendedTrustManager) was trimmed but the Java - // interface entry (e.g., X509TrustManager) survives. - if (checkInterfaces) { - var result = TryMatchInterfaces (self, jniClass, targetType!); + // at each level. getInterfaces() only returns directly declared + // interfaces so we must call it at each class in the hierarchy. + // This handles the case where an intermediate class entry (e.g., + // X509ExtendedTrustManager) was trimmed but the Java interface + // entry (e.g., X509TrustManager) survives. + if (targetType is { IsInterface: true }) { + var result = TryMatchInterfaces (self, jniClass, targetType); if (result != null) { return result; } @@ -280,6 +281,8 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } + // getInterfaces() returns only directly declared interfaces (not transitive), + // so we recurse into super-interfaces to find the matching TypeMap entry. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { IntPtr interfacesArray = GetJavaInterfaces (jniClass.Handle); From dbc89830940fd75f5c097f007bbc640c55c438d3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 13:55:04 +0200 Subject: [PATCH 08/14] Keep [DynamicDependency] attributes, remove obsolete API 21-23 workaround The [DynamicDependency] attributes on FindX509TrustManager are needed by both llvm-ir and trimmable typemap paths to preserve invoker types. Without them, the llvm-ir linker also trims X509ExtendedTrustManager entries, causing the same IX509TrustManager resolution failure. Removed the API 21-23 Conscrypt TrustManagerImpl workaround since minimum supported API level is now higher. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ServerCertificateCustomValidator.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs index f1508cd11e0..e70788754b1 100644 --- a/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs +++ b/src/Mono.Android/Xamarin.Android.Net/ServerCertificateCustomValidator.cs @@ -180,25 +180,9 @@ private static IX509TrustManager FindX509TrustManager(ITrustManager[] trustManag index = i; return x509TrustManager; } - - // On API 21-23, the default Java trust manager is TrustManagerImpl from Conscrypt. The class implements X509TrustManager - // but the .NET pattern matching will fail in this case and we need to cast it explicitly. - int apiLevel = (int)Build.VERSION.SdkInt; - if (apiLevel <= 23) { - if (IsTrustManagerImpl (trustManager)) { - index = i; - return trustManager.JavaCast (); - } - } } throw new InvalidOperationException($"Could not find {nameof(IX509TrustManager)} in {nameof(ITrustManager)} array."); - - static bool IsTrustManagerImpl (ITrustManager trustManager) - { - var javaClassName = JNIEnv.GetClassNameFromInstance (trustManager.Handle); - return javaClassName.Equals ("com/android/org/conscrypt/TrustManagerImpl", StringComparison.Ordinal); - } } private static ITrustManager[] ModifyTrustManagersArray (ITrustManager[] trustManagers, int originalTrustManagerIndex, IX509TrustManager replacement) From 218e295d78d9467b6b1accc3f3f81545e293ea41 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 14:04:43 +0200 Subject: [PATCH 09/14] Add tests for alias group base entry conditionality Address review feedback: add coverage that all-MCW alias groups emit a conditional (3-arg) base alias-holder entry, mixed ACW/MCW alias groups stay unconditional (2-arg), and essential runtime types remain unconditional regardless of peer types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapModelBuilderTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 7a34fc345fe..47f7eb6af31 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -135,6 +135,54 @@ public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivatio // Both peers get associations to alias holder Assert.Equal (2, model.Associations.Count); } + + [Fact] + public void Build_AllMcwAliasGroup_BaseEntryIsConditional () + { + // When all peers in an alias group are MCW bindings (trimmable), + // the base alias-holder entry should be conditional (3-arg). + var peers = new List { + MakeMcwPeer ("test/AllMcw", "Test.First", "A") with { DoNotGenerateAcw = true }, + MakeMcwPeer ("test/AllMcw", "Test.Second", "A") with { DoNotGenerateAcw = true }, + }; + + var model = BuildModel (peers); + var baseEntry = model.Entries.Single (e => e.JniName == "test/AllMcw"); + Assert.False (baseEntry.IsUnconditional, "All-MCW alias group base entry should be conditional"); + Assert.NotNull (baseEntry.TargetTypeReference); + } + + [Fact] + public void Build_MixedAcwMcwAliasGroup_BaseEntryIsUnconditional () + { + // When at least one peer in an alias group is an ACW (unconditional), + // the base alias-holder entry should be unconditional (2-arg). + var peers = new List { + MakeMcwPeer ("test/Mixed", "Test.Mcw", "A") with { DoNotGenerateAcw = true }, + MakeAcwPeer ("test/Mixed", "Test.Acw", "A"), + }; + + var model = BuildModel (peers); + var baseEntry = model.Entries.Single (e => e.JniName == "test/Mixed"); + Assert.True (baseEntry.IsUnconditional, "Mixed alias group with ACW should have unconditional base entry"); + Assert.Null (baseEntry.TargetTypeReference); + } + + [Fact] + public void Build_EssentialTypeAliasGroup_BaseEntryIsUnconditional () + { + // Essential runtime types (java/lang/Object etc.) should always be unconditional, + // even when all peers are MCW bindings. + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + MakeMcwPeer ("java/lang/Object", "Java.Lang.Another", "Mono.Android"), + }; + + var model = BuildModel (peers); + var baseEntry = model.Entries.Single (e => e.JniName == "java/lang/Object"); + Assert.True (baseEntry.IsUnconditional, "Essential type alias group should have unconditional base entry"); + Assert.Null (baseEntry.TargetTypeReference); + } } public class ConditionalAttributes From 901b283ec4dc213a88d439c1a44fea3f7a489845 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 14:26:27 +0200 Subject: [PATCH 10/14] Use JniEnvironment primitives instead of raw IntPtr JNI calls Replace raw JNIEnv.GetMethodID/CallObjectMethod/GetArrayLength/ GetObjectArrayElement/DeleteLocalRef with their JniEnvironment equivalents (JniMethodInfo, JniObjectReference, JniEnvironment.Arrays, JniEnvironment.InstanceMethods). This matches the style of the surrounding hierarchy walk code and uses proper ref disposal via JniObjectReference.Dispose. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 3462a2a7ed0..01036179ff2 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -23,6 +23,7 @@ public class TrimmableTypeMap static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; static bool s_nativeMethodsRegistered; + static JniMethodInfo? s_getInterfacesMethod; internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( @@ -231,7 +232,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } - static IntPtr s_getInterfacesMethod; internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { @@ -285,22 +285,21 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // so we recurse into super-interfaces to find the matching TypeMap entry. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { - IntPtr interfacesArray = GetJavaInterfaces (jniClass.Handle); - if (interfacesArray == IntPtr.Zero) { + var interfacesArray = CallGetInterfaces (jniClass); + if (!interfacesArray.IsValid) { return null; } try { - int length = JNIEnv.GetArrayLength (interfacesArray); + int length = JniEnvironment.Arrays.GetArrayLength (interfacesArray); for (int i = 0; i < length; i++) { - IntPtr ifaceClass = JNIEnv.GetObjectArrayElement (interfacesArray, i); - if (ifaceClass == IntPtr.Zero) { + var ifaceClass = JniEnvironment.Arrays.GetObjectArrayElement (interfacesArray, i); + if (!ifaceClass.IsValid) { continue; } try { - var ifaceRef = new JniObjectReference (ifaceClass); - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef); + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceClass); if (ifaceName != null) { var proxy = self.GetProxyForJniClass (ifaceName, targetType); if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { @@ -309,32 +308,32 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } // Recurse into super-interfaces - var result = TryMatchInterfaces (self, ifaceRef, targetType); + var result = TryMatchInterfaces (self, ifaceClass, targetType); if (result != null) { return result; } } finally { - JNIEnv.DeleteLocalRef (ifaceClass); + JniObjectReference.Dispose (ref ifaceClass); } } } finally { - JNIEnv.DeleteLocalRef (interfacesArray); + JniObjectReference.Dispose (ref interfacesArray); } return null; } - static IntPtr GetJavaInterfaces (IntPtr jniClass) + static JniObjectReference CallGetInterfaces (JniObjectReference jniClass) { - if (s_getInterfacesMethod == IntPtr.Zero) { - IntPtr classClass = JNIEnv.GetObjectClass (jniClass); + if (s_getInterfacesMethod is null) { + var classClass = JniEnvironment.Types.GetObjectClass (jniClass); try { - s_getInterfacesMethod = JNIEnv.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + s_getInterfacesMethod = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); } finally { - JNIEnv.DeleteLocalRef (classClass); + JniObjectReference.Dispose (ref classClass); } } - return JNIEnv.CallObjectMethod (jniClass, s_getInterfacesMethod); + return JniEnvironment.InstanceMethods.CallObjectMethod (jniClass, s_getInterfacesMethod); } static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) From 8a868e12e854f7015bf049fc73155979fa333645 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 14:46:27 +0200 Subject: [PATCH 11/14] Simplify: use Java.Lang.Class.GetInterfaces() instead of raw JNI Wrap the JNI class handle in Java.Lang.Class with DoNotTransfer ownership and call the existing GetInterfaces() binding. This reuses the JniPeerMembers method ID caching, thread-safe lookup, and JNI remapping that the generated binding already handles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 60 ++++++------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 01036179ff2..ac6994c7f9b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -23,7 +23,6 @@ public class TrimmableTypeMap static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; static bool s_nativeMethodsRegistered; - static JniMethodInfo? s_getInterfacesMethod; internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( @@ -285,55 +284,34 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // so we recurse into super-interfaces to find the matching TypeMap entry. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { - var interfacesArray = CallGetInterfaces (jniClass); - if (!interfacesArray.IsValid) { + using var cls = new Java.Lang.Class (jniClass.Handle, JniHandleOwnership.DoNotTransfer); + var interfaces = cls.GetInterfaces (); + if (interfaces is null || interfaces.Length == 0) { return null; } - try { - int length = JniEnvironment.Arrays.GetArrayLength (interfacesArray); - for (int i = 0; i < length; i++) { - var ifaceClass = JniEnvironment.Arrays.GetObjectArrayElement (interfacesArray, i); - if (!ifaceClass.IsValid) { - continue; - } - - try { - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceClass); - if (ifaceName != null) { - var proxy = self.GetProxyForJniClass (ifaceName, targetType); - if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { - return proxy; - } - } + foreach (var iface in interfaces) { + if (iface is null) { + continue; + } - // Recurse into super-interfaces - var result = TryMatchInterfaces (self, ifaceClass, targetType); - if (result != null) { - return result; - } - } finally { - JniObjectReference.Dispose (ref ifaceClass); + var ifaceRef = new JniObjectReference (iface.Handle); + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef); + if (ifaceName != null) { + var proxy = self.GetProxyForJniClass (ifaceName, targetType); + if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; } } - } finally { - JniObjectReference.Dispose (ref interfacesArray); - } - - return null; - } - static JniObjectReference CallGetInterfaces (JniObjectReference jniClass) - { - if (s_getInterfacesMethod is null) { - var classClass = JniEnvironment.Types.GetObjectClass (jniClass); - try { - s_getInterfacesMethod = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); - } finally { - JniObjectReference.Dispose (ref classClass); + // Recurse into super-interfaces + var result = TryMatchInterfaces (self, ifaceRef, targetType); + if (result != null) { + return result; } } - return JniEnvironment.InstanceMethods.CallObjectMethod (jniClass, s_getInterfacesMethod); + + return null; } static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) From f6b1e7bf05d361b38333b641764d04c88a51466e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 15:13:33 +0200 Subject: [PATCH 12/14] Fix review findings: DoNotRegister, remove extra blank line, clean up test - Use DoNotTransfer | DoNotRegister when wrapping JNI class handles in Java.Lang.Class to avoid peer table registration overhead - Remove extra blank line before GetProxyForJavaObject - Remove verbose diagnostic logging from device test, keep only the assertion with descriptive failure message - Remove unused Log import from test - Keep hierarchy walk using JniObjectReference (not Java.Lang.Class) to avoid potential recursion into TypeMap during class resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 5 ++-- .../TrimmableTypeMapTypeManagerTests.cs | 24 ------------------- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ac6994c7f9b..d496997c830 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -231,7 +231,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } - internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { @@ -282,9 +281,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // getInterfaces() returns only directly declared interfaces (not transitive), // so we recurse into super-interfaces to find the matching TypeMap entry. + // We wrap the JNI class handle in Java.Lang.Class to call the existing + // GetInterfaces() binding which handles method ID caching and JNI remapping. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { - using var cls = new Java.Lang.Class (jniClass.Handle, JniHandleOwnership.DoNotTransfer); + using var cls = new Java.Lang.Class (jniClass.Handle, JniHandleOwnership.DoNotTransfer | JniHandleOwnership.DoNotRegister); var interfaces = cls.GetInterfaces (); if (interfaces is null || interfaces.Length == 0) { return null; 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 068eafa992a..89244254587 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 @@ -442,7 +442,6 @@ namespace Xamarin.Android.NetTests using Java.Security; using Javax.Net.Ssl; using NUnit.Framework; - using Log = global::Android.Util.Log; [TestFixture] public class TrimmableTypeMapTrustManagerTests @@ -459,29 +458,6 @@ public void TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager () bool foundX509 = false; foreach (var tm in trustManagers) { - string javaClass = JNIEnv.GetClassNameFromInstance (tm.Handle); - Log.Info ("TypeMapTest", $"--- TrustManager element ---"); - Log.Info ("TypeMapTest", $" Managed type: {tm.GetType ().FullName}"); - Log.Info ("TypeMapTest", $" Java class: {javaClass}"); - Log.Info ("TypeMapTest", $" is IX509TrustManager: {tm is IX509TrustManager}"); - - // Walk the Java class hierarchy to see what the typemap would encounter - var selfRef = new Java.Interop.JniObjectReference (tm.Handle); - var jniClass = Java.Interop.JniEnvironment.Types.GetObjectClass (selfRef); - try { - int depth = 0; - while (jniClass.IsValid) { - var name = Java.Interop.JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); - Log.Info ("TypeMapTest", $" hierarchy[{depth}]: {name}"); - var super = Java.Interop.JniEnvironment.Types.GetSuperclass (jniClass); - Java.Interop.JniObjectReference.Dispose (ref jniClass); - jniClass = super; - depth++; - } - } finally { - Java.Interop.JniObjectReference.Dispose (ref jniClass); - } - if (tm is IX509TrustManager) { foundX509 = true; } From f546bce5f2d4981ca96e0d2eda83d76b9fd77bc1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 15:24:43 +0200 Subject: [PATCH 13/14] Dispose interface Class[] elements to avoid gref leaks Java.Lang.Class.GetInterfaces() returns Java.Lang.Class[] where each element holds a global ref. Dispose them in a finally block to avoid leaking grefs until GC collection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index d496997c830..feb25955168 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -291,24 +291,30 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - foreach (var iface in interfaces) { - if (iface is null) { - continue; - } + try { + foreach (var iface in interfaces) { + if (iface is null) { + continue; + } - var ifaceRef = new JniObjectReference (iface.Handle); - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef); - if (ifaceName != null) { - var proxy = self.GetProxyForJniClass (ifaceName, targetType); - if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { - return proxy; + var ifaceRef = new JniObjectReference (iface.Handle); + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (ifaceRef); + if (ifaceName != null) { + var proxy = self.GetProxyForJniClass (ifaceName, targetType); + if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; + } } - } - // Recurse into super-interfaces - var result = TryMatchInterfaces (self, ifaceRef, targetType); - if (result != null) { - return result; + // Recurse into super-interfaces + var result = TryMatchInterfaces (self, ifaceRef, targetType); + if (result != null) { + return result; + } + } + } finally { + foreach (var iface in interfaces) { + iface?.Dispose (); } } From fb52640ed11419ff5bef9375d454a069be885467 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 18 May 2026 15:27:32 +0200 Subject: [PATCH 14/14] Move TrustManager test to own file in Xamarin.Android.Net directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapTypeManagerTests.cs | 34 ------------------- .../TrustManagerMarshallingTests.cs | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs 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 89244254587..3ccae0da3ad 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 @@ -435,37 +435,3 @@ class TrimmableRegisteredGenericHolder : Java.Lang.Object public T Value { get; set; } } } - -namespace Xamarin.Android.NetTests -{ - using System.Linq; - using Java.Security; - using Javax.Net.Ssl; - using NUnit.Framework; - - [TestFixture] - public class TrimmableTypeMapTrustManagerTests - { - [Test] - public void TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager () - { - var tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); - tmf.Init ((KeyStore?) null); - - var trustManagers = tmf.GetTrustManagers (); - Assert.IsNotNull (trustManagers, "GetTrustManagers returned null"); - Assert.IsTrue (trustManagers.Length > 0, "GetTrustManagers returned empty array"); - - bool foundX509 = false; - foreach (var tm in trustManagers) { - if (tm is IX509TrustManager) { - foundX509 = true; - } - } - - Assert.IsTrue (foundX509, - $"No ITrustManager element was marshalled as IX509TrustManager. " + - $"Types found: {string.Join (", ", trustManagers.Select (t => t.GetType ().FullName))}"); - } - } -} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs new file mode 100644 index 00000000000..76a984979c1 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Android.Runtime; +using Java.Security; +using Javax.Net.Ssl; +using NUnit.Framework; + +namespace Xamarin.Android.NetTests +{ + [TestFixture] + public class TrustManagerMarshallingTests + { + [Test] + public void TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager () + { + var tmf = TrustManagerFactory.GetInstance (TrustManagerFactory.DefaultAlgorithm); + tmf.Init ((KeyStore?) null); + + var trustManagers = tmf.GetTrustManagers (); + Assert.IsNotNull (trustManagers, "GetTrustManagers returned null"); + Assert.IsTrue (trustManagers.Length > 0, "GetTrustManagers returned empty array"); + + bool foundX509 = false; + foreach (var tm in trustManagers) { + if (tm is IX509TrustManager) { + foundX509 = true; + } + } + + Assert.IsTrue (foundX509, + $"No ITrustManager element was marshalled as IX509TrustManager. " + + $"Types found: {string.Join (", ", trustManagers.Select (t => t.GetType ().FullName))}"); + } + } +}