Skip to content

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

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssues that need to be assigned.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions