Skip to content

[TrimmableTypeMap] Implement reflection-free TrimmableTypeMapValueManager and TrimmableTypeMapTypeManager#11617

Draft
simonrozsival wants to merge 47 commits into
mainfrom
dev/simonrozsival/java-interop-1441-android
Draft

[TrimmableTypeMap] Implement reflection-free TrimmableTypeMapValueManager and TrimmableTypeMapTypeManager#11617
simonrozsival wants to merge 47 commits into
mainfrom
dev/simonrozsival/java-interop-1441-android

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 9, 2026

Copy link
Copy Markdown
Member

Goal

Integrate the Java.Interop value-manager/type-manager split into dotnet/android while keeping the trimmable typemap runtime path reflection-free, AOT-friendly, and covered by targeted tests.

This PR is specifically about making the Android runtime use the new Java.Interop abstractions safely. Broader cleanup such as removing ManagedPeer support or changing NativeAOT runtime policy is intentionally left to follow-up PRs.

What changed

Java.Interop integration

  • Updated the Java.Interop submodule to the follow-up branch for [TrimmableTypeMap] Make adjustments to the base JniValueManager and JniTypeManager to make implementing the trimmable type map easier java-interop#1454.
  • The Java.Interop side now exposes only the minimal object-reference hook needed by JavaObjectArray<T>.SetElementAt():
    • CreateLocalObjectReferenceArgument(Type type, object? value) returns an owned local JniObjectReference for element assignment. Callers must dispose the returned reference.
  • JavaObjectArray<T>.Clear() now directly writes Java null into each slot; it no longer needs value-manager or value-marshaler state.
  • Reflection-backed Java.Interop code still uses value marshalers internally: it creates marshaler state, copies out an independent local reference, then destroys the state immediately.
  • The trimmable Android path does not implement or use GetValueMarshaler*() for JavaObjectArray<T> element assignment.

Runtime manager wiring

  • Switched legacy Mono/CoreCLR Android managers to the Java.Interop ReflectionJniTypeManager / ReflectionJniValueManager base implementations.
  • Added a CoreCLR Android value manager that preserves Android peer registration, GC bridge tracking, activation, and exception unboxing behavior.
  • Added trimmable typemap value/type managers for generated-typemap mode:
    • TrimmableTypeMapTypeManager resolves generated proxy/type-map data without reflection fallback.
    • TrimmableTypeMapValueManager constructs and marshals generated peers without the reflection-backed value manager.
    • Primitive arrays, Java arrays, peerables, proxies, strings, primitive wrappers, and general JavaConvert fallback cases are handled without reflection fallback.
  • Split the large JavaMarshalValueManager.cs implementation into per-type files.
  • Removed the old SimpleValueManager path.

Trimmable typemap build fixes

  • Preserved Microsoft.Android.Runtime.ManagedTypeMapping for linker/type-map steps that still need it.
  • Fixed post-trim Java source/stamp paths for multi-RID CoreCLR builds so RID-specific outputs do not collide or leave stale Java sources behind.
  • Kept [JniAddNativeMethodRegistrationAttribute] diagnostics focused on user assemblies; framework/runtime internal users are handled by generated replacements or intentionally unsupported runtime paths.
  • Removed the unproven RequiresUnreferencedCode annotation from ExportFieldAttribute. [ExportField] is handled by generated trimmable type-map code and does not need to warn simply for constructing the attribute.

Test strategy

  • Trimmable runtime tests exclude TrimmableTypeMapUnsupported via test categories.
  • ManagedPeer-dependent Java.Interop desktop fixture tests remain skipped by category in the trimmable Android lane. This PR does not add Android-local fixture workarounds for those tests; ManagedPeer removal/porting is left to follow-up work.
  • Reflection-manager-only Java.Interop tests remain covered by Java.Interop's standalone test suite; the Android trimmable lane uses generated managers.
  • Added/updated host-side tests for:
    • scanner XA4251 behavior,
    • trimmable runtimeconfig switches,
    • post-trim Java source generation,
    • [Export] / [ExportField] trimmable codegen,
    • trim-warning filtering for generated type-map code.

Review notes / intentional non-goals

  • NativeAOT runtime policy is not broadened in this PR; non-trimmable NativeAOT behavior is left unchanged for a separate PR.
  • ManagedPeer removal is not part of this PR. ManagedPeer-dependent tests are categorized unsupported rather than worked around locally.
  • Java.Interop value marshalers remain implementation detail for reflection-backed Java.Interop behavior; trimmable Android code uses generated managers and direct local object-reference creation.
  • The built-in TypeJniTypeSignature mapping intentionally keeps the current Type.GetTypeCode + explicit nullable typeof checks. Benchmarking showed Nullable.GetUnderlyingType() allocates and should not be used on this path.

Local validation

Validated locally on this branch with:

dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj \
  -p:Configuration=Debug \
  -m:1 \
  -nodeReuse:false \
  --no-restore \
  -v:minimal

dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj \
  -v minimal \
  --no-restore

Results:

  • Java.Interop build passed.
  • Trimmable typemap tests passed (562 passed).
  • dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal compiled Mono.Android.Runtime.dll; the remaining local failure is Android SDK provisioning (extras/android/m2repository.staging and docs.staging missing), not C# or trim-analyzer errors.

