Skip to content

[Xamarin.Android.Build.Tasks] Wire opt-in R8 runtime remapping - #12692

Merged
simonrozsival merged 25 commits into
mainfrom
simonrozsival-fix-r8-obfuscation-policy
Oct 9, 2026
Merged

simonrozsival merged 25 commits into
mainfrom
simonrozsival-fix-r8-obfuscation-policy

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Sep 5, 2026 •

Copy link
Copy Markdown
Member

Context: #12535

This is the product policy and build-orchestration layer for opt-in R8 runtime JNI remapping. It is stacked on:

A final dependent PR, #12844, moves the .NET-only R8 mapping task into Microsoft.Android.Build.Tasks.

This PR does not rewrite managed assemblies. Managed bindings retain their original JNI names; the lower layers generate native tables that translate lookups to the names and descriptors emitted by R8.

Opt-in

For a trimmed CoreCLR or NativeAOT application:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <AndroidLinkTool>r8</AndroidLinkTool>
  <AndroidTypeMapImplementation>trimmable</AndroidTypeMapImplementation>
  <PublishTrimmed>true</PublishTrimmed>
  <AndroidR8ObfuscationMode>runtime-remapping</AndroidR8ObfuscationMode>
</PropertyGroup>

runtime-remapping is the sole opt-in; there is no separate enable property. It does not apply to library projects. The existing private-members and disabled behavior remains unchanged. Unknown mode values report XA1050, while incompatible runtime-remapping configurations report XA4329.

Build orchestration

  • Runs R8 once, after ILLink or all per-RID NativeAOT ILC compilations.
  • Converts that final mapping into runtime remapping tables before native linking.
  • Defers NativeAOT linking until the shared R8 pass completes, then relinks each RID without rerunning ILC.
  • Applies the runtime-remapping-specific bootstrap, native-callback, manifest, resource, and JNI safety keep rules.
  • Preserves existing MAM remapping precedence and asset handling.
  • Tracks mapping inputs, generated XML, native table sources, task assemblies, native-link inputs, configuration changes, missing-output recovery, and opt-out for incremental builds.
  • Preserves generated ProGuard rule timestamps when their contents are unchanged so managed-only rebuilds do not rerun R8 unnecessarily.

Coverage in this layer

Host tests cover mode defaults, configuration validation, keep-rule policy, NativeAOT ProGuard configuration, packaging metadata, and incremental behavior. Build/device integration tests cover renamed types and members, constructors, overloads, inherited lookups, peer activation, single-pass ordering, multi-RID builds, missing-output recovery, and opt-out for CoreCLR and NativeAOT.

The generic lookup semantics and mapping/table-generation tests live in #12847 and #12848 respectively.

Experimental limitations

NativeAOT literal matching is conservative and can retain extra entries. Arbitrarily computed JNI names may require explicit remaps or keep rules. Conservative public/nested-class, interface, bootstrap, and native-callback keeps limit obfuscation. Existing Intune/R8 conflict handling is not full remapping-chain composition, and ambiguous reverse mappings for merged classes are omitted. This remains an experimental opt-in, not a production-readiness claim.


  • Useful description of why the change is necessary.
  • Links to related issues and dependent layers.
  • Unit and integration coverage.

Fixes: #12535

Copilot AI lite review requested due to automatic review settings September 5, 2026 19:02
@simonrozsival
simonrozsival marked this pull request as draft September 5, 2026 19:02

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.

Copilot review overview

🔵 Needs a closer look

It makes cross-cutting changes across MSBuild targets, build tasks, native runtime lookup code, and public API surface that require careful human validation beyond automated review.

Review tier: Lite
Findings: 3 Low severity

New issues introduced by this change (3)
Severity Finding
Low severity src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​GenerateJniRemappingNativeCodeTests.cs — ❌ error: Avoid the null-forgiving operator (!) in tests as well; it hides real nullability issues…
Low severity src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​R8Tests.cs — ⚠️ warning: Two [TestCase] attributes are on the same line, which makes the test metadata harder to…
Low severity src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​R8Mapping.cs — ⚠️ warning: Two method declarations are on the same line, which breaks the repo’s typical…
What changed in this PR

