[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking - #11798

Merged
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java
Jun 29, 2026
Merged

[TrimmableTypeMap] Keep user AndroidJavaSource Java under R8 shrinking#11798
jonathanpeppers merged 3 commits into
mainfrom
dev/simonrozsival/r8-keep-user-java

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

When R8 code shrinking is enabled, R8 removes any Java type that nothing keeps. .NET for Android keeps the Java callable wrappers (JCWs) of bound managed types by generating -keep rules from the acw-map (managed peer ↔ Java type). That works for generated JCWs, but user-authored AndroidJavaSource.java files marked Bind != true have no managed peer, so they never appear in the acw-map — and therefore get no keep rule. With shrinking on, R8 silently deletes them, and the app fails at runtime when it references those classes (often as a NoClassDefFoundError that only reproduces in Release/shrunk builds).

Fix

Pass user AndroidJavaSource (Bind != True) to the R8 task and emit -keep class <package>.<Type> { *; } for each, so user Java survives shrinking:

  • D8.targets — collect @(AndroidJavaSource) items where %(Bind) != 'True' into _R8KeepJavaSource and pass them to R8 via the new JavaSourceFiles property.
  • R8.cs — append keep rules in the same block that emits acw-map keeps. GetUserJavaTypes() derives the fully-qualified name as <package>.<FileNameWithoutExtension> (Java requires the public top-level type name to match the file name); ReadJavaPackage() parses the package declaration, skipping comments and stopping at the first import/type declaration. Names are de-duplicated.
  • R8Tests.cs — unit tests covering ReadJavaPackage: package present, trailing space before ;, comment headers, no package, and package after import/type (ignored).

Why split out

This is being carved out of the trimmable typemap mega-PR #11617. Although the bug surfaced while bringing up the NativeAOT trimmable path, it is not trimmable-specific — the acw-map keep logic and R8 shrinking apply equally to legacy/MonoVM and CoreCLR, so user AndroidJavaSource was at risk of being shrunk there too. Landing it independently de-risks #11617 and ships a general correctness fix sooner.

Context / related PRs

Verification

24/24 R8 unit tests pass (18 existing + 6 new). BuildAfterMultiDexIsNotRequired verified locally on NativeAOT and CoreCLR — user Java is retained after shrinking.

simonrozsivaland others added 2 commits June 29, 2026 09:54
The trimmable NativeAOT path enables R8 with shrinking (AndroidLinkTool=r8 ->
_R8EnableShrinking=True). When the application ProGuard config is generated from
the acw-map (the default, UseTrimmableNativeAotProguardConfiguration=false), the
R8 task only emits -keep rules for managed-mapped Java types. User-authored
AndroidJavaSource (Bind != true) has no managed peer and is therefore absent from
the acw-map, so R8 shrank it away. This made BuildAfterMultiDexIsNotRequired fail
on NativeAOT: the huge ManyMethods.java classes were removed, so multidex was no
longer required and classes2.dex was never produced.
Pass the user AndroidJavaSource (.java with Bind != true) to the R8 task and emit
'-keep class <package>.<Type> { *; }' for each, so user Java survives shrinking.
The type name is '<package>.<FileNameWithoutExtension>' (Java requires the public
top-level type name to match the file name).
Verified locally: BuildAfterMultiDexIsNotRequired(NativeAOT) and (CoreCLR) pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover package detection: present, trailing space, comment headers, no
package, and package-after-import/type (ignored). Exposes ReadJavaPackage
as internal for direct testing, matching the TryGetDisallowedOption pattern.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings June 29, 2026 07:59
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes R8 shrinking removing user-authored AndroidJavaSource.java files (those without a managed peer / absent from acw-map) by deriving their Java FQNs and emitting -keep class … { *; } rules when shrinking is enabled.

Changes:

  • Passes @(AndroidJavaSource) items with Bind != True to the R8 MSBuild task via a new JavaSourceFiles input.
  • Adds R8.GetUserJavaTypes() + R8.ReadJavaPackage() to derive the fully-qualified class names from .java sources and append -keep rules.
  • Adds unit tests for ReadJavaPackage parsing behavior across several common source layouts.
Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targetsCollects non-binding AndroidJavaSource items and forwards them to R8 as JavaSourceFiles.
src/Xamarin.Android.Build.Tasks/Tasks/R8.csAdds Java package parsing + FQN derivation and appends -keep rules to the generated R8 app config.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.csAdds test coverage for ReadJavaPackage() parsing rules.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment threadsrc/Xamarin.Android.Build.Tasks/Tasks/R8.cs
* R8Tests: expected parameter is string? (TestCase passes null).
* R8: comment that only the public top-level type is kept, and that
ReadJavaPackage is a lightweight scan (package precedes types in practice).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
Comment on lines +54 to +56
<!-- User-authored AndroidJavaSource (Bind != True) is not in the acw-map; pass it to R8 so it
is kept when shrinking is enabled rather than silently removed. -->
<_R8KeepJavaSource Include="@(AndroidJavaSource)" Condition=" '%(AndroidJavaSource.Bind)' != 'True' " />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not an easy way to turn this off if you don't want it, but I can't think of a case you would need to do that.

@jonathanpeppers
jonathanpeppers merged commit e86f075 into mainJun 29, 2026
40 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/r8-keep-user-java branch June 29, 2026 13:27
jonathanpeppers pushed a commit that referenced this pull request Jun 30, 2026
…TypeManagers (#11799)
## Summary
Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions.
This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work.
## Changes
- `external/Java.Interop` → `8d544738a` (from `70493645c`).
- Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance.
- `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override.
- Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries).
- `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths.
- No NativeAOT default change; the trimmable type/value managers are **not** part of this PR.
## Tests / baselines
- Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change).
- Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`.
## Context
Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jul 2, 2026
…11801)
## Summary
This PR adds the two runtime managers that make the **trimmable typemap** a fully functional, reflection-free peer-resolution path:
- **`TrimmableTypeMapValueManager`** — creates and tracks Java↔managed peers without reflection or `Activator`.
- **`TrimmableTypeMapTypeManager`** — resolves managed↔Java type mappings by delegating to the generated `TrimmableTypeMap`.
Both are selected through the existing `RuntimeFeature.TrimmableTypeMap` switch and wired into `JNIEnvInit`. The trimmable path is **opt-in**; the NativeAOT default remains `managed`, and Mono/CoreCLR defaults are unchanged. Also included is the `JavaConvert` collection-factory refactor and a generator fix that the trimmable managers depend on.
## Background
The classic Java.Interop runtime resolves the managed `Type` for a Java instance (and vice-versa) using `Type.GetType`, `MakeGenericType`, and `Activator`-style construction. That is incompatible with trimming and NativeAOT: the trimmer can't see which types are reachable, and the types can't be created at runtime under AOT.
The **trimmable typemap** replaces those reflection calls with a build-time-generated map of `JavaPeerProxy` objects (produced by `Microsoft.Android.Sdk.TrimmableTypeMap`). Each proxy knows how to construct a specific peer and describes its JNI ↔ managed association statically. Earlier PRs landed the generator, the proxy shapes (array / generic-base / interface / unresolvable peers), and the CoreCLR `JavaMarshal` value-manager split. **This PR adds the runtime managers that consume that generated map.**
## What's in this PR
### 1. `TrimmableTypeMapValueManager` (new)
A `JniRuntime.JniValueManager` that performs peer creation with **no reflection**:
- **Peer lifetime** (`AddPeer` / `PeekPeer` / `RemovePeer` / `FinalizePeer` / `CollectPeers` / `GetSurfacedPeers`) is delegated to `JavaMarshalRegisteredPeers`, i.e. the CoreCLR `JavaMarshal` GC-bridge machinery. `WaitForGCBridgeProcessing` is intentionally a no-op (documented: the wait can't close the bridge race on CoreCLR, where JNI wrapper threads hold their own `JniObjectReference` copies).
- **`CreatePeer`** resolves the requested target type (mapping `object`/`IJavaPeerable` → `Java.Interop.JavaObject`, `Exception` → `JavaException`) and asks `TrimmableTypeMap.Instance.CreateInstance (handle, resolvedType)` to build the peer from a generated proxy.
- **`ActivatePeer`** throws `PlatformNotSupportedException` — reflection-based activation is not part of this path.
- **`NotFoundFallback`** carefully reproduces the base `JniValueManager.CreatePeer` contract so `JavaCast`/`JavaAs` still surface the correct outcome when no proxy is found:
- target type has no Java mapping → `ArgumentException`
- Java instance not assignable to the target's Java class → `null` (so `JavaAs` returns null / `JavaCast` throws `InvalidCastException`)
- compatible classes but no proxy → `NotSupportedException` (a genuine generator gap, with a message pointing at the missing proxy)
The assignability check honors `RuntimeFeature.IsAssignableFromCheck` and mirrors the legacy cast diagnostic when assembly logging is enabled.
### 2. `TrimmableTypeMapTypeManager` (new)
A `JniRuntime.JniTypeManager` that has exactly two live responsibilities and throws for everything else it doesn't need:
- **Managed → Java** via `GetTypeSignatureCore`, backed by a `ConcurrentDictionary<Type, JniTypeSignature>` cache.
- **Java → managed** via `GetTypes` / `GetTypeForSimpleReference`, delegating to `TrimmableTypeMap`.
- **Array handling** diverges by runtime: NativeAOT reads a pre-generated array-proxy map (types can't be built at runtime), while CoreCLR builds array/generic types dynamically to save app size (suppressions are scoped to the CoreCLR-only branch).
### 3. `JNIEnvInit` wiring
`CreateValueManager` / `CreateTypeManager` now return the trimmable managers when `RuntimeFeature.TrimmableTypeMap` is set. The existing Mono / CoreCLR / managed selection is preserved, and the manager constructions were refactored into small local helpers so trimming suppressions (`IL2026` / `IL3050`) apply only to the exact branch that needs them. `RegisterJniNatives` is likewise gated so the reflection-based JNI registration path isn't emitted for the trimmable typemap.
### 4. `JavaConvert` collection-factory refactor
Generic collection marshalling (`IDictionary<,>` → `JavaDictionary<,>`, `IList<>` → `JavaList<>`, `ICollection<>` → `JavaCollection<>`) is split into two branches: a **factory-based converter** on the trimmable path (no `MakeGenericType`) and the classic `MakeGenericType` path elsewhere, with the reflection-requiring code isolated behind narrowly-scoped suppressions. Also adds `Nullable<T>` converter handling (null reference → null value).
### 5. Generator fix (`ModelBuilder`)
Emits a managed→Java typemap entry for **self-peer types** (`[JniTypeSignature(GenerateJavaPeer=false)]` or MCW bindings with no activation ctor). These are constructed managed-side with `new`, but their JNI name must still resolve so the correct Java class is instantiated; without the association they fell back to the generic `mono.android.runtime.JavaObject` peer and threw `ArrayStoreException` when placed into a typed Java array.
### 6. Cleanup
Removes the dead `TrimmableTypeMap` branch from `JavaMarshalValueManager` (that logic now lives in the dedicated `TrimmableTypeMapValueManager`).
## Behavioral impact
- **Opt-in only.** With `RuntimeFeature.TrimmableTypeMap` unset, behavior is unchanged. NativeAOT still defaults to `managed`.
- No new user-facing / localized strings; error messages point at the generator when a proxy is genuinely missing.
## Status
All earlier prerequisites have **merged into `main`**, and this PR is **rebased on latest `main`**, so it is no longer stacked or blocked — it contains only the value/type-manager implementations (plus the supporting `JavaConvert` and generator changes) on top of them:
- #11799 — CoreCLR `JavaMarshal` split + Java.Interop bump
- #11753 array proxies · #11749 generic base · #11751 unresolvable peers · #11769 interface proxies
- #11794 multidex/manifest base · #11796 manifest parity · #11798 R8 keep
## Testing
- `TrimmableTypeMapTypeManagerTests` and `TypeMapModelBuilderTests` updated/extended for the new type resolution and the self-peer generator fix.
- Export tests enabled for the trimmable typemap; `TrimmableTypeMapUnsupported` cases excluded.
- NativeAOT warning-count and CoreCLR `apkdesc` baselines updated to match.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 30, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author thisready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers