Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Documentation/docs-mobile/TOC.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@
href: messages/xa4325.md
- name: XA4326
href: messages/xa4326.md
- name: XA4327
href: messages/xa4327.md
- name: XA4328
href: messages/xa4328.md
- name: XA4329
href: messages/xa4329.md
- name: "XA5xxx: GCC and toolchain"
items:
- name: "XA5xxx: GCC and toolchain"
Expand Down
52 changes: 52 additions & 0 deletions Documentation/docs-mobile/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,33 @@ removing the existing one(s) and adding your own AOT profiles.
This property is `False` by default.


## AndroidEnableR8Obfuscation

A boolean property that opts an Android application into R8 name obfuscation.
The default is `false`; setting
[`$(AndroidR8ObfuscationMode)`](#androidr8obfuscationmode) alone does not enable it.
This feature is experimental.

The current implementation requires `AndroidLinkTool=r8`,
`AndroidTypeMapImplementation=trimmable`, `PublishTrimmed=true`, and either the
CoreCLR or NativeAOT runtime. Explicit incompatible settings produce
[XA4329](../messages/xa4329.md) rather than being silently changed.
This property has no effect on library projects.

For example:

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

Added in .NET 11.

## AndroidEnableRestrictToAttributes

An enum-style property with valid values of `obsolete` and `disable`.
Expand Down Expand Up @@ -1095,6 +1122,31 @@ r8 dex-compiler and shrinker. The default value is a path into the
.NET for Android workload installation. For further information see our
documentation on [D8 and R8][d8-r8].

## AndroidR8ObfuscationMode

Selects how managed JNI references are reconciled with R8's obfuscated Java
names. It is only used when
[`$(AndroidEnableR8Obfuscation)`](#androidenabler8obfuscation) is `true`.
The default is `runtime-remapping`.

| Value | Behavior |
|---|---|
| `runtime-remapping` | Keeps managed assemblies unchanged and translates JNI type/member lookups using generated native remapping tables. Available for trimmed CoreCLR and NativeAOT applications. |
| `experimental-rewriting` | Reserved for the separate managed-assembly rewriting implementation. This SDK does not yet include its build pipeline; selecting it reports [XA4329](../messages/xa4329.md). |

The runtime-remapping mode leaves managed assemblies unchanged. It runs R8 once,
after managed trimming or ILC, then uses the resulting R8 mapping to
generate native runtime remapping tables. CoreCLR selects remaps from linked
assemblies. NativeAOT selects remaps from retained JNI literals in ILC's native
object and statically links the table afterward.

Runtime-generated JNI names may require explicit remapping or keep rules.
Conservative keep rules still protect native callbacks, bootstrap code, and
resource-referenced names. Neither mode is selected as a fallback for another
mode; unrecognized values report XA4329 when obfuscation is enabled.

Added in .NET 11.

## AndroidResgenExtraArgs

Specifies
Expand Down
3 changes: 3 additions & 0 deletions Documentation/docs-mobile/messages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
+ [XA4324](xa4324.md): [{arch}] Unable to delete source file '{file}'.
+ [XA4325](xa4325.md): Failed to rewrite managed JNI names for R8. {message}
+ [XA4326](xa4326.md): Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous `JNIEnv.FindClass` source.
+ [XA4327](xa4327.md): Failed to generate the R8 JNI remapping data. {message}
+ [XA4328](xa4328.md): The R8 JNI remapping data is incomplete. {message}
+ [XA4329](xa4329.md): Invalid or unsupported R8 obfuscation configuration.

## XA5xxx: GCC and toolchain

Expand Down
53 changes: 53 additions & 0 deletions Documentation/docs-mobile/messages/xa4327.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: .NET for Android error XA4327
description: XA4327 error code
ms.date: 09/04/2026
f1_keywords:
- "XA4327"
---

# .NET for Android error XA4327

## Example messages

```
error XA4327: Failed to generate the R8 JNI remapping data. The R8 mapping file 'obj/Release/net11.0/android-arm64/r8-jni-final-mapping.txt' was not found.
```

## Issue

The build could not produce the data that lets the runtime translate the
original JNI names in the managed assemblies into the names R8 chose.

This only happens when R8 obfuscation is enabled with
`$(AndroidEnableR8Obfuscation)=true` and
`$(AndroidR8ObfuscationMode)=runtime-remapping`. The remapping is built from the
mapping file produced by the final R8 pass after managed trimming or ILC. On
NativeAOT, this also reports a missing or invalid ILC native object: remapping
data is selected from the surviving JNI literals in that object before the final
native link.

NativeAOT filtering supports normal generated JNI bindings whose class names,
member names, and descriptors are literal strings. It inspects the initialized
data of the 32-bit or 64-bit ILC ELF object, including UTF-16 literals and UTF-8
metadata. Shared strings can retain extra mappings; they do not make arbitrary
runtime-constructed JNI names safe. JNI names or descriptors constructed at
runtime require explicit remapping XML or R8 keep rules that preserve the
affected Java types and members.

## Solution

The message names the specific file that is missing or unreadable.

* Build with `-v:diag` (or check the binary log) for the output of the final R8
pass that should have produced the mapping file, and address any failure it reports.
* Delete the `obj` directory and rebuild if the intermediate output is in an
inconsistent state.
* For NativeAOT, ensure ILC completed and its `NativeObject` output exists before
remapping runs. Pre-ILC assemblies and dependency graphs cannot substitute for
that object. Missing or invalid retention data fails the build instead of
falling back to an unfiltered mapping.
* If the failure persists, [report an issue][report-issue] and include the full
error, a binary log, and, if possible, a project that reproduces it.

[report-issue]: https://github.com/dotnet/android/issues/new/choose
44 changes: 44 additions & 0 deletions Documentation/docs-mobile/messages/xa4328.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: .NET for Android warning XA4328
description: XA4328 warning code
ms.date: 09/04/2026
f1_keywords:
- "XA4328"
---

# .NET for Android warning XA4328

## Example message

```
warning XA4328: The R8 JNI remapping data is incomplete. The 'replace-type' entry for 'T com/contoso/MainActivity' was not emitted: another JNI remapping input already maps it to 'com/contoso/Renamed', which conflicts with 'a/b'.
```

## Issue

The R8 JNI runtime remapping is generated from the final R8 mapping file and is
merged with every other JNI remapping input in the build, such as the Intune
(MAM) mapping.

An entry produced from the R8 mapping described the same type or member as an
entry that another input already contributed, but mapped it somewhere else. The
pre-existing input wins and the conflicting entry is not emitted.

When the conflict is on a type, the type's reverse mapping and all of its
members are left to the other input as well, so the type named in the message is
not remapped for R8 at all.

The warning is also emitted when a Java signature in the R8 mapping file cannot
be converted to a JNI descriptor. That entry is skipped as well.

## Solution

Only one remapping input can own a given type or member.

* If the app uses the Intune (MAM) mapping, exclude the affected types from the
R8 renaming with a `-keep` rule in a `@(ProguardConfiguration)` file so the
final R8 pass does not rename them.
* If the conflict is unexpected, [report an issue][report-issue] and include the
full warning, the final R8 mapping file, and the other remapping input.

[report-issue]: https://github.com/dotnet/android/issues/new/choose
40 changes: 40 additions & 0 deletions Documentation/docs-mobile/messages/xa4329.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: .NET for Android error XA4329
description: XA4329 error code
ms.date: 09/05/2026
f1_keywords:
- "XA4329"
---

# .NET for Android error XA4329

## Example messages

```
Invalid value for AndroidEnableR8Obfuscation: 'yes'. Valid values are: true, false.
```

```
AndroidR8ObfuscationMode='experimental-rewriting' is not available in this SDK. Use 'runtime-remapping' or set AndroidEnableR8Obfuscation=false.
```

## Issue

An R8 obfuscation property has an invalid value, the selected mode is unavailable,
or the application's build configuration is incompatible with obfuscation.

## Solution

Set `AndroidEnableR8Obfuscation` to `true` or `false`. When enabled, use
`AndroidR8ObfuscationMode=runtime-remapping` (the default), `AndroidLinkTool=r8`,
`AndroidTypeMapImplementation=trimmable`, and `PublishTrimmed=true` with CoreCLR
or NativeAOT.

The `experimental-rewriting` value is reserved for a separate implementation
whose build pipeline is not included in this SDK. It does not fall back to
runtime remapping. Setting a mode alone does not enable obfuscation.
Runtime remapping does not rewrite managed assemblies; it uses the final R8
mapping to generate runtime lookup tables after trimming or ILC.

See [AndroidEnableR8Obfuscation](../building-apps/build-properties.md#androidenabler8obfuscation)
and [AndroidR8ObfuscationMode](../building-apps/build-properties.md#androidr8obfuscationmode).
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,24 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
return InstanceFields.GetOrAdd (encodedMember, static (member, fields) => {
string field, signature;
JniPeerMembers.GetNameAndSignature (member, out field, out signature);
return fields.Members.JniPeerType.GetInstanceField (field, signature);
return fields.GetFieldInfo (field, signature);
}, this);
}

JniFieldInfo GetFieldInfo (string field, string signature)
{
var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature);
if (newField.HasValue) {
var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
var fieldName = newField.Value.TargetJniFieldName ?? field;
var fieldSig = newField.Value.TargetJniFieldSignature ?? signature;

using var t = new JniType (typeName);
if (t.TryGetInstanceField (fieldName, fieldSig, out var f)) {
return f;
}
}
return Members.JniPeerType.GetInstanceField (field, signature);
}
}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ internal JniInstanceMethods (JniPeerMembers members)
declaringType.FullName));

