From b294c5b2f3925aa888061b1656c72bbb62caeb66 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 10:43:43 +0200 Subject: [PATCH 1/3] [TrimmableTypeMap] Fix stale legacy JCW contamination when switching typemap flavors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When switching MonoAndroidTypeMapFlavor between legacy and trimmable without a clean build, stale legacy JCWs with Runtime.register(typeName, klass, methods) calls persisted in the android/src/ intermediate directory alongside trimmable JCWs that only use Runtime.registerNatives(). Both got compiled into the APK, causing the legacy registration path to execute at runtime — a path incompatible with trimming. Root cause: the trimmable JCW generator wrote to typemap/java/ and then copied to android/src/, but never cleaned android/src/ first. Legacy JCWs with different package names survived the copy overlay. Three fixes: 1. Write trimmable JCWs directly to _AndroidIntermediateJavaSourceDirectory (android/src/) instead of a separate typemap/java/ directory, eliminating the copy step entirely. This is the same directory that _FindJavaStubFiles globs from, so no files can be missed or stale. 2. Add MonoAndroidTypeMapFlavor to the build properties cache so switching between legacy and trimmable triggers _CleanIntermediateIfNeeded, removing all stale artifacts from the previous flavor. 3. Always wire registerJniNativesFn so that if a stale JCW somehow calls Runtime.register(), TrimmableTypeMapTypeManager.RegisterNativeMembers throws UnreachableException with a clear diagnostic instead of the C++ side silently dropping the call. Also adds a unit test verifying trimmable JCWs never emit Runtime.register(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 4 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 3 +- .../Xamarin.Android.Build.Tests/BuildTest.cs | 43 +++++++++++++++++++ .../Generator/JcwJavaSourceGeneratorTests.cs | 10 +++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 2e8898e4616..15ea031a49b 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -185,9 +185,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap) { - args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; - } + args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; RunStartupHooksIfNeeded (); SetSynchronizationContext (); } 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 6ff49d6868c..99ec870dcda 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 @@ -19,7 +19,6 @@ <_TypeMapBaseOutputDir Condition=" '$(_TypeMapBaseOutputDir)' == '' ">$(IntermediateOutputPath) <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ - <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java @@ -63,7 +62,7 @@ Date: Thu, 9 Apr 2026 17:44:15 +0200 Subject: [PATCH 2/3] Add registerNatives assertion to ACW test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGeneratorTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 06443d87acb..47aa779f5eb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,9 +138,10 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_AcwType_NeverCallsRuntimeRegister () { var java = GenerateFixture ("my/app/MainActivity"); - // Trimmable JCWs must only call Runtime.registerNatives(klass). - // Runtime.register(typeName, klass, methods) is the legacy path that - // relies on reflection-based callback registration — incompatible with trimming. + // Trimmable JCWs must call Runtime.registerNatives(klass) and avoid + // the legacy Runtime.register(typeName, klass, methods) path, which + // relies on reflection-based callback registration and breaks trimming. + AssertContainsLine ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); Assert.DoesNotContain ("Runtime.register (\"", java); } From 7d12929be4cbe4a5d7f8c76d2f9cab3ff5c467d7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 18:19:54 +0200 Subject: [PATCH 3/3] Pin typemap switching test to CoreCLR Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/BuildTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index e7b92eb1930..a67d06dd2c8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -850,6 +850,7 @@ public void BuildAfterUpgradingNuget ([Values] AndroidRuntime runtime) public void SwitchingTypeMapImplementationTriggersClean () { var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); using (var b = CreateApkBuilder ()) { b.CleanupAfterSuccessfulBuild = b.CleanupOnDispose = false;