[Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping - #12692
Draft
simonrozsival wants to merge 1 commit into
Draft
[Xamarin.Android.Build.Tasks] Add opt-in R8 runtime remapping#12692simonrozsival wants to merge 1 commit into
simonrozsival wants to merge 1 commit into
Conversation
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>
Contributor
There was a problem hiding this comment.
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
New issues introduced by this change (3)
| Severity | Finding |
|---|---|
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… |
|
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/R8Tests.cs — |
|
src/Xamarin.Android.Build.Tasks/Utilities/JniRemapping/R8Mapping.cs — |
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!) afterAssert.IsNotNull, which the compiler can’t reason about. Prefer?? throwso 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}"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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
during final R8.
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.
fields, inherited members, and managed-peer activation. Share the native
lookup implementation between CoreCLR and NativeAOT; weak empty tables
preserve the disabled NativeAOT path.
AAPT rules in their incremental parent target so no-op builds cannot
delete rules needed by resource-inflated classes.
changes, including missing-output recovery and switching obfuscation off.
Public opt-in
For a trimmed CoreCLR or NativeAOT application:
$(AndroidEnableR8Obfuscation)defaults tofalse.$(AndroidR8ObfuscationMode)defaults toruntime-remapping; selecting amode alone does not enable obfuscation. Library projects are unaffected.
experimental-rewritingis reserved for the separate rewriting workstreamand 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
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.
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.
Fixes: #12535