Skip to content

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

Draft
simonrozsival wants to merge 2 commits into
mainfrom
simonrozsival-fix-r8-obfuscation-policy
Draft

[Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping#12692
simonrozsival wants to merge 2 commits into
mainfrom
simonrozsival-fix-r8-obfuscation-policy

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Context: #12535

The SDK disables R8 obfuscation because managed JNI callers retain the
original Java names. This change adds an experimental alternative to
assembly rewriting: use R8's mapping to translate JNI lookups through the
existing runtime remapping machinery, keeping managed assemblies unchanged.

This is an alternative to the rewriting pipelines in #12632 and #12634,
not an integration of those pipelines.

Build and runtime changes

  • Run a naming-only seed R8 pass before ILLink/ILC, then apply that mapping
    during final R8.
  • Select CoreCLR mappings from linked assemblies. For NativeAOT, filter
    against surviving JNI literals in allocated sections of ILC's ELF object,
    generate a native table object, and link it after ILC. Do not treat
    NativeAOT's original publish assemblies as trimmed output.
  • Extend runtime lookups for forward/reverse types, method descriptors,
    fields, inherited members, and managed-peer activation. Share the native
    lookup implementation between CoreCLR and NativeAOT; weak empty tables
    preserve the disabled NativeAOT path.
  • Preserve JNI bootstrap names and manifest/resource keep rules. Track
    AAPT rules in their incremental parent target so no-op builds cannot
    delete rules needed by resource-inflated classes.
  • Track generated tables, native link inputs, and public configuration
    changes, including missing-output recovery and switching obfuscation off.

Public opt-in

For a trimmed CoreCLR or NativeAOT application:

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

$(AndroidEnableR8Obfuscation) defaults to false.
$(AndroidR8ObfuscationMode) defaults to runtime-remapping; selecting a
mode alone does not enable obfuscation. Library projects are unaffected.

experimental-rewriting is reserved for the separate rewriting workstream
and currently reports XA4329 rather than silently falling back. Invalid
values and incompatible enabled configurations also produce a localized
configuration error. MonoVM is not supported by this opt-in.

Validation

  • 88 focused R8/remapping host cases passed.
  • 15 public-option configuration cases passed.
  • CoreCLR and NativeAOT emulator cases passed with public options enabled,
    then disabled on the same project. Coverage includes actually renamed
    classes, constructors, overloads, instance/static fields, peer activation,
    no-op builds, missing-table recovery without ILC, and AAPT rule recovery.
  • NativeAOT MAUI sample-content baseline and remapped APKs exercised project
    and task navigation, keyboard editing, save/reopen, and Manage Meta on an
    arm64 API-35 emulator. Both used identical sample sources with local
    InputMethodManager constructor preservation and XamlC for NativeAOT.

The earlier MAUI NativeAOT snapshot produced a 314,376-byte remapping object
instead of retaining the full seed map. Those APKs used the prototype's
former private switch; the public API was subsequently covered by the
configuration/device cases above. No new startup-performance claim is
made by this PR.

Experimental limitations

NativeAOT literal matching is conservative and can retain extra entries.
Arbitrarily computed JNI names need explicit remaps or keep rules.
Conservative class/interface/native-callback keeps still limit obfuscation,
and existing Intune/R8 conflict handling is not full remapping-chain
composition. This is not a production-readiness claim.


  • Useful description of why the change is necessary.
  • Links to related issues.
  • Unit tests.

Fixes: #12535

Context: #12535
R8 obfuscation changes JNI names that managed bindings still use. Add an
experimental alternative to assembly rewriting by translating those names
through the existing runtime remapping machinery.
Run a naming-only R8 seed before ILLink or ILC and apply its mapping during
final R8. Select CoreCLR remaps from linked assemblies and NativeAOT
remaps from retained ELF literals, then link the NativeAOT table after
ILC. Extend lookups for reverse types, descriptors, fields, and peers.
Expose AndroidEnableR8Obfuscation, defaulting to false, and
AndroidR8ObfuscationMode, defaulting to runtime-remapping. Reserve
experimental-rewriting with a clear error until its separate pipeline is
available. Diagnose incompatible configurations with XA4329.
Preserve JNI bootstrap and resource keep rules, track incremental table
and native-link inputs, and support switching obfuscation back off.
Include task, configuration, device regression tests, and documentation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CopilotAI 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

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.

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)
SeverityFinding
Low severitysrc/​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 severitysrc/​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 severitysrc/​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.
FileDescription
tests/​MSBuildDeviceIntegration/​Tests/​R8RuntimeRemappingTests.csDevice test validating obfuscated members/types and incremental/missing-output recovery.
src/​Xamarin.Android.Build.Tasks/​Xamarin.Android.D8.targetsWires new R8 inputs/outputs and enables mapping input/output + obfuscation flag.
src/​Xamarin.Android.Build.Tasks/​Xamarin.Android.Common.targetsAdds opt-in properties, validation (XA4329), incremental inputs, and AAPT rules tracking changes.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​R8Mapping.csExtends mapping parsing/projection for class/method/field data used by remapping generation.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​NativeAotJniRetention.csNativeAOT ELF-based literal retention to conservatively select required remap entries.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​JniDescriptorText.csConverts Java source-form types to JNI tokens + builds method descriptors.
src/​Xamarin.Android.Build.Tasks/​Utilities/​JniRemapping/​JniAssemblyRewriter.csAdds scan-only entrypoint for linked-assembly analysis (no rewriting).
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​R8Tests.csAdds unit coverage for keep-option and config generation behavior.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​GenerateTrimmableTypeMapTests.csVerifies NativeAOT proguard generation respects allowobfuscation.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Tasks/​GenerateJniRemappingNativeCodeTests.csNew tests for native remap table emission, ordering, and legacy compatibility.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​InvalidConfigTests.csTests defaults + invalid configuration errors for new MSBuild properties.
src/​Xamarin.Android.Build.Tasks/​Tasks/​R8.csAdds seed mapping mode, applymapping support, and conditional dontobfuscate removal.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateR8JniRemapping.csGenerates JNI remapping XML from R8 mapping + existing remaps; supports NativeAOT retention path.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateR8JniManifestProguardConfiguration.csGenerates manifest keep rules to stabilize seed mapping applicability.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateProguardConfiguration.csEmits allowobfuscation on keep rules when runtime remapping is enabled.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateNativeAotProguardConfiguration.csEmits allowobfuscation for NativeAOT-generated keep rules when enabled.
src/​Xamarin.Android.Build.Tasks/​Tasks/​GenerateJniRemappingNativeCode.csExtends generated tables to include reverse types + fields; exposes info for tests.
src/​Xamarin.Android.Build.Tasks/​Resources/​proguard_xamarin.cfgAdds/adjusts keep rules needed for stable seed/final graphs and bootstrap types.
src/​Xamarin.Android.Build.Tasks/​Resources/​proguard_trimmable_nativeaot.cfgAligns NativeAOT baseline keep rules with remapping needs and seed/final stability.
src/​Xamarin.Android.Build.Tasks/​Properties/​Resources.resxAdds XA4327/8/9 localized strings for errors/warnings/validation.
src/​Xamarin.Android.Build.Tasks/​Properties/​Resources.Designer.csUpdates generated resource accessors for XA4327/8/9.
src/​Xamarin.Android.Build.Tasks/​MSBuild/​Xamarin/​Android/​Xamarin.Android.Aapt2.targetsMoves AAPT proguard rule tracking to incremental parent target.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.Trimmable.targetsImports 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.targetsIncludes 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.targetsReworks linked-assembly proguard inputs; adds remapping-assembly prep + incremental inputs.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.TypeMap.LlvmIr.targetsAdds remapping enable flag into proguard generation and incremental inputs.
src/​Xamarin.Android.Build.Tasks/​Microsoft.Android.Sdk/​targets/​Microsoft.Android.Sdk.R8JniRemapping.targetsNew 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.targetsLinks remapping object into NativeAOT shared library and updates incremental inputs.
src/​native/​nativeaot/​include/​runtime-base/​internal-pinvokes.hhAdds internal pinvokes for reverse-type and field lookup.
src/​native/​nativeaot/​host/​jni-remapping-tables-stub.ccProvides weak empty table symbols for apps without remapping objects.
src/​native/​nativeaot/​host/​internal-pinvoke-stubs.ccRemoves now-shared remapping pinvoke stubs from NativeAOT host stubs.
src/​native/​nativeaot/​host/​host.ccPlumbs jniRemappingInUse via shared JniRemapping::is_in_use().
src/​native/​nativeaot/​host/​CMakeLists.txtAdds shared remapping sources and stub table compilation to NativeAOT host build.
src/​native/​native.targetsIncludes shared remapping sources/headers in NativeAOT flavor build inputs.
src/​native/​mono/​xamarin-app-stub/​xamarin-app.hhUpdates stub ABI structs to include target_signature + field remapping structures.
src/​native/​mono/​xamarin-app-stub/​application_dso_stub.ccUpdates stub table initializers for new method signature field.
src/​native/​mono/​runtime-base/​internal-pinvokes.hhAdds internal pinvoke declarations for reverse-type and field lookup.
src/​native/​mono/​pinvoke-override/​pinvoke-tables.includeExtends pinvoke table entries/count for the new remapping exports.
src/​native/​mono/​pinvoke-override/​generate-pinvoke-tables.ccAdds new internal pinvoke names to generator input list.
src/​native/​mono/​monodroid/​internal-pinvokes.ccAdds MonoVM-safe placeholder exports for new remapping entrypoints.
src/​native/​clr/​xamarin-app-stub/​application_dso_stub.ccExtends CLR stub tables to include reverse types + fields + signature pinning.
src/​native/​clr/​runtime-base/​jni-remapping.ccImplements binary-search remapping lookups (types, reverse types, methods, fields) + is_in_use().
src/​native/​clr/​pinvoke-override/​precompiled.ccMaps new internal pinvoke entrypoints to implementations.
src/​native/​clr/​include/​xamarin-app.hhDeclares remapping table symbols and adds field + reverse type structures.
src/​native/​clr/​include/​runtime-base/​jni-remapping.hhDeclares shared lookup surface including reverse type and field lookup.
src/​native/​clr/​include/​runtime-base/​internal-pinvokes.hhDeclares new remapping pinvokes for CoreCLR runtime.
src/​native/​clr/​host/​internal-pinvokes-shared.ccCentralizes shared remapping pinvoke implementations for CoreCLR/NativeAOT.
src/​native/​clr/​host/​internal-pinvokes-clr.ccRemoves remapping implementations now provided by shared file.
src/​native/​clr/​host/​host.ccUses JniRemapping::is_in_use() for init flag and includes remapping header.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMapValueManager.csEnsures FindClass uses replacement type name when remapping is enabled.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMapTypeManager.csAdds reverse-type handling for Java-to-managed lookups; uses replacement type for signatures.
src/​Mono.Android/​Microsoft.Android.Runtime/​TrimmableTypeMap.csUses reverse type for proxy lookup and replacement type for FindClass checks.
src/​Mono.Android/​Microsoft.Android.Runtime/​JniRemappingLookup.csAdds reverse type + field lookup plumbing and supports target-method-signature.
src/​Mono.Android/​Android.Runtime/​RuntimeNativeMethods.csAdds LibraryImport declarations for reverse type + field lookup pinvokes.
src/​Mono.Android/​Android.Runtime/​AndroidRuntime.csExposes GetOriginalTypeCore via reverse-type lookup.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop/​JniPeerMembersTests.csAdds tests validating remapped field names and pinned target signatures.
external/​Java.Interop/​tests/​Java.Interop-Tests/​Java.Interop/​JavaVMFixture.csExtends test runtime type manager with field replacement support.
external/​Java.Interop/​src/​Java.Interop/​PublicAPI.Unshipped.txtRecords new public API surface additions for replacement fields + original type.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniType.csAdds TryGet{Static,Instance}Field helpers to support remapped field probing.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniRuntime.ReflectionJniTypeManager.csAdds null default implementation for field replacement in reflection manager.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniRuntime.JniTypeManager.csAdds ReplacementFieldInfo + original type + replacement field APIs.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniStaticMethods.csUses replacement lookup keyed by original type name (compat + remapping).
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniStaticFields.csAdds remapped static field probing and fallback to original lookup.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniInstanceMethods.csTracks original vs effective JNI type names; remaps ctor/method lookup accordingly.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.JniInstanceFields.csAdds remapped instance field probing and fallback behavior.
external/​Java.Interop/​src/​Java.Interop/​Java.Interop/​JniPeerMembers.csTracks original type name and performs replacement lookups across base types/types.
Documentation/​docs-mobile/​TOC.ymlAdds XA4327/8/9 docs to TOC.
Documentation/​docs-mobile/​messages/​xa4329.mdDocuments invalid/unsupported configuration errors and resolutions.
Documentation/​docs-mobile/​messages/​xa4328.mdDocuments remapping incompleteness warnings (conflicts/signature conversion).
Documentation/​docs-mobile/​messages/​xa4327.mdDocuments remapping generation failures and troubleshooting steps.
Documentation/​docs-mobile/​messages/​index.mdAdds XA4327/8/9 to messages index.
Documentation/​docs-mobile/​building-apps/​build-properties.mdDocuments 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 on lines +36 to +48
string TestDirectory {
get {
Assert.IsNotNull (directory);
return directory!;
}
}

List<BuildErrorEventArgs> Errors {
get {
Assert.IsNotNull (errors);
return errors!;
}
}

[TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")]
[TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';'
[TestCase ("package com.example.app;\npublic class Foo {}", "com.example.app")] [TestCase ("package com.example.app ;\npublic class Foo {}", "com.example.app")] // space before ';'
return javaMethodName.Length != 0;
}

internal static string BuildClassEntry (string className) => $"C\t{className}"; internal static string BuildFieldEntry (string className, string fieldName) => $"F\t{className}\t{fieldName}";
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>
Sign up for freeto 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.

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

2 participants

@simonrozsival