diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 1244c066b05..8f5f6eeef8a 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -364,6 +364,12 @@ href: messages/xa4323.md - name: XA4324 href: messages/xa4324.md + - name: XA4325 + href: messages/xa4325.md + - name: XA4326 + href: messages/xa4326.md + - name: XA4327 + href: messages/xa4327.md - name: "XA5xxx: GCC and toolchain" items: - name: "XA5xxx: GCC and toolchain" diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 475a57bbbdd..a5fc5014f04 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -468,6 +468,17 @@ removing the existing one(s) and adding your own AOT profiles. This property is `False` by default. +## AndroidEnableR8JniNameObfuscation + +A boolean property that enables R8 obfuscation of Java type, method, and field names +referenced by managed JNI metadata. The build uses an R8-generated mapping to rewrite +managed assemblies before trimming, then applies the same +mapping during the final R8 invocation. + +This property requires `AndroidLinkTool=r8`, +`AndroidTypeMapImplementation=trimmable`, the CoreCLR runtime, and +`PublishTrimmed=true`. +The default value is `False`. ## AndroidEnableRestrictToAttributes diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index dabb47571de..7fb2a66e29a 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -254,6 +254,9 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [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. + [XA4323](xa4323.md): Ignoring directory '{directory}' as it does not exist. + [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 validate R8 JNI mapping data. {message} ## XA5xxx: GCC and toolchain diff --git a/Documentation/docs-mobile/messages/xa4325.md b/Documentation/docs-mobile/messages/xa4325.md new file mode 100644 index 00000000000..d1a125a4138 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4325.md @@ -0,0 +1,56 @@ +--- +title: .NET for Android error XA4325 +description: XA4325 error code +ms.date: 09/01/2026 +f1_keywords: + - "XA4325" +--- + +# .NET for Android error XA4325 + +## Example messages + +``` +error XA4325: Failed to rewrite managed JNI names for R8. The 'SourceFiles' and 'DestinationFiles' item groups must contain the same number of items. +``` + +``` +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. +``` + +## Issue + +When R8 obfuscates Java type and member names, the JNI names embedded in your +managed assemblies must be updated to match the obfuscated names. This error +means that step failed, so the app would not have been able to find its Java +types at run time. + +There are two causes: + +* **Mismatched item groups.** The `SourceFiles` and `DestinationFiles` item + groups passed to the `RewriteJniNamesForR8` task did not contain the same + number of items. This only happens if a custom target invokes the task + directly, or if a target that produces these item groups has been overridden. + +* **An assembly could not be rewritten.** A specific assembly could not be read + or reconstructed. The message names the assembly and includes the underlying + reason, such as the file not containing managed metadata or containing + malformed IL. + +## Solution + +For the mismatched item groups case, review any custom targets that call +`RewriteJniNamesForR8` and make sure `SourceFiles` and `DestinationFiles` are +populated in matching order, or set `DestinationDirectory` instead of +`DestinationFiles`. + +For the assembly failure case, first confirm the named file is a managed +assembly and is not corrupt. Deleting the `bin/` and `obj/` directories and +rebuilding clears a partially written or stale file. + +If the named file is a valid managed assembly and the failure persists, this is +unexpected. Please [report an issue][report-issue] and include the full error +message, the name of the assembly, and, if possible, a project that reproduces +the failure. + +[report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/Documentation/docs-mobile/messages/xa4326.md b/Documentation/docs-mobile/messages/xa4326.md new file mode 100644 index 00000000000..d793367e06d --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4326.md @@ -0,0 +1,39 @@ +--- +title: .NET for Android warning XA4326 +description: XA4326 warning code +ms.date: 09/01/2026 +f1_keywords: + - "XA4326" +--- + +# .NET for Android warning XA4326 + +## Example message + +``` +warning XA4326: Unable to safely rewrite a JNI member lookup because its class handle does not have one structurally unambiguous JNIEnv.FindClass source. +``` + +## Issue + +R8 renamed a Java class referenced by a managed JNI member lookup. The class +handle is assigned more than once, comes from an ambiguous control-flow path, or +cannot be proven to come directly from `JNIEnv.FindClass`. + +The class name can be rewritten, but the corresponding member name cannot be +safely associated with one original Java class. Guessing could make the managed +assembly request a member from the wrong obfuscated class. + +## Solution + +This warning is unexpected for code generated by .NET for Android. Please +[report an issue][report-issue] and include the full warning, the affected +assembly, its R8 mapping file, and, if possible, a project that reproduces the +warning. + +If the assembly contains custom JNI code, keep each `JNIEnv.FindClass` call and +the member lookup that uses its result in an unambiguous sequence. Initialize +each cached class-handle field from one direct `JNIEnv.FindClass` call and do +not assign another class handle to the same field. + +[report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/Documentation/docs-mobile/messages/xa4327.md b/Documentation/docs-mobile/messages/xa4327.md new file mode 100644 index 00000000000..8dcbe8617e3 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4327.md @@ -0,0 +1,45 @@ +--- +title: .NET for Android error XA4327 +description: XA4327 error code +ms.date: 09/01/2026 +f1_keywords: + - "XA4327" +--- + +# .NET for Android error XA4327 + +## Example messages + +``` +error XA4327: Failed to validate R8 JNI mapping data. The R8 JNI rewrite manifest 'obj/Release/net11.0-android/r8-jni-rewrite-manifest.txt' was not found. +``` + +``` +error XA4327: Failed to validate R8 JNI mapping data. The final R8 mapping did not preserve the JNI seed mapping for class 'com/example/MyView'. +``` + +## Issue + +When R8 obfuscates Java type and member names, the build first generates a seed +mapping and rewrites the corresponding names in managed assemblies. The final R8 +invocation must preserve every rewritten name that remains reachable after +trimming. + +This error means that a required mapping or manifest could not be read, the +merged Android manifest could not be used to generate seed keep rules, a linked +assembly could not be scanned, the final R8 mapping changed a seeded name, or +final R8 removed a JNI entry that remained reachable from managed code. It can +also mean that an obfuscated post-trim JNI class could not be mapped uniquely +back to the original Java source generated before trimming. + +## Solution + +Delete the project's `bin/` and `obj/` directories and rebuild to clear stale or +partially written intermediate files. Also review custom ProGuard configuration +for rules that rename or remove the type or member named in the error. + +If the failure persists without custom ProGuard rules, please +[report an issue][report-issue] and include the full XA4327 message, the R8 +mapping files, and, if possible, a project that reproduces the failure. + +[report-issue]: https://github.com/dotnet/android/issues/new/choose diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 6434c515e27..2a0ba4da1c5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -42,9 +42,10 @@ sealed class PEAssemblyBuilder // Avoids creating duplicate __utf8_N types when multiple fields share the same size. readonly Dictionary _sizedTypeCache = new (); - // Deduplication cache for UTF-8 string RVA fields. Strings like "()V" that repeat across - // many proxy types are stored once and shared via the same FieldDefinitionHandle. - readonly Dictionary _utf8FieldCache = new (StringComparer.Ordinal); + // JNI signatures are owner-independent and can safely share one RVA field. JNI method names + // are owner-specific after R8 rewriting, so each registration receives its own field. + readonly Dictionary _sharedUtf8FieldCache = new (StringComparer.Ordinal); + readonly Dictionary> _uniqueUtf8FieldCache = new (StringComparer.Ordinal); TypeDefinitionHandle _privateImplDetailsType; int _utf8FieldCounter; @@ -273,31 +274,57 @@ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string manage } /// - /// Emits deduplicated RVA fields containing the supplied null-terminated UTF-8 strings. + /// Emits RVA fields containing the supplied null-terminated UTF-8 strings. + /// are deduplicated, while every occurrence in + /// receives a separate field. /// Fields are grouped by size so each group is emitted contiguously on its sized helper /// type before any consuming types are emitted. /// - public void PrepareUtf8Fields (IEnumerable values) + public void PrepareUtf8Fields (IEnumerable sharedValues, IEnumerable uniqueValues) { - var valuesBySize = new SortedDictionary> (); - foreach (string value in values) { + var sharedValuesBySize = new SortedDictionary> (); + foreach (string value in sharedValues) { int size = System.Text.Encoding.UTF8.GetByteCount (value) + 1; - if (!valuesBySize.TryGetValue (size, out var valuesForSize)) { + if (!sharedValuesBySize.TryGetValue (size, out var valuesForSize)) { valuesForSize = new SortedSet (StringComparer.Ordinal); - valuesBySize.Add (size, valuesForSize); + sharedValuesBySize.Add (size, valuesForSize); } valuesForSize.Add (value); } - foreach (var group in valuesBySize) { - var sizedType = GetOrCreateSizedType (group.Key); - foreach (string value in group.Value) { - AddUtf8Field (value, sizedType); + var uniqueValuesBySize = new SortedDictionary> (); + foreach (string value in uniqueValues) { + int size = System.Text.Encoding.UTF8.GetByteCount (value) + 1; + if (!uniqueValuesBySize.TryGetValue (size, out var valuesForSize)) { + valuesForSize = new SortedDictionary (StringComparer.Ordinal); + uniqueValuesBySize.Add (size, valuesForSize); + } + valuesForSize.TryGetValue (value, out int count); + valuesForSize [value] = count + 1; + } + + var sizes = new SortedSet (sharedValuesBySize.Keys); + sizes.UnionWith (uniqueValuesBySize.Keys); + foreach (int size in sizes) { + var sizedType = GetOrCreateSizedType (size); + if (sharedValuesBySize.TryGetValue (size, out var sharedForSize)) { + foreach (string value in sharedForSize) { + _sharedUtf8FieldCache.Add (value, AddUtf8Field (value, sizedType)); + } + } + if (uniqueValuesBySize.TryGetValue (size, out var uniqueForSize)) { + foreach (var pair in uniqueForSize) { + var fields = new Queue (pair.Value); + for (int i = 0; i < pair.Value; i++) { + fields.Enqueue (AddUtf8Field (pair.Key, sizedType)); + } + _uniqueUtf8FieldCache.Add (pair.Key, fields); + } } } } - void AddUtf8Field (string value, TypeDefinitionHandle sizedType) + FieldDefinitionHandle AddUtf8Field (string value, TypeDefinitionHandle sizedType) { // Encode to null-terminated UTF-8 (all JNI names/signatures are ASCII). _sigBlob.Clear (); @@ -313,8 +340,7 @@ void AddUtf8Field (string value, TypeDefinitionHandle sizedType) Metadata.GetOrAddBlob (_sigBlob)); Metadata.AddFieldRelativeVirtualAddress (fieldHandle, rva); - - _utf8FieldCache [value] = fieldHandle; + return fieldHandle; } /// @@ -322,13 +348,25 @@ void AddUtf8Field (string value, TypeDefinitionHandle sizedType) /// public FieldDefinitionHandle GetUtf8Field (string value) { - if (_utf8FieldCache.TryGetValue (value, out var existing)) { + if (_sharedUtf8FieldCache.TryGetValue (value, out var existing)) { return existing; } throw new InvalidOperationException ($"UTF-8 field '{value}' was not prepared before type emission."); } + /// + /// Returns and consumes one previously prepared unique UTF-8 RVA field. + /// + public FieldDefinitionHandle GetUniqueUtf8Field (string value) + { + if (_uniqueUtf8FieldCache.TryGetValue (value, out var fields) && fields.Count > 0) { + return fields.Dequeue (); + } + + throw new InvalidOperationException ($"Unique UTF-8 field '{value}' was not prepared before type emission."); + } + void EnsurePrivateImplDetailsType () { if (!_privateImplDetailsType.IsNil) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 56a160a7914..8dd441ce965 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -195,7 +196,10 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) } EmitMemberReferences (); - _pe.PrepareUtf8Fields (EnumerateNativeRegistrationStrings (model.ProxyTypes)); + var validRegistrations = EnumerateValidNativeRegistrations (model.ProxyTypes); + _pe.PrepareUtf8Fields ( + validRegistrations.Select (registration => registration.JniSignature), + validRegistrations.Select (registration => registration.JniMethodName)); // Track wrapper targets → handles for RegisterNatives. var wrapperHandles = new Dictionary (); @@ -219,17 +223,30 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); } - static IEnumerable EnumerateNativeRegistrationStrings (IReadOnlyList proxies) + static List EnumerateValidNativeRegistrations (IReadOnlyList proxies) { + var wrapperTargets = new HashSet (); + foreach (var proxy in proxies) { + foreach (var method in proxy.UcoMethods) { + wrapperTargets.Add (UcoWrapperTargetData.From (proxy, method.WrapperName)); + } + foreach (var constructor in proxy.UcoConstructors) { + wrapperTargets.Add (UcoWrapperTargetData.From (proxy, constructor.WrapperName)); + } + } + + var registrations = new List (); foreach (var proxy in proxies) { if (!proxy.IsAcw) { continue; } foreach (var registration in proxy.NativeRegistrations) { - yield return registration.JniMethodName; - yield return registration.JniSignature; + if (wrapperTargets.Contains (registration.WrapperTarget)) { + registrations.Add (registration); + } } } + return registrations; } static List OrderProxiesForWrapperTargets (IReadOnlyList proxies) @@ -1631,11 +1648,12 @@ void EmitRegisterNatives (JavaPeerProxyData proxy, return; } - // Get the prepared, deduplicated RVA fields for each unique name/signature string. + // Method names are unique per registration because R8 member mappings are owner-specific. + // Signatures remain safely deduplicated because descriptor class mappings are owner-independent. var nameFields = new FieldDefinitionHandle [validRegs.Count]; var sigFields = new FieldDefinitionHandle [validRegs.Count]; for (int i = 0; i < validRegs.Count; i++) { - nameFields [i] = _pe.GetUtf8Field (validRegs [i].Reg.JniMethodName); + nameFields [i] = _pe.GetUniqueUtf8Field (validRegs [i].Reg.JniMethodName); sigFields [i] = _pe.GetUtf8Field (validRegs [i].Reg.JniSignature); } diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets index eed1d4765df..94a4eff2ac7 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets @@ -245,7 +245,10 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. ProguardRuleOutput="$(_Aapt2ProguardRules)" /> - + + true + true + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index b12f80fdc8f..07dccc01a5a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -5,10 +5,11 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider + <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateProguardConfiguration - <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink + <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">_RunILLink @@ -23,6 +24,57 @@ + + + <_AndroidR8JniRewrittenAssemblyDirectory>$(IntermediateOutputPath)r8-jni-rewritten/ + <_AndroidR8JniRewriteStamp>$(_AndroidStampDirectory)_AndroidRewriteJniNamesBeforeILLink.stamp + + + <_AndroidR8JniOriginalManagedAssembly Remove="@(_AndroidR8JniOriginalManagedAssembly)" /> + <_AndroidR8JniHashedManagedAssembly Remove="@(_AndroidR8JniHashedManagedAssembly)" /> + <_AndroidR8JniRewrittenManagedAssembly Remove="@(_AndroidR8JniRewrittenManagedAssembly)" /> + <_AndroidR8JniExpectedRewriteOutput Remove="@(_AndroidR8JniExpectedRewriteOutput)" /> + <_AndroidR8JniMissingRewriteOutput Remove="@(_AndroidR8JniMissingRewriteOutput)" /> + <_AndroidR8JniOriginalManagedAssembly Include="@(ManagedAssemblyToLink)" /> + + + + + + <_AndroidR8JniRewrittenManagedAssembly Include="@(_AndroidR8JniHashedManagedAssembly->'$(_AndroidR8JniRewrittenAssemblyDirectory)%(Hash)/%(Filename)%(Extension)')" /> + <_AndroidR8JniExpectedRewriteOutput Include="@(_AndroidR8JniRewrittenManagedAssembly);$(_AndroidR8JniRewriteManifest)" /> + <_AndroidR8JniMissingRewriteOutput Include="@(_AndroidR8JniExpectedRewriteOutput)" Condition="!Exists('%(Identity)')" /> + + + + + + + + + + + + + + + <_AndroidR8JniExpectedRewriteOutput Remove="@(_AndroidR8JniExpectedRewriteOutput)" /> + <_AndroidR8JniMissingRewriteOutput Remove="@(_AndroidR8JniMissingRewriteOutput)" /> + + + + Inputs="@(_LinkedAssemblyForProguard);$(_AndroidR8JniSeedMapping);$(_AndroidR8JniRewriteManifest)" + Outputs="$(_ProguardProjectConfiguration);$(_AndroidR8JniReachabilityManifest)"> + OutputFile="$(_ProguardProjectConfiguration)" + R8MappingFile="$(_AndroidR8JniSeedMapping)" + R8RewriteManifestFile="$(_AndroidR8JniRewriteManifest)" + R8ReachabilityManifestFile="$(_AndroidR8JniReachabilityManifest)" /> + + + + + + + @@ -32,20 +36,35 @@ <_PostTrimTypeMapJavaFilesList>$(_PostTrimTypeMapJavaBaseOutputDir)typemap/linked-java-files.txt <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(RuntimeIdentifiers)' != '' ">$([System.String]::Copy('$(RuntimeIdentifiers)').Split(';')[0]) <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' ">$(RuntimeIdentifier) + <_AndroidR8JniManifestBaseOutputDir>$(IntermediateOutputPath) + <_AndroidR8JniManifestBaseOutputDir Condition=" '$(RuntimeIdentifiers)' != '' and '$(_OuterIntermediateOutputPath)' == '' and '$(RuntimeIdentifier)' == '' ">$(IntermediateOutputPath)$(_PostTrimTypeMapFirstRuntimeIdentifier)/ + <_AndroidR8JniManifestBaseOutputDir Condition=" '$(RuntimeIdentifiers)' != '' and '$(_OuterIntermediateOutputPath)' == '' and '$(RuntimeIdentifier)' != '' and '$(RuntimeIdentifier)' != '$(_PostTrimTypeMapFirstRuntimeIdentifier)' ">$(IntermediateOutputPath)../$(_PostTrimTypeMapFirstRuntimeIdentifier)/ <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTypeMapJavaOutputDirectory) + <_TypeMapJavaStubsSourceDirectory Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_TypeMapJavaOutputDirectory) <_TypeMapJavaStubsSourceDirectory Condition=" '$(_TypeMapJavaStubsSourceDirectory)' == '' ">$(_TypeMapJavaOutputDirectory) <_PostTrimTrimmableTypeMapJavaStamp>$(_PostTrimTypeMapJavaBaseOutputDir)stamp/_GeneratePostTrimTrimmableTypeMapJavaSources.stamp + <_AndroidR8JniSeedDirectory Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_TypeMapBaseOutputDir)r8-jni-seed/ + <_AndroidR8JniSeedAcwMap Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)acw-map.txt + <_AndroidR8JniSeedApplicationRegistration Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)java/net/dot/android/ApplicationRegistration.java - <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true' ">$(IntermediateOutputPath)acw-map.txt - <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true' ">$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java + <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedAcwMap) + <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedApplicationRegistration) + <_PreTrimTypeMapAcwMapOutputFile Condition=" '$(_PreTrimTypeMapAcwMapOutputFile)' == '' and ('$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true') ">$(IntermediateOutputPath)acw-map.txt + <_PreTrimTypeMapApplicationRegistrationOutputFile Condition=" '$(_PreTrimTypeMapApplicationRegistrationOutputFile)' == '' and ('$(_AndroidRuntime)' != 'CoreCLR' or '$(PublishTrimmed)' != 'true') ">$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java <_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp + <_AndroidR8JniSeedMapping Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)mapping.txt + <_AndroidR8JniRewriteManifest Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniManifestBaseOutputDir)r8-jni-rewrite-manifest.txt + <_AndroidR8JniReachabilityManifest Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniManifestBaseOutputDir)r8-jni-reachability-manifest.txt + <_AndroidR8JniSeedXamarinConfiguration Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)xamarin.cfg + <_AndroidR8JniSeedJavaClassDirectory Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)classes/ + <_AndroidR8JniSeedJavaStamp Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_AndroidR8JniSeedDirectory)compile-java.stamp <_TrimmableRemoveRegisterFlag>$(_AndroidStampDirectory)_RemoveRegisterAttribute.stamp <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'CoreCLR' ">_RemoveRegisterAttributeCoreClr <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">_RemoveRegisterAttributeNativeAot @@ -54,6 +73,7 @@ Both are touched only when their producing target actually runs, so _GenerateJavaStubs stays incremental while still reacting to post-trim JCW regeneration. --> <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) + <_TrimmableJavaSourceStamp Condition=" '$(_AndroidEnableR8JniNameRewriting)' == 'true' ">$(_TrimmableTypeMapOutputStamp) <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TrimmableTypeMapOutputStamp) @@ -67,6 +87,170 @@ + + + <_AndroidR8JniSeedJavaSource Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> + <_AndroidR8JniSeedJavaSource Include="$(_AndroidR8JniSeedApplicationRegistration)" /> + + + + + + + + + + + + + + + + + + + + + <_AndroidR8JniMergedManifest>$(_TypeMapBaseOutputDir)AndroidManifest.xml + <_AndroidR8JniMergedManifest Condition=" '$(AndroidManifestMerger)' == 'manifestmerger.jar' ">$(IntermediateOutputPath)android/AndroidManifest.xml + <_AndroidR8JniManifestProguardConfiguration>$(_AndroidR8JniSeedDirectory)manifest_rules.txt + + + + + + + + + + + + + + + <_AndroidR8JniSeedClassFile Include="$(_AndroidR8JniSeedJavaClassDirectory)**\*.class" /> + <_AndroidR8JniSeedManifestProguardConfiguration + Include="$(_AndroidR8JniManifestProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' and Exists('$(_AndroidR8JniManifestProguardConfiguration)') "> + true + true + + + <_AndroidR8JniSeedProguardConfiguration Include="$(ProguardConfigFiles)" Condition=" '$(ProguardConfigFiles)' != '' " /> + <_AndroidR8JniSeedProguardConfiguration + Include="@(ProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' and '%(ProguardConfiguration.AndroidGeneratedProguardConfiguration)' != 'true' " /> + <_AndroidR8JniSeedProguardConfiguration + Include="@(_AndroidR8JniSeedManifestProguardConfiguration)" + Condition=" '$(ProguardConfigFiles)' == '' " /> + <_AndroidR8JniSeedMapDiagnostics Condition=" '$(AndroidR8IgnoreWarnings)' == 'true' " Include="warning" To="info" /> + + + + + + + + + + + + + + + + + + + + + + + + + + <_ProguardConfiguration Include="@(ProguardConfiguration)" /> @@ -3082,6 +3106,11 @@ because xbuild doesn't support framework reference assemblies. + + <_AndroidEnableR8JniNameRewriting + Condition=" '$(_AndroidEnableR8JniNameRewriting)' == '' and '$(AndroidLinkTool)' == 'r8' and '$(AndroidTypeMapImplementation)' == 'trimmable' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' and '$(AndroidEnableR8JniNameObfuscation)' == 'true' ">true + <_AndroidEnableR8JniNameRewriting Condition=" '$(_AndroidEnableR8JniNameRewriting)' == '' ">false + - + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 48e55b3edc5..422e8d82af5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1219,6 +1219,11 @@ static List ReadCallTokens (byte [] ilBytes) return ReadInlineMethodTokens (ilBytes, 0x28); } + static List ReadLoadStaticFieldAddressTokens (byte [] ilBytes) + { + return ReadInlineMethodTokens (ilBytes, 0x7F); + } + static List ReadInlineMethodTokens (byte [] ilBytes, byte opcode) { var tokens = new List (); @@ -1499,19 +1504,17 @@ public void Generate_ExportProxy_StructuredGenericArgumentThrows () } [Fact] - public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () + public void Generate_MultipleAcwProxies_DeduplicatesSignaturesButNotMethodNames () { var peers = ScanFixtures (); - // Get all ACW peers — they likely share signatures like "()V" var acwPeers = peers.Where (p => !p.DoNotGenerateAcw && p.MarshalMethods.Count > 0).ToList (); Assert.True (acwPeers.Count >= 2, "Need at least 2 ACW peers to test deduplication"); + var model = ModelBuilder.Build (acwPeers, "DedupTest.dll", "DedupTest"); using var stream = GenerateAssembly (acwPeers, "DedupTest"); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - // Count fields with HasFieldRVA — these are our UTF-8 RVA fields. - // With deduplication, common strings like "()V" should appear only once. var rvaFields = reader.FieldDefinitions .Select (h => reader.GetFieldDefinition (h)) .Where (f => (f.Attributes & FieldAttributes.HasFieldRVA) != 0) @@ -1522,24 +1525,94 @@ public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () Assert.StartsWith ("__utf8_", reader.GetString (declaringType.Name)); }); - // Collect all JNI method names and signatures from the ACW peers - var allStrings = acwPeers - .SelectMany (p => p.MarshalMethods) - .SelectMany (m => new [] { m.JniName, m.JniSignature }) - .ToList (); - var uniqueStrings = allStrings.Distinct ().Count (); - - // With dedup, RVA field count should equal unique string count, not total string count. - // Also include constructor registrations (nctor_*), so use <= for a safe assertion. - Assert.True (rvaFields.Count <= uniqueStrings + acwPeers.Count * 2, - $"Expected at most {uniqueStrings + acwPeers.Count * 2} RVA fields (unique strings + ctor names/sigs), " + - $"but found {rvaFields.Count}. Deduplication may not be working."); - - // The key assertion: fewer RVA fields than total strings means dedup is working - if (allStrings.Count > uniqueStrings) { - Assert.True (rvaFields.Count < allStrings.Count, - $"Expected fewer RVA fields ({rvaFields.Count}) than total strings ({allStrings.Count}) due to deduplication"); - } + var registrations = model.ProxyTypes.SelectMany (proxy => proxy.NativeRegistrations).ToList (); + int expectedFieldCount = registrations.Count + + registrations.Select (registration => registration.JniSignature).Distinct (StringComparer.Ordinal).Count (); + Assert.Equal (expectedFieldCount, rvaFields.Count); + } + + [Fact] + public void Generate_SharedMethodNameUsesDistinctFieldsWhileSignatureRemainsShared () + { + var first = MakeAcwPeer ("test/First", "Test.First", "TestAsm") with { + JavaConstructors = [], + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "run", + NativeCallbackName = "n_Run", + JniSignature = "()V", + ManagedMethodName = "Run", + }, + ], + }; + var second = MakeAcwPeer ("test/Second", "Test.Second", "TestAsm") with { + JavaConstructors = [], + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "run", + NativeCallbackName = "n_Run", + JniSignature = "()V", + ManagedMethodName = "Run", + }, + ], + }; + + using var stream = GenerateAssembly ([first, second], "OwnerSpecificNames"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var firstFields = ReadRegisterNativesFieldTokens (pe, reader, "Test_First_Proxy"); + var secondFields = ReadRegisterNativesFieldTokens (pe, reader, "Test_Second_Proxy"); + + Assert.Equal (2, firstFields.Count); + Assert.Equal (2, secondFields.Count); + Assert.NotEqual (firstFields [0], secondFields [0]); + Assert.Equal (firstFields [1], secondFields [1]); + } + + [Fact] + public void Generate_RegistrationWithoutWrapperDoesNotConsumeUtf8Field () + { + var peer = MakeAcwPeer ("test/Valid", "Test.Valid", "TestAsm") with { + JavaConstructors = [], + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "run", + NativeCallbackName = "n_Run", + JniSignature = "()V", + ManagedMethodName = "Run", + }, + ], + }; + var model = ModelBuilder.Build ([peer], "MissingWrapper.dll", "MissingWrapper"); + model.ProxyTypes.Single ().NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = "n_Missing", + JniSignature = "(I)V", + WrapperMethodName = "missing_uco", + WrapperTarget = new UcoWrapperTargetData { + TypeNamespace = "_TypeMap.Proxies", + TypeName = "Missing_Proxy", + MethodName = "missing_uco", + }, + }); + + using var stream = new MemoryStream (); + new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)).Emit (model, stream); + stream.Position = 0; + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.Equal (2, reader.GetTableRowCount (TableIndex.FieldRva)); + Assert.Equal (2, ReadRegisterNativesFieldTokens (pe, reader, "Test_Valid_Proxy").Count); + } + + static List ReadRegisterNativesFieldTokens (PEReader pe, MetadataReader reader, string proxyTypeName) + { + var proxy = FindProxyType (reader, proxyTypeName); + var method = reader.GetMethodDefinition (FindMethodDefinition (reader, proxy, "RegisterNatives")); + var ilBytes = pe.GetMethodBody (method.RelativeVirtualAddress).GetILBytes (); + Assert.NotNull (ilBytes); + return ReadLoadStaticFieldAddressTokens (ilBytes); } [Fact]