DeclaringType = declaringType;
jniPeerType = new JniType (info.Name);
targetJniTypeName = info.Name;
jniPeerType = new JniType (targetJniTypeName);
jniPeerType.RegisterWithRuntime ();
}

JniPeerMembers? members;
JniType? jniPeerType;
readonly string? targetJniTypeName;

string TargetJniTypeName => targetJniTypeName ?? Members.JniPeerTypeName;

internal JniPeerMembers Members => members ?? throw new InvalidOperationException ();

Expand Down Expand Up @@ -59,7 +63,23 @@ public JniMethodInfo GetConstructor (string signature)
if (signature == null)
throw new ArgumentNullException (nameof (signature));
return InstanceMethods.GetOrAdd (signature, static (member, methods) =>
methods.JniPeerType.GetConstructor (member), this);
methods.GetConstructorCore (member), this);
}

JniMethodInfo GetConstructorCore (string signature)
{
// Constructors are never renamed, but their parameter types can be, so the descriptor
// still has to be translated.
var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, "<init>", signature, searchBaseTypes: false);
var targetSignature = newMethod?.TargetJniMethodSignature;
if (targetSignature != null && !string.Equals (targetSignature, signature, StringComparison.Ordinal)) {
var typeName = newMethod?.TargetJniType ?? TargetJniTypeName;
using var t = new JniType (typeName);
if (t.TryGetInstanceMethod ("<init>", targetSignature, out var m)) {
return m;
}
}
return JniPeerType.GetConstructor (signature);
}

internal JniInstanceMethods GetConstructorsForType (Type declaringType)
Expand Down Expand Up @@ -104,9 +124,9 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (string method, string signature)
{
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, DeclaringType, method, signature);
if (newMethod.HasValue) {
var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName;
var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName;
var methodName = newMethod.Value.TargetJniMethodName ?? method;
var methodSig = newMethod.Value.TargetJniMethodSignature ?? signature;

Expand All @@ -120,7 +140,7 @@ JniMethodInfo GetMethodInfo (string method, string signature)
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
return m;
}
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
Console.Error.WriteLine ($"warning: For declared method `{TargetJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
}
return JniPeerType.GetInstanceMethod (method, signature);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,26 @@ public JniFieldInfo GetFieldInfo (string encodedMember)
return StaticFields.GetOrAdd (encodedMember, static (member, fields) => {
string field, signature;
JniPeerMembers.GetNameAndSignature (member, out field, out signature);
return fields.Members.JniPeerType.GetStaticField (field, signature);
return fields.GetFieldInfo (field, signature);
}, this);
}

JniFieldInfo GetFieldInfo (string field, string signature)
{
var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, Members.ManagedPeerType, field, signature);
if (newField.HasValue) {
var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
var fieldName = newField.Value.TargetJniFieldName ?? field;
var fieldSig = newField.Value.TargetJniFieldSignature ?? signature;

using var t = new JniType (typeName);
if (t.TryGetStaticField (fieldName, fieldSig, out var f)) {
return f;
}
}
return Members.JniPeerType.GetStaticField (field, signature);
}

internal void Dispose ()
{
StaticFields.Clear ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (string method, string signature)
{
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
var newMethod = JniPeerMembers.GetReplacementMethodInfo (Members.JniPeerTypeName, Members.ManagedPeerType, method, signature);
if (newMethod.HasValue) {
using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName);
if (t.TryGetStaticMethod (
Expand Down
Loading
Loading