Skip to content

Commit 19b0184

Browse files
[r8-obfuscation] Rewrite managed JNI metadata from R8 mappings (#12630)
Related to #12535 Depends on #12629 Layer 3 of 6 in the replacement stack for #12575. This layer rewrites standard managed JNI metadata from R8 mappings. Trimmable typemap and FieldRVA handling follows in layer 4.
1 parent 411cc92 commit 19b0184

13 files changed

Lines changed: 2699 additions & 5 deletions

File tree

Documentation/docs-mobile/TOC.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@
364364
href: messages/xa4323.md
365365
- name: XA4324
366366
href: messages/xa4324.md
367+
- name: XA4325
368+
href: messages/xa4325.md
369+
- name: XA4326
370+
href: messages/xa4326.md
367371
- name: "XA5xxx: GCC and toolchain"
368372
items:
369373
- name: "XA5xxx: GCC and toolchain"

Documentation/docs-mobile/messages/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
254254
+ [XA4322](xa4322.md): Skipping library ProGuard configuration file '{file}' (from {source}) because it contains the unsupported global option '{option}'. Global ProGuard options are only allowed in application projects.
255255
+ [XA4323](xa4323.md): Ignoring directory '{directory}' as it does not exist.
256256
+ [XA4324](xa4324.md): [{arch}] Unable to delete source file '{file}'.
257+
+ [XA4325](xa4325.md): Failed to rewrite managed JNI names for R8. {message}
258+
+ [XA4326](xa4326.md): Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous `JNIEnv.FindClass` source.
257259

258260
## XA5xxx: GCC and toolchain
259261

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: .NET for Android error XA4325
3+
description: XA4325 error code
4+
ms.date: 09/01/2026
5+
f1_keywords:
6+
- "XA4325"
7+
---
8+
9+
# .NET for Android error XA4325
10+
11+
## Example messages
12+
13+
```
14+
error XA4325: Failed to rewrite managed JNI names for R8. The 'SourceFiles' and 'DestinationFiles' item groups must contain the same number of items.
15+
```
16+
17+
```
18+
error XA4325: Failed to rewrite managed JNI names for R8. Could not rewrite the JNI names in the assembly 'obj/Release/net11.0-android/android/Acme.App.dll': The file contains no managed metadata.
19+
```
20+
21+
## Issue
22+
23+
When R8 obfuscates Java type and member names, the JNI names embedded in your
24+
managed assemblies must be updated to match the obfuscated names. This error
25+
means that step failed, so the app would not have been able to find its Java
26+
types at run time.
27+
28+
There are two causes:
29+
30+
* **Mismatched item groups.** The `SourceFiles` and `DestinationFiles` item
31+
groups passed to the `RewriteJniNamesForR8` task did not contain the same
32+
number of items. This only happens if a custom target invokes the task
33+
directly, or if a target that produces these item groups has been overridden.
34+
35+
* **An assembly could not be rewritten.** A specific assembly could not be read
36+
or reconstructed. The message names the assembly and includes the underlying
37+
reason, such as the file not containing managed metadata or containing
38+
malformed IL.
39+
40+
## Solution
41+
42+
For the mismatched item groups case, review any custom targets that call
43+
`RewriteJniNamesForR8` and make sure `SourceFiles` and `DestinationFiles` are
44+
populated in matching order, or set `DestinationDirectory` instead of
45+
`DestinationFiles`.
46+
47+
For the assembly failure case, first confirm the named file is a managed
48+
assembly and is not corrupt. Deleting the `bin/` and `obj/` directories and
49+
rebuilding clears a partially written or stale file.
50+
51+
If the named file is a valid managed assembly and the failure persists, this is
52+
unexpected. Please [report an issue][report-issue] and include the full error
53+
message, the name of the assembly, and, if possible, a project that reproduces
54+
the failure.
55+
56+
[report-issue]: https://github.com/dotnet/android/issues/new/choose
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: .NET for Android warning XA4326
3+
description: XA4326 warning code
4+
ms.date: 09/01/2026
5+
f1_keywords:
6+
- "XA4326"
7+
---
8+
9+
# .NET for Android warning XA4326
10+
11+
## Example message
12+
13+
```
14+
warning XA4326: Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous JNIEnv.FindClass source.
15+
```
16+
17+
## Issue
18+
19+
R8 renamed a Java class referenced by a managed JNI member lookup. The class
20+
handle is assigned more than once, comes from an ambiguous control-flow path, or
21+
cannot be proven to come directly from `JNIEnv.FindClass`.
22+
23+
The class name can be rewritten, but the corresponding member name cannot be
24+
safely associated with one original Java class. Guessing could make the managed
25+
assembly request a member from the wrong obfuscated class.
26+
27+
## Solution
28+
29+
This warning is unexpected for code generated by .NET for Android. Please
30+
[report an issue][report-issue] and include the full warning, the affected
31+
assembly, its R8 mapping file, and, if possible, a project that reproduces the
32+
warning.
33+
34+
If the assembly contains custom JNI code, keep each `JNIEnv.FindClass` call and
35+
the member lookup that uses its result in an unambiguous sequence. Initialize
36+
each cached class-handle field from one direct `JNIEnv.FindClass` call and do
37+
not assign another class handle to the same field.
38+
39+
[report-issue]: https://github.com/dotnet/android/issues/new/choose

src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Xamarin.Android.Build.Tasks/Properties/Resources.resx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,23 @@ Remove the '{0}' reference from your project and add the '{1}' NuGet package ins
884884
<comment>{0} - The target architecture, such as Arm, Arm64, or X86_64
885885
{1} - The path to the source file which could not be deleted.</comment>
886886
</data>
887+
<data name="XA4325" xml:space="preserve">
888+
<value>Failed to rewrite managed JNI names for R8. {0}</value>
889+
<comment>{0} - A sentence describing the specific failure. It is supplied by one of the XA4325_* resources.</comment>
890+
</data>
891+
<data name="XA4325_AssemblyFailure" xml:space="preserve">
892+
<value>Could not rewrite the JNI names in the assembly '{0}': {1}</value>
893+
<comment>{0} - The path of the assembly which could not be rewritten.
894+
{1} - The underlying message describing why the assembly could not be rewritten. It is not localized.</comment>
895+
</data>
896+
<data name="XA4325_SourceDestinationCount" xml:space="preserve">
897+
<value>The 'SourceFiles' and 'DestinationFiles' item groups must contain the same number of items.</value>
898+
<comment>The following are literal MSBuild item group names and should not be translated: 'SourceFiles', 'DestinationFiles'.</comment>
899+
</data>
900+
<data name="XA4326" xml:space="preserve">
901+
<value>Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous JNIEnv.FindClass source.</value>
902+
<comment>The following are literal API names and should not be translated: JNI, JNIEnv.FindClass.</comment>
903+
</data>
887904
<data name="XA5101" xml:space="preserve">
888905
<value>Missing Android NDK toolchains directory '{0}'. Please install the Android NDK.</value>
889906
<comment>{0} - The path of the missing directory</comment>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using Microsoft.Android.Build.Tasks;
7+
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Utilities;
9+
using Xamarin.Android.Tasks.JniRemapping;
10+
11+
namespace Xamarin.Android.Tasks
12+
{
13+
/// <summary>
14+
/// Rewrites JNI names embedded in compiled assemblies: Android.Runtime.RegisterAttribute,
15+
/// the Java.Interop.Jni*SignatureAttribute family, and generated JniPeerMembers /
16+
/// RegisterNatives <c>ldstr</c> strings. The R8 <c>mapping.txt</c> supplies the obfuscated
17+
/// Java names that managed metadata must reference.
18+
///
19+
/// Each assembly is fully reconstructed with System.Reflection.Metadata: every metadata table
20+
/// row is cloned in its original order, so every entity token keeps its value, while the
21+
/// heaps, method bodies, managed resources, and mapped field data are re-emitted. Replacements
22+
/// may therefore be of any length.
23+
///
24+
/// An adjacent PDB is copied unchanged: it stays valid because method tokens, IL offsets, and
25+
/// the PE's CodeView identity (GUID, age, path) are all preserved.
26+
/// </summary>
27+
public class RewriteJniNamesForR8 : AndroidTask
28+
{
29+
static readonly StringComparison PathComparison = Path.DirectorySeparatorChar == '\\'
30+
? StringComparison.OrdinalIgnoreCase
31+
: StringComparison.Ordinal;
32+
33+
public override string TaskPrefix => "RJN";
34+
35+
[Required]
36+
public ITaskItem [] SourceFiles { get; set; } = [];
37+
38+
public ITaskItem [] DestinationFiles { get; set; } = [];
39+
40+
public string? DestinationDirectory { get; set; }
41+
42+
[Required]
43+
public string MappingFile { get; set; } = "";
44+
45+
public string? RewriteManifestFile { get; set; }
46+
47+
[Output]
48+
public ITaskItem [] RewrittenFiles { get; set; } = [];
49+
50+
public override bool RunTask ()
51+
{
52+
RewrittenFiles = [];
53+
if (DestinationDirectory.IsNullOrEmpty () && SourceFiles.Length != DestinationFiles.Length) {
54+
Log.LogCodedError ("XA4325", Properties.Resources.XA4325, Properties.Resources.XA4325_SourceDestinationCount);
55+
return !Log.HasLoggedErrors;
56+
}
57+
58+
R8Mapping mapping = R8Mapping.Load (MappingFile);
59+
var rewrittenFiles = new List<ITaskItem> (SourceFiles.Length);
60+
61+
for (int i = 0; i < SourceFiles.Length; i++) {
62+
string source = SourceFiles [i].ItemSpec;
63+
string destination = DestinationDirectory.IsNullOrEmpty ()
64+
? DestinationFiles [i].ItemSpec
65+
: Path.Combine (DestinationDirectory, Path.GetFileName (source));
66+
try {
67+
RewriteAssembly (source, destination, mapping);
68+
var rewritten = new TaskItem (SourceFiles [i]) {
69+
ItemSpec = destination,
70+
};
71+
rewritten.SetMetadata ("OriginalItemSpec", source);
72+
rewrittenFiles.Add (rewritten);
73+
} catch (JniRewriteException e) {
74+
Log.LogCodedError ("XA4325", Properties.Resources.XA4325,
75+
string.Format (Properties.Resources.XA4325_AssemblyFailure, source, e.Message));
76+
}
77+
}
78+
79+
if (!Log.HasLoggedErrors) {
80+
RewrittenFiles = rewrittenFiles.ToArray ();
81+
if (!RewriteManifestFile.IsNullOrEmpty ()) {
82+
WriteRewriteManifest (RewriteManifestFile, mapping.AccessedEntries);
83+
}
84+
}
85+
return !Log.HasLoggedErrors;
86+
}
87+
88+
static void WriteRewriteManifest (string path, IEnumerable<string> entries)
89+
{
90+
string? directory = Path.GetDirectoryName (path);
91+
if (!directory.IsNullOrEmpty ()) {
92+
Directory.CreateDirectory (directory);
93+
}
94+
Files.CopyIfStringChanged (R8Mapping.CreateManifestContent (entries), path);
95+
}
96+
97+
void RewriteAssembly (string sourcePath, string destinationPath, R8Mapping mapping)
98+
{
99+
string? destinationDirectory = Path.GetDirectoryName (destinationPath);
100+
if (!destinationDirectory.IsNullOrEmpty ()) {
101+
Directory.CreateDirectory (destinationDirectory);
102+
}
103+
104+
JniRewriteResult result = JniAssemblyRewriter.Rewrite (File.ReadAllBytes (sourcePath), mapping, Log);
105+
106+
Log.LogDebugMessage ($"RewriteJniNamesForR8: rewrote {result.ReplacementCount} JNI name(s) in '{Path.GetFileName (sourcePath)}'.");
107+
if (result.StrongNameSignatureCleared) {
108+
Log.LogDebugMessage ($"RewriteJniNamesForR8: '{Path.GetFileName (sourcePath)}' is strong-named; preserved its public-key identity and emitted a delay-signed linker input.");
109+
}
110+
111+
bool inPlace = AreSamePath (sourcePath, destinationPath);
112+
if (!inPlace || result.ReplacementCount != 0) {
113+
using var output = new MemoryStream (result.Image, writable: false);
114+
Files.CopyIfStreamChanged (output, destinationPath);
115+
}
116+
if (!inPlace) {
117+
CopyAdjacentPdbUnchanged (sourcePath, destinationPath);
118+
}
119+
}
120+
121+
internal static bool AreSamePath (string firstPath, string secondPath)
122+
=> String.Equals (Path.GetFullPath (firstPath), Path.GetFullPath (secondPath), PathComparison);
123+
124+
static void CopyAdjacentPdbUnchanged (string sourcePath, string destinationPath)
125+
{
126+
string pdbSource = Path.ChangeExtension (sourcePath, "pdb");
127+
string pdbDestination = Path.ChangeExtension (destinationPath, "pdb");
128+
if (File.Exists (pdbSource)) {
129+
Files.CopyIfChanged (pdbSource, pdbDestination);
130+
} else if (File.Exists (pdbDestination)) {
131+
Files.SetWriteable (pdbDestination);
132+
File.Delete (pdbDestination);
133+
}
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)