Adds an experimental opt-in for R8 obfuscation in .NET for Android by generating and consuming runtime JNI remapping tables (rather than rewriting managed assemblies), enabling obfuscated DEX outputs while preserving managed JNI name expectations.

Changes:

  • Introduces a two-pass R8 pipeline (seed mapping pre-trim + final R8 -applymapping) and generates JNI remapping XML/native tables for CoreCLR and NativeAOT.
  • Extends runtime remapping to cover reverse type lookups, rewritten method descriptors, and field remapping; shares native lookup code between CoreCLR and NativeAOT.
  • Adds/updates tests and documentation for new public properties and XA4327/8/9 diagnostics.
File Description
tests/​MSBuildDeviceIntegration/​Tests/​R8RuntimeRemappingTests.cs Device test validating obfuscated members/types and incremental/missing-output recovery.
src/​Xamarin.Android.Build.Tasks/​Xamarin.Android.D8.targets Wires new R8 inputs/outputs and enables mapping input/output + obfuscation flag.
src/​Xamarin.Android.Build.Tasks/​Xamarin.Android.Common.targets Adds opt-in properties, validation (XA4329), incremental inputs, and AAPT rules tracking changes.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​R8Mapping.cs Extends mapping parsing/projection for class/method/field data used by remapping generation.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​NativeAotJniRetention.cs NativeAOT ELF-based literal retention to conservatively select required remap entries.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​JniDescriptorText.cs Converts Java source-form types to JNI tokens + builds method descriptors.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​JniAssemblyRewriter.cs Adds scan-only entrypoint for linked-assembly analysis (no rewriting).
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​R8Tests.cs Adds unit coverage for keep-option and config generation behavior.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​GenerateTrimmableTypeMapTests.cs Verifies NativeAOT proguard generation respects allowobfuscation.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​GenerateJniRemappingNativeCodeTests.cs New tests for native remap table emission, ordering, and legacy compatibility.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​InvalidConfigTests.cs Tests defaults + invalid configuration errors for new MSBuild properties.
src/​Xamarin.Android.Build.Tasks/​Tasks/​R8.cs Adds seed mapping mode, applymapping support, and conditional dontobfuscate removal.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateR8JniRemapping.cs Generates JNI remapping XML from R8 mapping + existing remaps; supports NativeAOT retention path.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateR8JniManifestProguardConfiguration.cs Generates manifest keep rules to stabilize seed mapping applicability.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateProguardConfiguration.cs Emits allowobfuscation on keep rules when runtime remapping is enabled.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateNativeAotProguardConfiguration.cs Emits allowobfuscation for NativeAOT-generated keep rules when enabled.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateJniRemappingNativeCode.cs Extends generated tables to include reverse types + fields; exposes info for tests.
src/​Xamarin.Android.Build.Tasks/​Resources/​proguard_xamarin.cfg Adds/adjusts keep rules needed for stable seed/final graphs and bootstrap types.
src/​Xamarin.Android.Build.Tasks/​Resources/​proguard_trimmable_nativeaot.cfg Aligns NativeAOT baseline keep rules with remapping needs and seed/final stability.
src/​Xamarin.Android.Build.Tasks/​Properties/​Resources.resx Adds XA4327/8/9 localized strings for errors/warnings/validation.
src/​Xamarin.Android.Build.Tasks/​Properties/​Resources.Designer.cs Updates generated resource accessors for XA4327/8/9.
src/​Xamarin.Android.Build.Tasks/​MSBuild/​Xamarin/​Android/​Xamarin.Android.Aapt2.targets Moves AAPT proguard rule tracking to incremental parent target.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.Trimmable.targets Imports new R8 JNI remapping targets last to override pre-trim outputs as needed.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets Includes new properties in incremental stamps; passes obfuscation state into proguard generation.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets Reworks linked-assembly proguard inputs; adds remapping-assembly prep + incremental inputs.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.LlvmIr.targets Adds remapping enable flag into proguard generation and incremental inputs.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.R8JniRemapping.targets New MSBuild pipeline for seed mapping, remap XML, and NativeAOT late-linked table build.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.NativeAOT.targets Links remapping object into NativeAOT shared library and updates incremental inputs.
src/​native/​nativeaot/​include/​runtime-base/​internal-pinvokes.hh Adds internal pinvokes for reverse-type and field lookup.
src/​native/​nativeaot/​host/​jni-remapping-tables-stub.cc Provides weak empty table symbols for apps without remapping objects.
src/​native/​nativeaot/​host/​internal-pinvoke-stubs.cc Removes now-shared remapping pinvoke stubs from NativeAOT host stubs.
src/​native/​nativeaot/​host/​host.cc Plumbs jniRemappingInUse via shared JniRemapping::is_in_use().
src/​native/​nativeaot/​host/​CMakeLists.txt Adds shared remapping sources and stub table compilation to NativeAOT host build.
src/​native/​native.targets Includes shared remapping sources/headers in NativeAOT flavor build inputs.
src/​native/​mono/​xamarin-app-stub/​xamarin-app.hh Updates stub ABI structs to include target_signature + field remapping structures.
src/​native/​mono/​xamarin-app-stub/​application_dso_stub.cc Updates stub table initializers for new method signature field.
src/​native/​mono/​runtime-base/​internal-pinvokes.hh Adds internal pinvoke declarations for reverse-type and field lookup.
src/​native/​mono/​pinvoke-override/​pinvoke-tables.include Extends pinvoke table entries/count for the new remapping exports.
src/​native/​mono/​pinvoke-override/​generate-pinvoke-tables.cc Adds new internal pinvoke names to generator input list.
src/​native/​mono/​monodroid/​internal-pinvokes.cc Adds MonoVM-safe placeholder exports for new remapping entrypoints.
src/​native/​clr/​xamarin-app-stub/​application_dso_stub.cc Extends CLR stub tables to include reverse types + fields + signature pinning.
src/​native/​clr/​runtime-base/​jni-remapping.cc Implements binary-search remapping lookups (types, reverse types, methods, fields) + is_in_use().
src/​native/​clr/​pinvoke-override/​precompiled.cc Maps new internal pinvoke entrypoints to implementations.
src/​native/​clr/​include/​xamarin-app.hh Declares remapping table symbols and adds field + reverse type structures.
src/​native/​clr/​include/​runtime-base/​jni-remapping.hh Declares shared lookup surface including reverse type and field lookup.
src/​native/​clr/​include/​runtime-base/​internal-pinvokes.hh Declares new remapping pinvokes for CoreCLR runtime.
src/​native/​clr/​host/​internal-pinvokes-shared.cc Centralizes shared remapping pinvoke implementations for CoreCLR/NativeAOT.
src/​native/​clr/​host/​internal-pinvokes-clr.cc Removes remapping implementations now provided by shared file.
src/​native/​clr/​host/​host.cc Uses JniRemapping::is_in_use() for init flag and includes remapping header.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMapValueManager.cs Ensures FindClass uses replacement type name when remapping is enabled.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMapTypeManager.cs Adds reverse-type handling for Java-to-managed lookups; uses replacement type for signatures.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMap.cs Uses reverse type for proxy lookup and replacement type for FindClass checks.
src/​Mono.Android/​Microsoft.Android.Runtime/​JniRemappingLookup.cs Adds reverse type + field lookup plumbing and supports target-method-signature.
src/​Mono.Android/​Android.Runtime/​RuntimeNativeMethods.cs Adds LibraryImport declarations for reverse type + field lookup pinvokes.
src/​Mono.Android/​Android.Runtime/​AndroidRuntime.cs Exposes GetOriginalTypeCore via reverse-type lookup.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop/​JniPeerMembersTests.cs Adds tests validating remapped field names and pinned target signatures.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop/​JavaVMFixture.cs Extends test runtime type manager with field replacement support.
external/​Java.Interop/​src/​Java.Interop/​PublicAPI.Unshipped.txt Records new public API surface additions for replacement fields + original type.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniType.cs Adds TryGet{Static,Instance}Field helpers to support remapped field probing.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniRuntime.ReflectionJniTypeManager.cs Adds null default implementation for field replacement in reflection manager.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniRuntime.JniTypeManager.cs Adds ReplacementFieldInfo + original type + replacement field APIs.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniStaticMethods.cs Uses replacement lookup keyed by original type name (compat + remapping).
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniStaticFields.cs Adds remapped static field probing and fallback to original lookup.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniInstanceMethods.cs Tracks original vs effective JNI type names; remaps ctor/method lookup accordingly.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniInstanceFields.cs Adds remapped instance field probing and fallback behavior.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.cs Tracks original type name and performs replacement lookups across base types/types.
Documentation/​docs-mobile/​TOC.yml Adds XA4327/8/9 docs to TOC.
Documentation/​docs-mobile/​messages/​xa4329.md Documents invalid/unsupported configuration errors and resolutions.
Documentation/​docs-mobile/​messages/​xa4328.md Documents remapping incompleteness warnings (conflicts/signature conversion).
Documentation/​docs-mobile/​messages/​xa4327.md Documents remapping generation failures and troubleshooting steps.
Documentation/​docs-mobile/​messages/​index.md Adds XA4327/8/9 to messages index.
Documentation/​docs-mobile/​building-apps/​build-properties.md Documents AndroidEnableR8Obfuscation + AndroidR8ObfuscationMode properties.
Files not reviewed (1)
  • src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs: Generated file
