Skip to content

[TrimmableTypeMap] R8 shrinking removes unreferenced AndroidJavaSource (user Java) classes under NativeAOT #11774

Open

Description

@simonrozsival

Summary

Under the trimmable typemap (NativeAOT), R8 shrinking removes user-authored AndroidJavaSource Java classes that aren't referenced from the managed dependency graph. This breaks multidex behavior and, more importantly, can break real apps that call user Java via JNI/reflection (which ILC's managed graph can't see).

Reproduced via BuildAfterMultiDexIsNotRequired(NativeAOT):

Expected: file exists
But was:  ".../obj/Release/android/bin/classes2.dex"

Diagnosis

CreateMultiDexRequiredApplication adds two AndroidJavaSource files (ManyMethods.java, ManyMethods2.java), each with 32768 methods (Bind=False, unreferenced) → 65536 methods > the 64K dex limit → must produce classes2.dex.

Local repro (NativeAOT, arm64):

  • The Java is compiled: obj/Release/.../android/bin/classes/ManyMethods.class is 1.7 MB each.
  • The classes zip that D8/R8 consumes (classes.zip, 3.9 MB) contains ManyMethods.class / ManyMethods2.class.
  • But the produced classes.dex is only ~275 KB and contains none of those methods, and no classes2.dex is produced.

Because AndroidEnableMultiDex=True selects R8 (Xamarin.Android.D8.targets, _UseR8), and _R8EnableShrinking=True for AndroidLinkTool=r8, R8 shrinks the unreferenced ManyMethods classes away. The trimmable NativeAOT proguard config (GenerateNativeAotProguardConfiguration, built from the ILC DGML + acw-map) keeps ACW/JCW types and android.R/android.Manifest, but not user AndroidJavaSource classes — so R8 drops them.

The legacy/CoreCLR path keeps them (the CoreCLR variant of the test passes).

Why this matters beyond the test

AndroidJavaSource (Bind=False) classes are user-authored Java, often invoked from native/JNI or reflection. R8 shrinking them based on the managed reachability graph is unsound and can silently remove Java the app needs at runtime.

Fix direction (needs design)

  • Keep user AndroidJavaSource-derived classes in the trimmable NativeAOT proguard config (emit -keep for them), or
  • Don't apply R8 shrinking to the user Java side under NativeAOT (the managed side is already trimmed by ILC; the Java side's reachability isn't captured by the managed graph), or
  • Treat the app's own compiled Java classes as kept program classes.

Acceptance criteria

  • BuildAfterMultiDexIsNotRequired(NativeAOT) passes (classes2.dex produced when required, absent when not).
  • Unreferenced user AndroidJavaSource classes survive into the dex on NativeAOT.
  • No unnecessary dex bloat for normal apps.

References

  • Repro test: BuildAfterMultiDexIsNotRequired (src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs), CreateMultiDexRequiredApplication.
  • Code: src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets (_UseR8, _R8EnableShrinking), src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets (GenerateNativeAotProguardConfiguration).

Activity

  1. simonrozsival commented on Jul 1, 2026

    @simonrozsival
    MemberAuthor

    Another repro of this same bug (R8 shrinking Java out on trimmable NativeAOT due to missing keeps), from a different angle — bound library Java types rather than unreferenced Bind=False user Java:

    BindingWithAndroidJavaSource(NativeAOT) fails:

    `.../App/obj/Release/android/bin/classes.dex` should include `Lcom/xamarin/android/test/msbuildtest/JavaSourceJarTest;`!
    Expected: True / But was: False
    

    Verified via binlog that R8 does receive the library jars (obj/Release/lp/1/jl/libs/*.jar) as JavaLibrariesToEmbed, but no keep rule preserves JavaSourceJarTest / JavaSourceTestExtension / JavaSourceTestInterface, so R8 shrinks them out.

    One concrete fix lead this adds: the proguard config that does keep these types, proguard_project_references.cfg ($(_ProguardProjectConfiguration)), is explicitly excluded from R8 on trimmable NativeAOT in Xamarin.Android.Common.targets:

    <_ProguardConfiguration Include="$(_ProguardProjectConfiguration)"
        Condition=" '$(AndroidLinkTool)' != '' and ('$(_AndroidRuntime)' != 'NativeAOT' or '$(_AndroidTypeMapImplementation)' != 'trimmable') " />

    That file already contains the correct, complete keeps:

    -keep class com.xamarin.android.test.msbuildtest.JavaSourceTestInterface { *; }
    -keep class com.xamarin.android.test.msbuildtest.JavaSourceJarTest { *; }
    -keep class com.xamarin.android.test.msbuildtest.JavaSourceTestExtension { *; }
    

    …while the trimmable replacement proguard_project_primary.cfg is empty apart from # ACW keep rules are generated from NativeAOT ILC metadata.

    So in addition to the fix directions already listed here, one option is simply to stop excluding proguard_project_references.cfg on trimmable NativeAOT (it already carries the right keeps via the standard ACW-map path) — though the exclusion was deliberate, so it's worth confirming it doesn't over-keep and undermine trimming/size.

    This surfaces once NativeAOT defaults to the trimmable typemap. Opened #11835 for the binding-specific repro before finding this issue; closing that as a duplicate of this one. Acceptance criteria there: BindingWithAndroidJavaSource(NativeAOT) should also pass.

  2. added this to the .NET 12 milestone on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssues that need to be assigned.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions