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 () {