Suppressed comments (1)

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs:112

  • ❌ error: This uses the null-forgiving operator (info!) after Assert.IsNotNull, which the compiler can’t reason about. Prefer ?? throw so nullability is enforced without suppressions.

Comment thread src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs Outdated
simonrozsival added a commit that referenced this pull request Sep 6, 2026
Context: #12692

Follow the existing Intune contract: member lookup keys contain the
replaced owner type and the original managed member name and descriptor.
Generate R8 member entries and collision keys in that same form rather
than teaching every consumer to try both original and replaced owners.

Remove JniPeerOriginalTypeName, the duplicate constructor identity, and
the now-unused Java.Interop reverse-type hook.  Keep reverse lookup for
peer activation and derive desugared companion names in the Android
remapping helper where the original name is actually needed.

Cover residual-owner keys, descriptors, MAM conflicts and duplicates, and
real renamed-class construction and member access on both runtimes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review September 8, 2026 13:34
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

❌ Needs Changes — 1 error found.

The remapping pipeline is well covered and the runtime/native lookup design is coherent, but field-entry conflict detection currently omits the JNI descriptor from the key. Under R8 class merging this can discard a valid field remap and cause runtime lookup failure. CI has no observed failures, with the latest build checks still in progress.

Generated by Android PR Reviewer for #12692 · gpt56 · 770.9 AIC · ⌖ 5.5 AIC · ⊞ 26.3K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Tasks/Tasks/GenerateR8JniRemapping.cs Outdated
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