@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/java-interop-1441-android branch from 42a1b8e to 47450cf Compare June 10, 2026 06:17
@simonrozsival

Copy link
Copy Markdown
Member Author

Rebased onto #11622 (external/Java.Interop d7dbad5) and revalidated targeted Mono.Android build locally.\n\n/azp run

@simonrozsival simonrozsival changed the base branch from main to dependabot/submodules/external/Java.Interop-d7dbad5 June 10, 2026 07:11
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@simonrozsival simonrozsival changed the base branch from dependabot/submodules/external/Java.Interop-d7dbad5 to main June 11, 2026 10:15
dependabot Bot and others added 18 commits June 11, 2026 12:15
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to `d7dbad5`.
- [Commits](dotnet/java-interop@b881d21...d7dbad5)

---
updated-dependencies:
- dependency-name: external/Java.Interop
  dependency-version: d7dbad5e30a8f03743a508a95c4e9159fe1f6607
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Split the Android JavaMarshal value manager into CoreCLR and trimmable implementations that share peer registration and GC bridge integration through a reusable helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the trimmable typemap value manager on the abstract JniValueManager base, sharing only peer registration and GC bridge state with the CoreCLR value manager. Leave value marshaling unsupported for now until Android has trimmable-specific marshalers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move TrimmableTypeMapTypeManager off ReflectionJniTypeManager and implement type lookup through explicit built-in mappings plus the generated trimmable typemap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove newly added UnconditionalSuppressMessage attributes, propagate Requires annotations from reflection-backed managers, and carry DAM annotations through JavaPeerProxy/TrimmableTypeMap target type metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace newly added suppressions on reflection-backed managers with RequiresUnreferencedCode and RequiresDynamicCode propagation. Leave trimmable value/type managers free of UnconditionalSuppressMessage attributes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace remaining UnconditionalSuppressMessage attributes in the reflection-backed Android manager implementations with RequiresUnreferencedCode/RequiresDynamicCode where appropriate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace invoker lookup and legacy TypeManager peer creation suppressions with RequiresUnreferencedCode/RequiresDynamicCode propagation. Keep GetObject suppression because adding DAM there breaks delegate/reflection table use sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Annotate runtime feature switches with FeatureGuard and structure manager factory branches so reflection-backed manager creation is guarded by the relevant runtime feature instead of broad Requires annotations on the factory methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the single-use JavaMarshalReflectionValueManagerBase and keep the shared peer/GC bridge state in JavaMarshalPeerManager, directly delegated by the CoreCLR value manager.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make both TrimmableTypeMapTypeManager RegisterNativeMembers overloads throw UnreachableException directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make unused trimmable JniTypeManager paths fail loudly, remove ManagedPeer from trimmable runtime artifacts, and add an initial AOT-safe value-marshaling implementation for the trimmable value manager.

Update tests and trimmable runtime coverage to use feature switches via AppContext and enable the value-marshaling test bucket for follow-up triage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the Java.Interop proxy and peerable value marshalers from the trimmable value manager instead of duplicating peerable marshaling locally. This also updates the Java.Interop submodule to the follow-up branch with the shared proxy marshaler and re-enables the trimmable tests now covered by the shared marshalers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Handle Java peerable elements and Java primitive array wrappers without reflection in the trimmable value manager. This lets JavaObjectArray<JavaObject> preserve peer identity and JavaObjectArray<JavaInt32Array> create and read int-array elements correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Add Android trimmable RenameClass fixture variants without ManagedPeer.construct so JniPeerMembersTests.ReplacementTypeUsedForMethodLookup can exercise replacement-type method lookup on device.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Add Android trimmable CallNonvirtual fixture variants without ManagedPeer.construct so MethodBindingTests can validate virtual dispatch behavior under the trimmable typemap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Use the existing InnerException member directly in throwable unwrapping paths instead of adding a redundant Exception property.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Keep the PR focused by reverting NativeAOT manager wiring and ManagedTypeManager cleanup, removing Android-local ManagedPeer fixture replacements, and dropping ManagedPeer absence assertions. Unsupported ManagedPeer-dependent Java.Interop tests remain skipped by category.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@simonrozsival simonrozsival changed the title Use Java.Interop value manager split in Android [TrimmableTypeMap] Implement reflection-free TrimmableTypeMapValueManager and TrimmableTypeMapTypeManager Jun 12, 2026
Record the benchmark finding that the current Type.GetTypeCode plus explicit nullable checks path is zero-allocation, and avoid Nullable.GetUnderlyingType because it allocates.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

simonrozsival and others added 2 commits June 12, 2026 15:17
Move each top-level Java marshal value manager type into its own source file and route JavaObjectArray state creation through JniValueManager so the trimmable path no longer needs a value-marshaler implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove unused trimmable value-marshaler helper code and keep only the object-reference state path needed for JavaObjectArray element assignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Update the trimmable value manager to return standalone local JNI references for JavaObjectArray element assignment and remove the remaining marshaler-state cleanup path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Update the trimmable value manager override to match the CreateLocalObjectReferenceArgument naming from Java.Interop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant