From 4c5c8a5b9716f598940a5fe803d0432a03ea8f7f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 18 Sep 2026 12:57:04 -0500 Subject: [PATCH] [NativeAOT] Preserve GC bridge temporary peers through R8 R8 can reduce mono.android.GCUserPeer to an abstract class with no constructor or reference callbacks. NativeAOT constructs this helper through JNI when processing empty strongly connected components, so NewObject returns null and the GC bridge aborts. Keep only the helper class, default constructor, and two JNI reference callbacks. Add a Release NativeAOT build regression that verifies all three methods survive in the final DEX output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../proguard_trimmable_nativeaot.cfg | 6 ++++ .../TrimmableTypeMapBuildTests.cs | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg index 8c53ede5e9c..5b90c0906de 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg +++ b/src/Xamarin.Android.Build.Tasks/Resources/proguard_trimmable_nativeaot.cfg @@ -6,6 +6,12 @@ -keep class net.dot.android.crypto.** { *; (...); } # NativeAOT resolves these interface methods through JNI during startup. -keep class mono.android.IGCUserPeer { *; } +# The GC bridge constructs temporary peers and calls these methods through JNI. +-keep class mono.android.GCUserPeer { + (); + public void monodroidAddReference(java.lang.Object); + public void monodroidClearReferences(); +} # Native hosts resolve these package-private fields by name during startup. -keepclassmembers class mono.android.Runtime { static java.lang.Class *; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 38fd53867a1..cf7754c0c1e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -1640,6 +1640,38 @@ public void Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws () $"`{dexFile}` should include the UncaughtExceptionMarshaler runtime ACW."); } + [Test] + public void Build_WithTrimmableTypeMap_KeepsNativeAotGcBridgeTemporaryPeer () + { + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + LinkTool = "r8", + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var dexDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin")); + var dexFiles = Directory.GetFiles (dexDirectory, "classes*.dex"); + Assert.IsNotEmpty (dexFiles, "R8 should produce DEX files."); + foreach (var (method, signature) in new [] { + ("", "()V"), + ("monodroidAddReference", "(Ljava/lang/Object;)V"), + ("monodroidClearReferences", "()V"), + }) { + Assert.IsTrue (dexFiles.Any (dex => DexUtils.ContainsClassWithMethod ( + "Lmono/android/GCUserPeer;", method, signature, dex, AndroidSdkPath)), + $"R8 must preserve GCUserPeer.{method}{signature} for the native GC bridge."); + } + } + [Test] public void Build_WithTrimmableTypeMap_DeletesStaleGeneratedJavaSources () {