⚠️ Needs Changes

Found 2 errors in JNI field remapping: inherited remaps can override hidden derived fields, and the newly enabled field fixtures run on MonoVM even though its field-remapping entry point is a stub. The overall cross-runtime design and focused coverage are substantial, but these correctness gaps should be addressed before merge.

CI has not run for the latest commit: the dotnet-android check is currently action_required (awaiting pipeline authorization); CLA passes.

Generated by Android PR Reviewer for #12692 · gpt56 · 1.1K AIC · ⌖ 18.7 AIC · ⊞ 25.7K
Comment /review to run again

Comment thread external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs Outdated
Comment thread tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml Outdated
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

❌ Needs changes

Found 4 errors in the new runtime-remapping paths:

  • Member lookup can select a renamed base member before an unchanged hiding member on the current type.
  • Java-to-managed lookup can resolve a residual-name/original-name collision to the wrong managed type.
  • MonoVM wildcard and parameter-only remaps can shadow more-specific descriptors because of generated sort order.
  • Generated LLVM member-array symbols can collide or become invalid for Unicode JNI names.

The overall single-pass R8 pipeline is thoughtfully integrated across CoreCLR and NativeAOT, with substantial build, unit, and device coverage. All 44 current CI checks completed successfully, but the cases above require adversarial regression coverage before merge.

Generated by Android PR Reviewer for #12692 · copilot · gpt56 · 1.8K AIC · ⌖ 11.5 AIC · ⊞ 21K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs Outdated
Comment thread src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs Outdated
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

⚠️ Needs Changes — 0 errors, 1 warning, 0 suggestions.

