Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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 @@ -382,6 +382,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
45 changes: 37 additions & 8 deletions Documentation/docs-mobile/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,16 +1120,45 @@ documentation on [D8 and R8][d8-r8].
An enum-style property that specifies how `r8` obfuscates Java names when
[`$(AndroidLinkTool)`](#androidlinktool) is `r8`. Supported values are:

- `private-members` preserves Java class and interface names and public or
protected member names. Private and package-private members can be
obfuscated, and R8 optimization is enabled.
- `disabled` disables obfuscation, preserves all Java names, and uses the
non-optimizing Android R8 defaults.
| Value | Behavior |
|---|---|
| `private-members` | Preserves Java class and interface names and public or protected member names. Private and package-private members can be obfuscated, and R8 optimization is enabled. |
| `disabled` | Disables obfuscation, preserves all Java names, and uses the non-optimizing Android R8 defaults. |
| `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). |

This property does not disable R8 code shrinking. It was introduced in a .NET 10
servicing release and defaults to `disabled` in .NET 10 and to `private-members`
in .NET 11 and later. The experimental `runtime-remapping` value was added in
.NET 11 and must be selected explicitly.

The `runtime-remapping` value 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
runtime-remapping mode has no effect on library projects.

This property does not disable R8 code shrinking.
For example:

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

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.

This property was introduced in a .NET 10 servicing release. It defaults to
`disabled` in .NET 10 and to `private-members` in .NET 11 and later.
Runtime-generated JNI names may require explicit remapping or keep rules.
Conservative keep rules still protect native callbacks, bootstrap code, and
resource-referenced names. No mode falls back to another mode; unrecognized
values report [XA1050](../messages/xa1050.md) when R8 is enabled.

## AndroidResgenExtraArgs

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 @@ -263,6 +263,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
6 changes: 5 additions & 1 deletion Documentation/docs-mobile/messages/xa1050.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ f1_keywords:
## Example messages

```dotnetcli
error XA1050: The 'AndroidR8ObfuscationMode' MSBuild property has an invalid value of 'private-member'. Valid values are 'disabled' and 'private-members'.
error XA1050: The 'AndroidR8ObfuscationMode' MSBuild property has an invalid value of 'private-member'. Valid values are 'disabled', 'private-members', 'runtime-remapping', and 'experimental-rewriting'.
```

## Issue
Expand All @@ -32,3 +32,7 @@ Set `$(AndroidR8ObfuscationMode)` to one of the supported values:

Use `disabled` to preserve all Java names and use the legacy non-optimizing
Android R8 configuration.

The experimental `runtime-remapping` value is available for trimmed CoreCLR and
NativeAOT applications with trimmable type maps. `experimental-rewriting` is
reserved but is not implemented; selecting it reports [XA4329](xa4329.md).
52 changes: 52 additions & 0 deletions Documentation/docs-mobile/messages/xa4327.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
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
`$(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
42 changes: 42 additions & 0 deletions Documentation/docs-mobile/messages/xa4329.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
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

```
AndroidR8ObfuscationMode=runtime-remapping requires $(AndroidTypeMapImplementation) to be 'trimmable', but it is 'llvm-ir'.
```

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

## Issue

The selected R8 mode is unavailable, or the application's build configuration is
incompatible with runtime remapping. Unknown mode values report [XA1050](xa1050.md)
when R8 is enabled.

## Solution

Use `AndroidR8ObfuscationMode=disabled` to preserve all Java names, or use
`private-members`, the default in .NET 11, to obfuscate only non-public members.
To enable runtime remapping, use `AndroidR8ObfuscationMode=runtime-remapping`,
`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.
Runtime remapping does not rewrite managed assemblies; it uses the final R8
mapping to generate runtime lookup tables after trimming or ILC.

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

JniFieldInfo GetFieldInfo (ReadOnlySpan<char> field, ReadOnlySpan<char> signature)
{
var newField = JniPeerMembers.GetReplacementFieldInfo (Members.JniPeerTypeName, field, signature);
if (newField.HasValue) {
var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : signature;

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

newField = JniPeerMembers.GetBaseReplacementFieldInfo (Members.ManagedPeerType, field, signature);
if (newField.HasValue) {
var typeName = newField.Value.TargetJniType ?? Members.JniPeerTypeName;
var fieldName = newField.Value.TargetJniFieldName is string name ? name.AsSpan () : field;
var fieldSig = newField.Value.TargetJniFieldSignature is string sig ? sig.AsSpan () : 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 @@ -60,7 +64,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.AsSpan ()), 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, "<init>", signature);
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.AsSpan ());
}

internal JniInstanceMethods GetConstructorsForType (Type declaringType)
Expand Down Expand Up @@ -105,9 +125,31 @@ public JniMethodInfo GetMethodInfo (string encodedMember)
JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signature)
{
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
var newMethod = JniPeerMembers.GetReplacementMethodInfo (TargetJniTypeName, method, signature);
if (newMethod.HasValue) {
var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName;
var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method;
var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature;

using var t = new JniType (typeName);
if (newMethod.Value.TargetJniMethodInstanceToStatic &&
t.TryGetStaticMethod (methodName, methodSig, out m)) {
m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount;
m.StaticRedirect = new JniType (typeName);
return m;
}
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
return m;
}
Console.Error.WriteLine ($"warning: For declared method `{TargetJniTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
}
if (JniPeerType.TryGetInstanceMethod (method, signature, out m)) {
return m;
}

newMethod = JniPeerMembers.GetBaseReplacementMethodInfo (DeclaringType, method, signature);
if (newMethod.HasValue) {
var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName;
var typeName = newMethod.Value.TargetJniType ?? TargetJniTypeName;
var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method;
var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature;

Expand All @@ -121,7 +163,6 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signa
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}`!");
}
return JniPeerType.GetInstanceMethod (method, signature);
}
Expand Down
Loading
Loading