Skip to content

[TrimmableTypeMap] Honor AndroidApplicationJavaClass for the JCW base and manifest#11794

Merged
jonathanpeppers merged 1 commit into
mainfrom
dev/simonrozsival/trimmable-multidex-appjavaclass
Jun 29, 2026
Merged

[TrimmableTypeMap] Honor AndroidApplicationJavaClass for the JCW base and manifest#11794
jonathanpeppers merged 1 commit into
mainfrom
dev/simonrozsival/trimmable-multidex-appjavaclass

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Summary

When $(AndroidApplicationJavaClass) is set — e.g. to android.support.multidex.MultiDexApplication when $(AndroidEnableMultiDex) is true — the trimmable typemap (NativeAOT) did not account for it. This caused two NativeAOT-only failures that the legacy typemap implementation handled correctly:

  • CustomApplicationClassAndMultiDex — A user Application subclass's JCW extended android.app.Application instead of the multidex base, breaking MultiDex apps. JcwJavaSourceGenerator now applies the same swap CallableWrapperType does: if a type's base is android.app.Application and an application-java-class override is set, it emits that override as the extends clause.
  • ClassLibraryHasNoWarnings — The injected MultiDexApplication manifest name has no managed peer, so RootManifestReferencedTypes logged a spurious XA4250. It's a Java framework type, so the warning is now skipped when the unresolved name is the configured application-java-class override.

The application-java-class value is threaded from ManifestConfig through to both the JCW generator and manifest-reference rooting.

Context

This was previously part of the trimmable typemap mega-PR (#11617). The fix is fully self-contained — JcwJavaSourceGenerator and TrimmableTypeMapGenerator already exist on main, so it applies cleanly on its own and is split out for independent review/merge.

Tests

Adds generator unit tests for both behaviors:

  • JcwJavaSourceGeneratorTests — JCW base-class swap when the override is set.
  • TrimmableTypeMapGeneratorTests — manifest rooting no longer warns on the override class.

All 82 generator unit tests pass locally.

… and manifest (J1, J3)

When $(AndroidApplicationJavaClass) is set — e.g. to
android.support.multidex.MultiDexApplication when $(AndroidEnableMultiDex) is
true — the trimmable typemap did not account for it, causing two NativeAOT-only
failures:

* CustomApplicationClassAndMultiDex: a user Application subclass's JCW extended
  android.app.Application instead of the multidex base. JcwJavaSourceGenerator now
  applies the same swap the legacy CallableWrapperType does: if a type's base is
  android.app.Application and an application-java-class override is set, emit that
  override as the `extends` clause.

* ClassLibraryHasNoWarnings: the injected MultiDexApplication manifest name has no
  managed peer, so RootManifestReferencedTypes logged a spurious XA4250. It is a
  Java framework type, so the warning is now skipped when the unresolved name is
  the configured application-java-class override.

The application-java-class value is threaded from ManifestConfig through to both
the JCW generator and manifest-reference rooting. Adds generator unit tests for
both behaviors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026
Copilot AI review requested due to automatic review settings June 29, 2026 06:53

Copilot AI 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

Updates the TrimmableTypeMap (NativeAOT) generator pipeline to correctly honor $(AndroidApplicationJavaClass) in both generated JCW Java sources and manifest-referenced type rooting, aligning behavior with the legacy typemap implementation (notably for MultiDex scenarios).

Changes:

  • Thread ApplicationJavaClass from ManifestConfig into the JCW generator and manifest rooting logic.
  • Generate Application-subclass JCWs to extends $(AndroidApplicationJavaClass) when the managed base is android.app.Application.
  • Suppress XA4250 warnings for the configured AndroidApplicationJavaClass when it is referenced by the manifest but has no managed peer (e.g., android.support.multidex.MultiDexApplication), and add unit tests covering both behaviors.
Show a summary per file
File Description
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs Adds a unit test asserting manifest rooting does not warn for the configured application Java class override.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs Adds unit tests verifying JCW extends behavior for Application subclasses with/without AndroidApplicationJavaClass.
src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs Passes ApplicationJavaClass through to manifest rooting and JCW generation; skips warning for unresolved configured application Java class.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs Adds optional applicationJavaClass parameter and swaps the JCW base class for Application subclasses accordingly.

Copilot's findings

  • Files reviewed: 4/4 changed files
  • Comments generated: 0

@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@simonrozsival simonrozsival 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
jonathanpeppers pushed a commit that referenced this pull request Jun 29, 2026
#11798)

## 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

- #11617 — parent mega-PR (reflection-free trimmable typemap) this is sliced from.
- #11643 — removes the managed NativeAOT typemap in favor of the trimmable one (the path that exposed this gap).
- Sibling slices already split out off `main`: #11794 (AndroidApplicationJavaClass/multidex), #11796 (manifest generation parity). Other merged groundwork: #11749, #11751, #11752, #11753, #11764, #11769.

## Verification

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers merged commit 8a58176 into main Jun 29, 2026
41 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/trimmable-multidex-appjavaclass branch June 29, 2026 13:56
simonrozsival added a commit that referenced this pull request Jun 29, 2026
)

## Summary

Follow-up to the trimmable typemap manifest-generation work being split out of #11617 (and to the XA4243 manifest fix in #11785). This brings the **trimmable typemap** `ManifestGenerator` to parity with the legacy `ManifestDocument` for NativeAOT, completing the activity attribute coverage and the library/placeholder merge behavior that #11752 didn't cover.

## Changes

* **`PropertyMapper`** — add the remaining activity manifest attributes: `banner`, `colorMode`, `enableVrMode`, `lockTaskMode`, `logo`, `maxAspectRatio`, `maxRecents`, `allowEmbedded`, `autoRemoveFromRecents`, `relinquishTaskIdentity`, `resumeWhilePausing`, `showForAllUsers`/`showOnLockScreen`/`showWhenLocked`, `singleUser`, `visibleToInstantApps`, `recreateOnConfigChanges`, `rotationAnimation`. Adds a `Number` mapping kind with invariant-culture formatting for `maxAspectRatio`/`maxRecents`.
* **`ComponentElementBuilder`** — emit `<layout>` child properties from `LayoutProperties`.
* **`ComponentInfo`** — carry `LayoutProperties`.
* **`AndroidEnumConverter`** — `RotationAnimation` and `persistableMode` conversions.
* **`ManifestGenerator`** — merge library (`.aar`) manifests for the legacy merger, resolve/normalize `$(AndroidManifestPlaceholders)` (incl. package-name resolution), warn XA1010 on invalid placeholders, sort component attributes alphabetically, dedup `tools:`-namespace nodes, and emit `uses-sdk` correctly when no min is specified.

## Context

Part of slicing the #11617 mega-PR into reviewable units. `ManifestGenerator` and friends already exist on `main` (via #10991, #11037, #11143, #11752), so this applies cleanly on its own. Companion slices: #11794 (AndroidApplicationJavaClass/multidex).

## Tests

Extends `ManifestGeneratorTests` to cover all activity attributes, layout properties, placeholder resolution/warnings, library-manifest merge, and dedup. **575 generator unit tests pass locally.**
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this ready-to-review This 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