The runtime-remapping implementation has broad coverage across Java.Interop, CoreCLR, NativeAOT, MSBuild incrementality, and device/build tests. One MSBuild ordering issue remains: the post-R8 NativeAOT link can run after _CompileToDalvik fails and may consume stale output or obscure the primary failure.

CI is still in progress: the completed checks are passing, with no failures currently reported.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692 · copilot · gpt56 · 738.1 AIC · ⌖ 18.2 AIC · ⊞ 21K
Comment /review to run again

simonrozsival added a commit that referenced this pull request Sep 18, 2026
## Motivation

The existing Intune/MAM JNI method-remapping path stores target type and method names as stable NUL-terminated UTF-8 strings in generated native data. The old path materialized those names as managed UTF-16 strings and then encoded them back to UTF-8 for `FindClass` and `GetMethodID`.

That round trip is unnecessary and becomes more important as remapping is reused by larger consumers such as R8.

## Approach

- Add pointer-backed target type, method-name, and method-signature values to `ReplacementMethodInfo`.
- Keep pointer and string representations independent so reading a compatibility string property does not implicitly decode native memory.
- Add `JniType` lookup paths for pointer/pointer and mixed pointer/span member names and signatures.
- Let `JniPeerMembers` retain a replacement type pointer and use it directly for `FindClass` and later member-remapping lookups.
- Keep native pointers in `JniMethodInfo` Debug metadata and decode them only if `Name`, `Signature`, or `ToString()` is explicitly requested.
- Retain the existing string/span paths for custom `JniTypeManager` implementations.
- Document UTF-8 encoding, NUL termination, ownership, and lifetime requirements for every pointer API.

The successful generated-remapping path therefore passes the pregenerated UTF-8 type, method name, and optional signature directly to JNI without copying them or converting them to a managed string.

This PR is method-only and does not add R8, field remapping, reverse-type mapping, inherited-member fallback, or NativeAOT remapping support.

## Relationship to other PRs

- This PR is based directly on `main` and provides the Java.Interop representation and JNI lookup primitives.
- #12796 supplies the generated pointers, performs managed table search, and removes the native remapping P/Invokes.
- #12692 can build on this stack to add the R8-specific field, reverse-type, inherited-member, and NativeAOT pieces.

## Validation

