Skip to content

[Trimming] ILLink preserves unused members in Android Bindings with TrimMode=full #12270

Description

@TimBurik

Android framework version

net10.0-android

Affected platform version

.NET 10.0.100

Description

We've been analyzing the proguard rules that are generated for our Android application during the build, and noticed that for types wrapped in Android Binding project there are much more "keep class" and "keepclassmembers" rules generated then expected.

Expected result:
If Main Application is using TrimMode=full and AndroidLinkTool=r8, then only actually used Java classes and members should be explicitly preserved as reachable from managed code.

Actual result:
For all reachable Java classes all methods are explicitly preserved via generated proguard files, which in turn may preserve classes that are not reachable from managed code.

Steps to Reproduce

Here's the simplified example, similar to to what we have in our application:

  1. Android Binding project is marked with IsTrimmable=true and distributed as NuGet package. It wraps Java API similar to following:
package com.third.party;

class Sdk {
  static Sdk getInstance();
  void initialize(String key);
  Service getService();
}

interface Service {
  void load(Size size);
}

class Size {
  int getHeight();
  int getWidth();
}
  1. MainApplication is using TrimMode=full and AndroidLinkTool=r8. It imports the NuGet package with bindings and uses it the following way (no other APIs are used):
Sdk.GetInstance().Initialize("sdk_key");
  1. After the build, we see the following generated rules

In proguard_project_references.cfg:

-keep class com.third.party.Sdk
-keepclassmembers class com.third.party.Sdk {
   *** getService(...);
   *** getInstance(...);
   *** initialize(...);
}

-keep class com.third.party.Service
-keepclassmembers class com.third.party.Service {
}

-keep class com.third.party.Size
-keepclassmembers class com.third.party.Size {
   *** getHeight(...);
   *** getWidth(...);
}

In proguard_project_primary.cfg:

-keep class com.third.party.Service { *; }
-keep class com.third.party.Service { *; }
-keep class com.third.party.Service { *; }

So not only the unused Sdk.getService() method was preserved, it also started a chain that preserved other unused types. I would also assume that corresponding managed types were not trimmed.

The culprit here might be MarkJavaObjects task, specifically how methods are selected for preservation:

if (IsUserType (type) && type.HasMethods) {
foreach (var method in type.Methods.Where (m => m.Overrides != null))
PreserveMethod (type, method);

It uses condition type.Methods.Where(m => m.Overrides != null) to select methods, but maybe better way would be to check for type.Methods.Where(m => m.HasOverrides), as Overrides might return empty collection instead of null (https://github.com/jbevain/cecil/blob/882ca5eedda1e62eb41bd5869aeb15d8f1538e51/Mono.Cecil/MethodDefinition.cs#L222-L243)

Did you find any workaround?

No response

Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: App+Library BuildIssues when building Library projects or Application projects.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions