From 5c2760d052425190378957d47d7440da25c6fb43 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 14 May 2026 10:56:12 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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