- Java.Interop Debug build.
- Java.Interop `JniPeerMembersTests`: 12 passed, 1 skipped.
- The JVM fixture exercises stable unmanaged UTF-8 type/name/signature storage, signature fallback, instance/static lookup, and instance-to-static remapping.
@simonrozsival simonrozsival changed the title [Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping [Microsoft.Android.Build.Tasks] Add opt-in R8 runtime remapping Sep 21, 2026
@simonrozsival simonrozsival changed the title [Microsoft.Android.Build.Tasks] Add opt-in R8 runtime remapping [Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping Sep 21, 2026
@simonrozsival
simonrozsival added this pull request to stack #12845 September 21, 2026 12:41
simonrozsival and others added 3 commits October 7, 2026 16:50
Integrate the typemap selector removal while retaining runtime-remapping validation and OS-native generated ProGuard path construction.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Integrate the typemap selector removal while retaining runtime-remapping validation and OS-native generated ProGuard path construction. Compute the clean directory before evaluating Exists so the condition remains valid MSBuild syntax.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve expected and actual configuration paths with System.IO.Path before comparing them so equivalent mixed Windows separators do not fail the target test.

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

Copy link
Copy Markdown
Member Author

@dalexsoto review

@dalexsoto dalexsoto left a comment

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.

Completed the full current 32-file correctness, integration/completeness, candidate-ledger and final additional-blocker passes. The resolved lookup/codegen concerns, accepted post-R8 target ordering and explicit-primary-RID coverage do not leave additional independent blockers.

Preserve live field remaps for JavaInterop1 bindings. The class-only TypeMap retention change removes the fallback that retained field identities, while ScanFieldLikeMember still recognizes only RegisterAttribute. The supported AndroidCodegenTarget=JavaInterop1 emitter deliberately omits that attribute on generated field properties; its accessors instead use constant JNI identifiers such as value.I.

A concrete supported case is a JavaInterop1 binding for public example.PurePeer.value, consumed by a trimmed Release CoreCLR application with runtime-remapping and an empty custom ProGuard file, as used by the existing device fixture. The binding option is distinct from _AndroidJcwCodegenTarget, which stays at its supported default. The active primary rule retains PurePeer and its fields with allowobfuscation; no name-preserving rule matches this field in that configuration. This is not a blanket claim about every default AGP policy.

If R8 renames the field, the head scanner records the class but no field key, so GenerateR8JniRemapping drops the replacement. Java.Interop then probes the original value:I and cannot resolve the renamed field. The base scanner/task contract retained the field key through RecordAllMappings; no complete base application run is claimed. These are live generated constants, not the accepted arbitrary-computed-JNI-name limitation.

Please provide and consume field-specific retention metadata for this emitter, or preserve these field names until supported, and add generated JavaInterop1 instance/static-field coverage. This need not restore all unused member mappings. Evidence is immutable producer/consumer source tracing and a source-derived model; I did not execute the binding compiler, R8 or a JNI/device reproduction, and a is only an illustrative permitted residual name.

JavaInterop1 binding field accessors use constant JNI field identifiers but do not carry RegisterAttribute metadata. Preserve field names for runtime-remapping builds using this emitter while leaving class and method renaming enabled.

Cover generated instance and static field access in the runtime fixture and retain the existing manually registered field-remapping assertions.

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

Copy link
Copy Markdown
Member Author

Addressed Alex’s JavaInterop1 field-remapping blocker in f4fee64e3f.

AndroidCodegenTarget=JavaInterop1 runtime-remapping builds now emit -keepclassmembernames class * { <fields>; }, because generated field accessors use constant JNI identifiers without field-specific attributes. The rule is limited to that emitter/mode; classes and methods remain renameable, and manually registered fields continue through the remapping table.

The existing CoreCLR/NativeAOT device fixture now selects JavaInterop1 and covers both generated instance/static fields while retaining the manually registered hidden-field remapping assertions.

Validation:

  • R8Tests: 32 passed
  • MSBuildDeviceIntegration.csproj Release build: succeeded, 0 errors (38 existing generated-code warnings)
  • git diff --check: passed

A private API 35 arm64 runtime attempt was blocked before app build because this worktree’s local workload registration is incomplete after the unavailable pinned-pack bootstrap failure (NETSDK1147); no device product failure occurred.

@dalexsoto dalexsoto left a comment

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.

Re-reviewed the complete 32-file change at f4fee64. Preserving field names is an acceptable conservative fix; I am not requesting maximal field obfuscation or reintroducing the rejected MSBuildLastTaskResult guard. Four concrete blockers remain:

  1. The original mixed-emitter field-retention case is still uncovered. R8.cs:167-168 gates preservation on the consuming application's AndroidCodegenTarget, forwarded by Xamarin.Android.D8.targets:86. The original supported case was a JavaInterop1 binding library consumed by a default-XA, trimmed CoreCLR application, not an application globally switched to JavaInterop1. That application still emits no field-name-preserving rule. With the reachable empty custom R8 configuration, the generated library field can be obfuscated while the metadata-only scanner omits its F key and runtime lookup falls back to its original name/signature. This is the same previous blocker, not a new objection to preservation. Cover the actual consumed binding emitters, or preserve fields conservatively for runtime-remapping; if the decision stays conditional, include its effective inputs in the production incremental cache.

  2. The new JavaInterop1 fixture currently generates uncompilable C#. The emitter switch at R8RuntimeRemappingTests.cs:80 activates a preexisting reference-importer defect. In current-source build 1627672, both ObfuscatedMembersRun(CoreCLR) and ObfuscatedMembersRun(NativeAOT) fail CoreCompile in generated Example.RuntimePeer.cs, with six errors each: CS1056, CS1519, CS1002, and CS1525 involving backticks and the following ref expression. Neither reaches installation or runtime assertions.

    The producer path is JavaArray<T>/JavaObjectArray<T> declaring JniTypeSignature("java/lang/Object", ArrayRank=1), while external/Java.Interop/tools/generator/Java.Interop.Tools.Generator.Importers/CecilApiImporter.cs:118-143,253-265 ignores the rank. SymbolTable.cs:153-181,271-325 then permits those array wrappers to shadow scalar java.lang.Object; their CLR generic-arity names pass through the base and CreateHidden writers. The source-derived JavaObjectArray shape matches all reported backtick/ref columns, but I have not retrieved the generated C# or inspected the published reference DLL metadata. Correct the ranked-versus-scalar registration, preserving the proper Object peer, rather than merely stripping the backtick. The importer code itself is unchanged from base/788; this revision's fixture selection exposes it.

  3. The handwritten binding must also be adapted to the JavaInterop1 base API. Independently of those syntax errors, HiddenPeerBinding's base(handle, transfer) call at line 106 still expects the XA IntPtr, JniHandleOwnership constructor. The selected generator's SourceWriters/JavaLangObjectConstructor.cs:23-32 emits ref JniObjectReference, JniObjectReferenceOptions instead. After restoring canonical Java.Interop.JavaObject selection, the old ThresholdClass/ThresholdType overrides are also absent from that base API. Adapt or bridge the handwritten activation path and overrides while preserving constructor rooting. A successful integration-test-project build compiles the embedded fixture strings, not this generated application. This is source-proven and currently masked by item 2; the CI logs do not show observed CS1503/CS1620 constructor diagnostics.

  4. The hidden-field mapping assertion contradicts the active keep policy. R8RuntimeRemappingTests.cs:178-180 requires hiddenValue's target name to differ from its source name. The new -keepclassmembernames class * { <fields>; } rule in R8.cs:287-288 also preserves manually registered fields, including hiddenValue. Owner-only remapping can legitimately remain while the field name stays unchanged, as supported by GenerateR8JniRemapping.cs:254-273. Update the fixture to check that valid remapping/lookup contract rather than demand field-name obfuscation that its own policy forbids. Fixing either compilation issue does not fix this assertion.

The review used immutable source tracing and source-derived models, plus the published current-head CI build logs; no local SDK/R8/JNI/device execution is claimed. The actual CI failure is generated-code syntax, not the author's separate local NETSDK1147 workload/bootstrap limitation, and it does not establish an observed runtime field or assertion failure.

JavaInterop1 binding libraries can be consumed by applications using a different code generator, so the consuming application property cannot identify every constant JNI field lookup.

Preserve field names conservatively for all runtime-remapping builds. Keep class and method renaming enabled, and validate field remapping through renamed owners with stable field names.

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

Copy link
Copy Markdown
Member Author

Addressed all four blockers from the f4fee64 review in 8d94599f68.

  • Field-name preservation is now unconditional for runtime-remapping: -keepclassmembernames class * { <fields>; }. This covers JavaInterop1 binding libraries consumed by default-XA applications and removes the emitter property/cache dependency.
  • Removed the global AndroidCodegenTarget=JavaInterop1 fixture switch, so the test no longer enters the unrelated ranked-type importer defect or requires JavaInterop1-specific handwritten base constructors/overrides.
  • The fixture remains on the default supported app emitter and exercises generated instance/static fields.
  • Field XML assertions now require stable target field names while verifying lookup through genuinely renamed owners, including the handwritten hiddenValue binding.

Validation:

  • R8Tests: 31 passed, 0 failed
  • Release MSBuildDeviceIntegration.csproj build: succeeded, 0 errors (37 existing generated-code warnings)
  • git diff --check: passed

The prior current-head CI generated-code failure path has been removed rather than patched around.

simonrozsival and others added 2 commits October 8, 2026 17:08
Fields whose names and signatures are preserved do not need replace-field entries. The renamed owner is handled by the type mapping and the runtime assertions exercise the generated field accessors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A field whose name and signature are unchanged does not require a replace-field entry even when its owner is renamed. The replace-type entry handles owner translation and the device execution verifies the actual lookup.

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

Copy link
Copy Markdown
Member Author

@dalexsoto review

@dalexsoto

Copy link
Copy Markdown
Member

@simonrozsival I haven't submitted a new GitHub review for this context: the head changed from 92984b8 to de57d2f while the complete review was in progress. I will not submit a decision against the superseded revision; the new head remains queued for a fresh current-revision review.

@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto review

@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto stamp

1 similar comment
@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto stamp

@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

⚠️ Needs Changes — 0 errors, 3 warnings.

The runtime-remapping orchestration is comprehensive and the production changes are well covered across CoreCLR and NativeAOT. I found one XA4329 documentation mismatch and two integration-test gaps where the assertions can pass without proving the stated ordering or table-update behavior. All 42 reported GitHub/Azure checks are green.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692 · copilot · gpt56 · 561.9 AIC · ⌖ 11.3 AIC · ⊞ 26.4K
Comment /review to run again

Comment thread Documentation/docs-mobile/messages/xa4329.md Outdated
Comment thread tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs Outdated
Comment thread tests/MSBuildDeviceIntegration/Tests/R8RuntimeRemappingTests.cs Outdated
Document the actual PublishTrimmed validation for XA4329, assert runtime-specific trim and native-link nodes exist before checking R8 ordering, and prove a changed rule removes the int-overload remapping entry while leaving the other overload mapped.

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

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692

@github-actions github-actions Bot 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.

✅ No additional findings on the current head.

I reviewed the complete 32-file change and the latest fixes at 925d390. The XA4329 documentation now matches the actual validation, the ordering test proves the runtime-specific trim and native-link nodes exist, and the changed-rule test verifies that the late-linked remapping table content changes. The previously raised JNI lookup, field-retention, code-generation, orchestration, and incrementality concerns are addressed or resolved in the current implementation.

CI is still in progress: CLA and both Android Tools lanes pass; the Linux, macOS, Windows, and aggregate Azure DevOps checks remain running/queued, so this review does not claim a green CI result.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • azcliprod.blob.core.windows.net

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "azcliprod.blob.core.windows.net"

See Network Configuration for more information.

Generated by Android PR Reviewer for #12692 · copilot · gpt56 · 170.5 AIC · ⌖ 11.2 AIC · ⊞ 26.4K
Comment /review to run again

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Oct 9, 2026
@simonrozsival
simonrozsival merged commit 26a426d into main Oct 9, 2026
42 checks passed
@simonrozsival
simonrozsival deleted the simonrozsival-fix-r8-obfuscation-policy branch October 9, 2026 11:52
simonrozsival added a commit that referenced this pull request Oct 10, 2026
…mbly (#12844)

## Summary

This PR is stacked on and depends on #12692. That parent is built on the focused lower chain #12846, #12847, and #12848.

This final layer is only a task-assembly boundary change for the runtime-R8 remapping producer. It moves `GenerateR8JniRemapping` and the .NET-only helpers it directly requires into `Microsoft.Android.Build.Tasks`:

- `JniRemappingAssemblyScanner`
- `R8Mapping`
- `JniDescriptorText`
- `NativeAotJniRetention`

The generic runtime table producer remains in `Xamarin.Android.Build.Tasks`: `GenerateJniRemappingNativeCode` and `JniRemappingNativeCodeGenerator` are unchanged and continue to serve both MAM and R8 remapping.

The relocation also:

- loads only `GenerateR8JniRemapping` from the modern assembly using the existing Full/Core `UsingTask` pattern
- tracks both modern and legacy task assemblies in mixed incremental targets
- links only the metadata helpers required by the read-only scanner
- packages `ELFSharp.dll` beside the modern task assembly under `tools/net`
- keeps net10 `MSBuildDeviceIntegration` compatible by linking `R8Mapping.cs` instead of referencing the net11 task project
- preserves a test-only aliased legacy task reference to validate generated XML through `MergeRemapXml` and `GenerateJniRemappingNativeCode`

This PR does not restore or relocate the removed managed assembly rewriter, rewrite planners/rebuilders, rewrite-only tests, or `XA4325`/`XA4326` behavior.

## Validation

- `Microsoft.Android.Build.Tasks` build
- `Xamarin.Android.Build.Tasks` build
- 86 focused modern R8 remapping tests
- 41 trimmable typemap integration tests
- `MSBuildDeviceIntegration` project build
- verified packaged `tools/net/ELFSharp.dll`
- final read-only review found no blocking issues
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

r8-runtime-rewriting ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

R8: unconditional -dontobfuscate makes Google Play's Feb 2027 25% obfuscation requirement unachievable

4 participants