From 5e1adf6a48972b18a7351f2e245fe82a367d0c3e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:05:15 +0000
Subject: [PATCH 01/10] Initial plan
From 9d16443398120e86803807fe59d6c57f5cd197ef Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:33:17 +0000
Subject: [PATCH 02/10] [TrimmableTypeMap] switch package hash generation to
xxhash64
Agent-Logs-Url: https://github.com/dotnet/android/sessions/425dd1fe-e6a4-43e0-b0a9-de6b1e343c11
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 10 +++++-----
.../Scanner/JavaPeerScannerTests.EdgeCases.cs | 2 +-
.../Scanner/JavaPeerScannerTests.cs | 8 ++++----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 08ee7cd337d..df847a0d657 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1331,7 +1331,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
///
/// Compute both JNI name and compat JNI name for a type without [Register] or component Name.
- /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package.
+ /// JNI name uses XxHash64 hash of "namespace:assemblyName" for the package.
/// Compat JNI name uses the raw managed namespace (lowercased).
/// If a declaring type has [Register], its JNI name is used as prefix for both.
/// Generic backticks are replaced with _.
@@ -1345,7 +1345,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
return (name, name);
}
- var packageName = GetCrc64PackageName (ns, index.AssemblyName);
+ var packageName = GetXxHash64PackageName (ns, index.AssemblyName);
var jniName = $"{packageName}/{typeName}";
string compatName = ns.Length == 0
@@ -1360,7 +1360,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
/// registered JNI name or the outermost namespace.
/// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types
/// and if a parent has [Register] or a component attribute JNI name, uses that
- /// as prefix instead of computing CRC64 from the namespace.
+ /// as prefix instead of computing XxHash64 from the namespace.
///
static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index)
{
@@ -1465,7 +1465,7 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring
declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim ();
}
- static string GetCrc64PackageName (string ns, string assemblyName)
+ static string GetXxHash64PackageName (string ns, string assemblyName)
{
// Only Mono.Android preserves the namespace directly
if (assemblyName == "Mono.Android") {
@@ -1473,7 +1473,7 @@ static string GetCrc64PackageName (string ns, string assemblyName)
}
var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}");
- var hash = System.IO.Hashing.Crc64.Hash (data);
+ var hash = System.IO.Hashing.XxHash64.Hash (data);
return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
index b1f96d6d320..6db3290c015 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
@@ -54,7 +54,7 @@ public void Scan_EmptyNamespace_Handled ()
[InlineData ("MyApp.UnnamedActivity")]
[InlineData ("MyApp.UnregisteredClickListener")]
[InlineData ("MyApp.UnregisteredExporter")]
- public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName)
+ public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName)
{
Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName);
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index a3b725caecb..d9f47800e16 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -88,10 +88,10 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes ()
}
[Theory]
- [InlineData ("MyApp.PlainActivitySubclass", "crc64eb3df85c64aa1af6/PlainActivitySubclass")]
- [InlineData ("MyApp.UnregisteredClickListener", "crc64eb3df85c64aa1af6/UnregisteredClickListener")]
- [InlineData ("MyApp.UnregisteredExporter", "crc64eb3df85c64aa1af6/UnregisteredExporter")]
- public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, string expectedJavaName)
+ [InlineData ("MyApp.PlainActivitySubclass", "crc6403e39dfcc696a727/PlainActivitySubclass")]
+ [InlineData ("MyApp.UnregisteredClickListener", "crc6403e39dfcc696a727/UnregisteredClickListener")]
+ [InlineData ("MyApp.UnregisteredExporter", "crc6403e39dfcc696a727/UnregisteredExporter")]
+ public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName)
{
Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName);
}
From 174f08f2268a1488146bf607fe0b200ad6e7abb7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 07:37:16 +0000
Subject: [PATCH 03/10] [TrimmableTypeMap] clarify hashed package helper naming
Agent-Logs-Url: https://github.com/dotnet/android/sessions/425dd1fe-e6a4-43e0-b0a9-de6b1e343c11
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index df847a0d657..3770defe435 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1345,7 +1345,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
return (name, name);
}
- var packageName = GetXxHash64PackageName (ns, index.AssemblyName);
+ var packageName = GetHashedPackageName (ns, index.AssemblyName);
var jniName = $"{packageName}/{typeName}";
string compatName = ns.Length == 0
@@ -1465,7 +1465,7 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring
declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim ();
}
- static string GetXxHash64PackageName (string ns, string assemblyName)
+ static string GetHashedPackageName (string ns, string assemblyName)
{
// Only Mono.Android preserves the namespace directly
if (assemblyName == "Mono.Android") {
@@ -1474,6 +1474,7 @@ static string GetXxHash64PackageName (string ns, string assemblyName)
var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}");
var hash = System.IO.Hashing.XxHash64.Hash (data);
+ // Keep the historical package prefix for compatibility.
return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
}
From 10496782453f9246c2a6fb43f6eba53dae0040f0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 09:54:27 +0000
Subject: [PATCH 04/10] [TrimmableTypeMap] switch generated package prefix to
xx64
Agent-Logs-Url: https://github.com/dotnet/android/sessions/b04a9e6a-bf80-4ff1-8601-4078c145728e
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Generator/ManifestGenerator.cs | 16 ++++++++--------
.../Scanner/JavaPeerScanner.cs | 3 +--
.../ScannerComparisonTests.Helpers.cs | 6 +++---
.../ScannerComparisonTests.cs | 8 ++++----
.../Generator/TypeMapModelBuilderTests.cs | 4 ++--
.../Scanner/JavaPeerScannerTests.Behavior.cs | 2 +-
.../Scanner/JavaPeerScannerTests.EdgeCases.cs | 4 ++--
.../Scanner/JavaPeerScannerTests.cs | 6 +++---
8 files changed, 24 insertions(+), 25 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
index e83e7120ff6..aa0ad3ea57b 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
@@ -148,23 +148,23 @@ XDocument CreateDefaultManifest ()
///
/// Manifest templates may use compat JNI names (e.g., "android.apptests.App")
- /// but the trimmable path generates JCWs with CRC-based names (e.g., "crc64.../App").
+ /// but the trimmable path generates JCWs with hashed package names (e.g., "xx64.../App").
/// This method rewrites any compat name references to the actual JCW name so the
/// Android runtime can find the class.
///
void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers)
{
- // Build mapping: fully-qualified compat Java name → CRC Java name
- var compatToCrc = new Dictionary (allPeers.Count, StringComparer.Ordinal);
+ // Build mapping: fully-qualified compat Java name → hashed Java name
+ var compatToHashed = new Dictionary (allPeers.Count, StringComparer.Ordinal);
foreach (var peer in allPeers) {
string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName);
string compatName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName);
if (javaName != compatName) {
- compatToCrc [compatName] = javaName;
+ compatToHashed [compatName] = javaName;
}
}
- if (compatToCrc.Count == 0) {
+ if (compatToHashed.Count == 0) {
return;
}
@@ -173,7 +173,7 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers
// - fully qualified ("com.example.app.MainActivity")
// - relative to the manifest package, starting with '.' (".MainActivity")
// - bare, with no '.' at all ("MainActivity"), also relative to the package
- // Resolve to the fully-qualified form before the lookup, then write the CRC
+ // Resolve to the fully-qualified form before the lookup, then write the hashed
// name back so duplicate detection later in the pipeline works correctly.
var packageName = (string?) manifest.Attribute ("package") ?? "";
@@ -187,8 +187,8 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers
continue;
}
var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName);
- if (compatToCrc.TryGetValue (resolved, out var crcName)) {
- nameAttr.Value = crcName;
+ if (compatToHashed.TryGetValue (resolved, out var hashedName)) {
+ nameAttr.Value = hashedName;
}
}
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 3770defe435..4af5dc8f503 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1474,8 +1474,7 @@ static string GetHashedPackageName (string ns, string assemblyName)
var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}");
var hash = System.IO.Hashing.XxHash64.Hash (data);
- // Keep the historical package prefix for compatibility.
- return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
+ return $"xx64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
}
static string ExtractNamespace (string fullName)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs
index 5014fe28a4c..c08c1b95f7e 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs
@@ -72,12 +72,12 @@ static string[]? AllUserTypesAssemblyPaths {
}
}
- static string NormalizeCrc64 (string javaName)
+ static string NormalizeHashedPackageName (string javaName)
{
- if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) {
+ if (javaName.StartsWith ("crc64", StringComparison.Ordinal) || javaName.StartsWith ("xx64", StringComparison.Ordinal)) {
int slash = javaName.IndexOf ('/');
if (slash > 0) {
- return "crc64.../" + javaName.Substring (slash + 1);
+ return "hash.../" + javaName.Substring (slash + 1);
}
}
return javaName;
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
index fcfe51cb1df..1628af0140c 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
@@ -115,8 +115,8 @@ public void ExactTypeMap_UserTypesFixture ()
var fixturePath = paths! [0];
var (legacy, _) = ScannerRunner.RunLegacy (fixturePath);
var (newEntries, _) = ScannerRunner.RunNew (paths);
-var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList ();
-var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList ();
+var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList ();
+var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeHashedPackageName (e.JavaName) }).ToList ();
AssertTypeMapMatch (legacyNormalized, newNormalized);
}
@@ -132,9 +132,9 @@ public void ExactMarshalMethods_UserTypesFixture ()
var (_, newMethods) = ScannerRunner.RunNew (paths);
var legacyNormalized = legacyMethods
-.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value);
+.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value);
var newNormalized = newMethods
-.ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value);
+.ToDictionary (kvp => NormalizeHashedPackageName (kvp.Key), kvp => kvp.Value);
var result = MarshalMethodDiffHelper.CompareUserTypeMarshalMethods (legacyNormalized, newNormalized);
AssertNoDiffs ("MISSING from new scanner", result.Missing);
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index bd9d1ab6b0d..1efe5f88b5b 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -267,10 +267,10 @@ public void Build_PeerWithInvoker_CreatesProxy ()
[InlineData ("MyApp.UnregisteredExporter")]
[InlineData ("MyApp.UnregisteredHelper")]
[InlineData ("MyApp.DerivedFromComponentBase")]
- public void Build_Crc64RenamedPeer_StoresFinalJavaNameOnProxy (string managedName)
+ public void Build_HashedRenamedPeer_StoresFinalJavaNameOnProxy (string managedName)
{
var peer = FindFixtureByManagedName (managedName);
- Assert.StartsWith ("crc64", peer.JavaName);
+ Assert.StartsWith ("xx64", peer.JavaName);
Assert.NotEqual (peer.CompatJniName, peer.JavaName);
var model = BuildModel (new [] { peer }, "MyTypeMap");
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
index ec8ba3e7ed0..365cfaf4bef 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -114,7 +114,7 @@ public void Scan_CompatJniName (string javaName, string expectedCompat)
public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace ()
{
var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper");
- Assert.StartsWith ("crc64", unregistered.JavaName);
+ Assert.StartsWith ("xx64", unregistered.JavaName);
Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName);
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
index 6db3290c015..2a772cbe98e 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
@@ -22,7 +22,7 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered ()
Assert.Equal ("android/app/Activity", baseType.BaseJavaName);
var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase");
- Assert.StartsWith ("crc64", derived.JavaName);
+ Assert.StartsWith ("xx64", derived.JavaName);
}
[Theory]
@@ -56,7 +56,7 @@ public void Scan_EmptyNamespace_Handled ()
[InlineData ("MyApp.UnregisteredExporter")]
public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName)
{
- Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName);
+ Assert.StartsWith ("xx64", FindFixtureByManagedName (managedName).JavaName);
}
[Fact]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index d9f47800e16..81201d21439 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -88,9 +88,9 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes ()
}
[Theory]
- [InlineData ("MyApp.PlainActivitySubclass", "crc6403e39dfcc696a727/PlainActivitySubclass")]
- [InlineData ("MyApp.UnregisteredClickListener", "crc6403e39dfcc696a727/UnregisteredClickListener")]
- [InlineData ("MyApp.UnregisteredExporter", "crc6403e39dfcc696a727/UnregisteredExporter")]
+ [InlineData ("MyApp.PlainActivitySubclass", "xx6403e39dfcc696a727/PlainActivitySubclass")]
+ [InlineData ("MyApp.UnregisteredClickListener", "xx6403e39dfcc696a727/UnregisteredClickListener")]
+ [InlineData ("MyApp.UnregisteredExporter", "xx6403e39dfcc696a727/UnregisteredExporter")]
public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName)
{
Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName);
From ec9206763d6399febc7733147c8a16b4a255142c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:37:34 +0000
Subject: [PATCH 05/10] [TrimmableTypeMap] support AndroidPackageNamingPolicy
with XxHash64 default
Agent-Logs-Url: https://github.com/dotnet/android/sessions/a90e2abd-ca59-4b98-a498-64caa983b1d0
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 +
.../Scanner/JavaPeerScanner.cs | 61 ++++++++++++++++---
.../TrimmableTypeMapGenerator.cs | 9 +--
...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 +
.../Tasks/GenerateTrimmableTypeMap.cs | 4 +-
.../Xamarin.Android.Common.props.in | 2 +-
.../Generator/FixtureTestBase.cs | 20 ++++++
.../Scanner/JavaPeerScannerTests.cs | 16 +++++
8 files changed, 103 insertions(+), 13 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
index 249bdc8def1..99399656317 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
@@ -5,6 +5,7 @@
$(TargetFrameworkNETStandard)
enable
Nullable
+ true
Microsoft.Android.Sdk.TrimmableTypeMap
true
..\..\product.snk
@@ -18,6 +19,8 @@
+
+
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 4af5dc8f503..6759b46738d 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -6,6 +6,7 @@
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
+using Java.Interop.Tools.JavaCallableWrappers;
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
///
@@ -16,8 +17,19 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
///
public sealed class JavaPeerScanner : IDisposable
{
+ enum HashedPackageNamingPolicy {
+ XxHash64,
+ LowercaseCrc64,
+ }
+
readonly Dictionary assemblyCache = new (StringComparer.Ordinal);
readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new ();
+ readonly HashedPackageNamingPolicy packageNamingPolicy;
+
+ public JavaPeerScanner (string? packageNamingPolicy = null)
+ {
+ this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy);
+ }
///
/// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex.
@@ -913,7 +925,7 @@ static string GetJavaAccess (MethodAttributes access)
return registerJniName;
}
- // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
+ // Fall back to already-scanned results (component-attributed or hashed-package peers)
if (results.TryGetValue (baseTypeName, out var basePeer)) {
return basePeer.JavaName;
}
@@ -1331,12 +1343,12 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
///
/// Compute both JNI name and compat JNI name for a type without [Register] or component Name.
- /// JNI name uses XxHash64 hash of "namespace:assemblyName" for the package.
+ /// JNI name uses the selected package naming policy hash for "namespace:assemblyName".
/// Compat JNI name uses the raw managed namespace (lowercased).
/// If a declaring type has [Register], its JNI name is used as prefix for both.
/// Generic backticks are replaced with _.
///
- static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index)
+ (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index)
{
var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index);
@@ -1360,7 +1372,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
/// registered JNI name or the outermost namespace.
/// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types
/// and if a parent has [Register] or a component attribute JNI name, uses that
- /// as prefix instead of computing XxHash64 from the namespace.
+ /// as prefix instead of computing hashed package names from the namespace.
///
static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index)
{
@@ -1465,16 +1477,51 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring
declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim ();
}
- static string GetHashedPackageName (string ns, string assemblyName)
+ string GetHashedPackageName (string ns, string assemblyName)
{
// Only Mono.Android preserves the namespace directly
if (assemblyName == "Mono.Android") {
return ns.ToLowerInvariant ().Replace ('.', '/');
}
- var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}");
+ return packageNamingPolicy switch {
+ HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns + ":" + assemblyName),
+ _ => "xx64" + ToXxHash64 (ns + ":" + assemblyName),
+ };
+ }
+
+ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNamingPolicy)
+ {
+ if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) {
+ return HashedPackageNamingPolicy.LowercaseCrc64;
+ }
+
+ return HashedPackageNamingPolicy.XxHash64;
+ }
+
+ static string ToLegacyCrc64 (string value)
+ {
+ var data = System.Text.Encoding.UTF8.GetBytes (value);
+ var hash = Crc64Helper.Compute (data);
+ var buf = new char [hash.Length * 2];
+ int i = 0;
+ foreach (var b in hash) {
+ buf [i++] = GetHexLowerChar (b >> 4);
+ buf [i++] = GetHexLowerChar (b & 0xF);
+ }
+ return new string (buf);
+ }
+
+ static string ToXxHash64 (string value)
+ {
+ var data = System.Text.Encoding.UTF8.GetBytes (value);
var hash = System.IO.Hashing.XxHash64.Hash (data);
- return $"xx64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
+ return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ();
+ }
+
+ static char GetHexLowerChar (int value)
+ {
+ return (char) (value < 10 ? ('0' + value) : ('a' + value - 10));
}
static string ExtractNamespace (string fullName)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
index 5d14490503a..d877a70b028 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
@@ -27,13 +27,14 @@ public TrimmableTypeMapResult Execute (
HashSet frameworkAssemblyNames,
bool useSharedTypemapUniverse = false,
ManifestConfig? manifestConfig = null,
- XDocument? manifestTemplate = null)
+ XDocument? manifestTemplate = null,
+ string? packageNamingPolicy = null)
{
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
_ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
_ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames));
- var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies);
+ var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy);
if (allPeers.Count == 0) {
logger.LogNoJavaPeerTypesFound ();
return new TrimmableTypeMapResult ([], [], allPeers);
@@ -104,9 +105,9 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes
return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []);
}
- (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
+ (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy)
{
- using var scanner = new JavaPeerScanner ();
+ using var scanner = new JavaPeerScanner (packageNamingPolicy);
var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count);
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
index 40a2c1519c3..9ab79fbc2b3 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
@@ -98,6 +98,7 @@
Debug="$(AndroidIncludeDebugSymbols)"
NeedsInternet="$(AndroidNeedsInternetPermission)"
EmbedAssemblies="$(EmbedAssembliesIntoApk)"
+ PackageNamingPolicy="$(AndroidPackageNamingPolicy)"
ManifestPlaceholders="$(AndroidManifestPlaceholders)"
CheckedBuild="$(_AndroidCheckedBuild)"
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
index f80a14157f1..cb4d51021d3 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
@@ -68,6 +68,7 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
public bool Debug { get; set; }
public bool NeedsInternet { get; set; }
public bool EmbedAssemblies { get; set; }
+ public string? PackageNamingPolicy { get; set; }
public string? ManifestPlaceholders { get; set; }
public string? CheckedBuild { get; set; }
public string? ApplicationJavaClass { get; set; }
@@ -131,7 +132,8 @@ public override bool RunTask ()
frameworkAssemblyNames,
useSharedTypemapUniverse: !Debug,
manifestConfig,
- manifestTemplate);
+ manifestTemplate,
+ PackageNamingPolicy);
GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths);
GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources);
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
index 02589ee9384..95727a3c42d 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
@@ -21,7 +21,7 @@
{abi}{versionCode:D5}
UpdateGeneratedFiles
True
- LowercaseCrc64
+ XxHash64
False
@BUNDLETOOL_VERSION@
<_XamarinAndroidMSBuildDirectory>$(MSBuildThisFileDirectory)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
index 98b28be0591..bf997025616 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -36,6 +36,18 @@ private protected static string TestFixtureAssemblyPath {
private protected static List ScanFixtures () => _cachedScanResult.Value.peers;
+ private protected static List ScanFixtures (string packageNamingPolicy)
+ {
+ using var scanner = new JavaPeerScanner (packageNamingPolicy);
+ var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
+ var mdReader = peReader.GetMetadataReader ();
+ var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name);
+ var assemblies = new [] { (assemblyName, peReader) };
+ var peers = scanner.Scan (assemblies);
+ peReader.Dispose ();
+ return peers;
+ }
+
private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo;
private protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
@@ -54,6 +66,14 @@ private protected static JavaPeerInfo FindFixtureByManagedName (string managedNa
return peer;
}
+ private protected static JavaPeerInfo FindFixtureByManagedName (string managedName, string packageNamingPolicy)
+ {
+ var peers = ScanFixtures (packageNamingPolicy);
+ var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
static (string ns, string shortName) ParseManagedTypeName (string managedName)
{
var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index 81201d21439..3fd457f7346 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
+using Java.Interop.Tools.JavaCallableWrappers;
using Xunit;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
@@ -95,4 +97,18 @@ public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, str
{
Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName);
}
+
+ [Fact]
+ public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash ()
+ {
+ const string managedName = "MyApp.PlainActivitySubclass";
+ var withXxHash64 = FindFixtureByManagedName (managedName).JavaName;
+ var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName;
+
+ var data = Encoding.UTF8.GetBytes ("MyApp:TestFixtures");
+ var expectedHash = string.Concat (Crc64Helper.Compute (data).Select (b => b.ToString ("x2")));
+ Assert.Equal ($"crc64{expectedHash}/PlainActivitySubclass", withCrc64);
+ Assert.StartsWith ("xx64", withXxHash64);
+ Assert.NotEqual (withXxHash64, withCrc64);
+ }
}
From 8d5709b9d5c15c37dcb7a06d7b16c6941ae17923 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 24 Apr 2026 08:13:27 +0000
Subject: [PATCH 06/10] [TrimmableTypeMap] reduce hashing allocations in
JavaPeerScanner
Agent-Logs-Url: https://github.com/dotnet/android/sessions/9fd6ed14-5c7b-463e-b1b0-ebefd496f067
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 102 ++++++++++++++----
1 file changed, 84 insertions(+), 18 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 6759b46738d..caab3320fde 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -1485,8 +1486,8 @@ string GetHashedPackageName (string ns, string assemblyName)
}
return packageNamingPolicy switch {
- HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns + ":" + assemblyName),
- _ => "xx64" + ToXxHash64 (ns + ":" + assemblyName),
+ HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns, assemblyName),
+ _ => "xx64" + ToXxHash64 (ns, assemblyName),
};
}
@@ -1499,29 +1500,94 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming
return HashedPackageNamingPolicy.XxHash64;
}
- static string ToLegacyCrc64 (string value)
+ static string ToLegacyCrc64 (string ns, string assemblyName)
{
- var data = System.Text.Encoding.UTF8.GetBytes (value);
- var hash = Crc64Helper.Compute (data);
- var buf = new char [hash.Length * 2];
- int i = 0;
- foreach (var b in hash) {
- buf [i++] = GetHexLowerChar (b >> 4);
- buf [i++] = GetHexLowerChar (b & 0xF);
- }
- return new string (buf);
+ int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ ulong crc = ulong.MaxValue;
+ ulong length = 0;
+ Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
+ Span hash = stackalloc byte [8];
+ WriteUInt64LittleEndian (hash, crc ^ length);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
+
+ static string ToXxHash64 (string ns, string assemblyName)
+ {
+ int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ Span hash = stackalloc byte [8];
+ System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
+
+ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
+ {
+ return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName);
+ }
+
+ static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination)
+ {
+ int bytesWritten = 0;
+ fixed (char* nsPtr = ns)
+ fixed (byte* destinationPtr = destination) {
+ bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length);
+ }
+
+ destination [bytesWritten++] = (byte) ':';
+
+ fixed (char* assemblyNamePtr = assemblyName)
+ fixed (byte* destinationPtr = destination) {
+ bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten);
+ }
+
+ return bytesWritten;
+ }
+
+ static string ToHexString (ReadOnlySpan hash, bool lowercase)
+ {
+ const int maxStackCharLength = 128;
+ int charLength = hash.Length * 2;
+ Span chars = charLength <= maxStackCharLength
+ ? stackalloc char [charLength]
+ : new char [charLength];
+
+ for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) {
+ byte b = hash [i];
+ chars [j] = GetHexValue (b / 16, lowercase);
+ chars [j + 1] = GetHexValue (b % 16, lowercase);
+ }
+
+ return ((ReadOnlySpan) chars).ToString ();
}
- static string ToXxHash64 (string value)
+ static void WriteUInt64LittleEndian (Span destination, ulong value)
{
- var data = System.Text.Encoding.UTF8.GetBytes (value);
- var hash = System.IO.Hashing.XxHash64.Hash (data);
- return BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ();
+ destination [0] = (byte) value;
+ destination [1] = (byte) (value >> 8);
+ destination [2] = (byte) (value >> 16);
+ destination [3] = (byte) (value >> 24);
+ destination [4] = (byte) (value >> 32);
+ destination [5] = (byte) (value >> 40);
+ destination [6] = (byte) (value >> 48);
+ destination [7] = (byte) (value >> 56);
}
- static char GetHexLowerChar (int value)
+ static char GetHexValue (int value, bool lowercase)
{
- return (char) (value < 10 ? ('0' + value) : ('a' + value - 10));
+ return (char) (value < 10
+ ? value + '0'
+ : value - 10 + (lowercase ? 'a' : 'A'));
}
static string ExtractNamespace (string fullName)
From 05cebc7c2354556a1cc2e00025e046dc5a8fbd4b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 24 Apr 2026 10:19:50 +0000
Subject: [PATCH 07/10] [TrimmableTypeMap] extract scanner hashing helper and
scope defaults
Agent-Logs-Url: https://github.com/dotnet/android/sessions/144010de-77de-456e-aee5-4db9e9a1a5cb
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 95 +-----------------
.../Scanner/ScannerHashingHelper.cs | 98 +++++++++++++++++++
...soft.Android.Sdk.TypeMap.Trimmable.targets | 4 +-
.../Xamarin.Android.Common.props.in | 3 +-
.../Scanner/JavaPeerScannerTests.cs | 6 +-
.../Scanner/ScannerHashingHelperTests.cs | 21 ++++
6 files changed, 127 insertions(+), 100 deletions(-)
create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index caab3320fde..e1de513ec6b 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1,5 +1,4 @@
using System;
-using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -1486,8 +1485,8 @@ string GetHashedPackageName (string ns, string assemblyName)
}
return packageNamingPolicy switch {
- HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ToLegacyCrc64 (ns, assemblyName),
- _ => "xx64" + ToXxHash64 (ns, assemblyName),
+ HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName),
+ _ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName),
};
}
@@ -1500,96 +1499,6 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming
return HashedPackageNamingPolicy.XxHash64;
}
- static string ToLegacyCrc64 (string ns, string assemblyName)
- {
- int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] rented = ArrayPool.Shared.Rent (byteCount);
- try {
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
- ulong crc = ulong.MaxValue;
- ulong length = 0;
- Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
- Span hash = stackalloc byte [8];
- WriteUInt64LittleEndian (hash, crc ^ length);
- return ToHexString (hash, lowercase: true);
- } finally {
- ArrayPool.Shared.Return (rented);
- }
- }
-
- static string ToXxHash64 (string ns, string assemblyName)
- {
- int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] rented = ArrayPool.Shared.Rent (byteCount);
- try {
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
- Span hash = stackalloc byte [8];
- System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash);
- return ToHexString (hash, lowercase: true);
- } finally {
- ArrayPool.Shared.Return (rented);
- }
- }
-
- static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
- {
- return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName);
- }
-
- static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination)
- {
- int bytesWritten = 0;
- fixed (char* nsPtr = ns)
- fixed (byte* destinationPtr = destination) {
- bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length);
- }
-
- destination [bytesWritten++] = (byte) ':';
-
- fixed (char* assemblyNamePtr = assemblyName)
- fixed (byte* destinationPtr = destination) {
- bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten);
- }
-
- return bytesWritten;
- }
-
- static string ToHexString (ReadOnlySpan hash, bool lowercase)
- {
- const int maxStackCharLength = 128;
- int charLength = hash.Length * 2;
- Span chars = charLength <= maxStackCharLength
- ? stackalloc char [charLength]
- : new char [charLength];
-
- for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) {
- byte b = hash [i];
- chars [j] = GetHexValue (b / 16, lowercase);
- chars [j + 1] = GetHexValue (b % 16, lowercase);
- }
-
- return ((ReadOnlySpan) chars).ToString ();
- }
-
- static void WriteUInt64LittleEndian (Span destination, ulong value)
- {
- destination [0] = (byte) value;
- destination [1] = (byte) (value >> 8);
- destination [2] = (byte) (value >> 16);
- destination [3] = (byte) (value >> 24);
- destination [4] = (byte) (value >> 32);
- destination [5] = (byte) (value >> 40);
- destination [6] = (byte) (value >> 48);
- destination [7] = (byte) (value >> 56);
- }
-
- static char GetHexValue (int value, bool lowercase)
- {
- return (char) (value < 10
- ? value + '0'
- : value - 10 + (lowercase ? 'a' : 'A'));
- }
-
static string ExtractNamespace (string fullName)
{
// Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer")
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
new file mode 100644
index 00000000000..c59b1956589
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Buffers;
+using Java.Interop.Tools.JavaCallableWrappers;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal static class ScannerHashingHelper
+{
+ internal static string ToLegacyCrc64 (string ns, string assemblyName)
+ {
+ int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ ulong crc = ulong.MaxValue;
+ ulong length = 0;
+ Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
+ Span hash = stackalloc byte [8];
+ WriteUInt64LittleEndian (hash, crc ^ length);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
+
+ internal static string ToXxHash64 (string ns, string assemblyName)
+ {
+ int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ Span hash = stackalloc byte [8];
+ System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
+
+ static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
+ {
+ return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName);
+ }
+
+ static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span destination)
+ {
+ int bytesWritten = 0;
+ fixed (char* nsPtr = ns)
+ fixed (byte* destinationPtr = destination) {
+ bytesWritten += System.Text.Encoding.UTF8.GetBytes (nsPtr, ns.Length, destinationPtr, destination.Length);
+ }
+
+ destination [bytesWritten++] = (byte) ':';
+
+ fixed (char* assemblyNamePtr = assemblyName)
+ fixed (byte* destinationPtr = destination) {
+ bytesWritten += System.Text.Encoding.UTF8.GetBytes (assemblyNamePtr, assemblyName.Length, destinationPtr + bytesWritten, destination.Length - bytesWritten);
+ }
+
+ return bytesWritten;
+ }
+
+ static string ToHexString (ReadOnlySpan hash, bool lowercase)
+ {
+ const int maxStackCharLength = 128;
+ int charLength = hash.Length * 2;
+ Span chars = charLength <= maxStackCharLength
+ ? stackalloc char [charLength]
+ : new char [charLength];
+
+ for (int i = 0, j = 0; i < hash.Length; i += 1, j += 2) {
+ byte b = hash [i];
+ chars [j] = GetHexValue (b / 16, lowercase);
+ chars [j + 1] = GetHexValue (b % 16, lowercase);
+ }
+
+ return ((ReadOnlySpan) chars).ToString ();
+ }
+
+ static void WriteUInt64LittleEndian (Span destination, ulong value)
+ {
+ destination [0] = (byte) value;
+ destination [1] = (byte) (value >> 8);
+ destination [2] = (byte) (value >> 16);
+ destination [3] = (byte) (value >> 24);
+ destination [4] = (byte) (value >> 32);
+ destination [5] = (byte) (value >> 40);
+ destination [6] = (byte) (value >> 48);
+ destination [7] = (byte) (value >> 56);
+ }
+
+ static char GetHexValue (int value, bool lowercase)
+ {
+ return (char) (value < 10
+ ? value + '0'
+ : value - 10 + (lowercase ? 'a' : 'A'));
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
index 9ab79fbc2b3..b8563874aeb 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
@@ -11,6 +11,8 @@
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps
+ <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy)
+ <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">XxHash64
true
@@ -98,7 +100,7 @@
Debug="$(AndroidIncludeDebugSymbols)"
NeedsInternet="$(AndroidNeedsInternetPermission)"
EmbedAssemblies="$(EmbedAssembliesIntoApk)"
- PackageNamingPolicy="$(AndroidPackageNamingPolicy)"
+ PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)"
ManifestPlaceholders="$(AndroidManifestPlaceholders)"
CheckedBuild="$(_AndroidCheckedBuild)"
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
index 95727a3c42d..b13f3abe411 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.props.in
@@ -21,7 +21,8 @@
{abi}{versionCode:D5}
UpdateGeneratedFiles
True
- XxHash64
+ <_AndroidPackageNamingPolicySetByUser Condition=" '$(AndroidPackageNamingPolicy)' != '' ">true
+ LowercaseCrc64
False
@BUNDLETOOL_VERSION@
<_XamarinAndroidMSBuildDirectory>$(MSBuildThisFileDirectory)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index 3fd457f7346..c27f9b0c515 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using Java.Interop.Tools.JavaCallableWrappers;
using Xunit;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
@@ -105,9 +103,7 @@ public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash ()
var withXxHash64 = FindFixtureByManagedName (managedName).JavaName;
var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName;
- var data = Encoding.UTF8.GetBytes ("MyApp:TestFixtures");
- var expectedHash = string.Concat (Crc64Helper.Compute (data).Select (b => b.ToString ("x2")));
- Assert.Equal ($"crc64{expectedHash}/PlainActivitySubclass", withCrc64);
+ Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withCrc64);
Assert.StartsWith ("xx64", withXxHash64);
Assert.NotEqual (withXxHash64, withCrc64);
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
new file mode 100644
index 00000000000..a17c6924ea5
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
@@ -0,0 +1,21 @@
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class ScannerHashingHelperTests
+{
+ [Theory]
+ [InlineData ("MyApp", "TestFixtures", "ec59e927bc71f4d8")]
+ [InlineData ("System.Collections.Generic", "My.Assembly", "9ff866e93b19f500")]
+ [InlineData ("Hello", "World", "f6bdbfa73a558c54")]
+ public void ToLegacyCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected)
+ {
+ Assert.Equal (expected, ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName));
+ }
+
+ [Fact]
+ public void ToXxHash64_KnownInput_HasStableOutput ()
+ {
+ Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures"));
+ }
+}
From e9ae72c9ba148539fc7fd3739fd77b42e5cc8319 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 27 Apr 2026 09:50:07 +0000
Subject: [PATCH 08/10] [TrimmableTypeMap] default trimmable policy to Crc64
and keep LowercaseCrc64 legacy
Agent-Logs-Url: https://github.com/dotnet/android/sessions/fdb58e79-42f0-4d7d-8180-2b58c52ae3f8
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Generator/ManifestGenerator.cs | 2 +-
.../Scanner/JavaPeerScanner.cs | 7 ++++-
.../Scanner/ScannerHashingHelper.cs | 14 ++++++++++
...soft.Android.Sdk.TypeMap.Trimmable.targets | 2 +-
.../Generator/TypeMapModelBuilderTests.cs | 2 +-
.../Scanner/JavaPeerScannerTests.Behavior.cs | 2 +-
.../Scanner/JavaPeerScannerTests.EdgeCases.cs | 4 +--
.../Scanner/JavaPeerScannerTests.cs | 28 ++++++++++++-------
.../Scanner/ScannerHashingHelperTests.cs | 6 ++++
9 files changed, 50 insertions(+), 17 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
index aa0ad3ea57b..553ba08972f 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
@@ -148,7 +148,7 @@ XDocument CreateDefaultManifest ()
///
/// Manifest templates may use compat JNI names (e.g., "android.apptests.App")
- /// but the trimmable path generates JCWs with hashed package names (e.g., "xx64.../App").
+ /// but the trimmable path generates JCWs with hashed package names (e.g., "crc64.../App").
/// This method rewrites any compat name references to the actual JCW name so the
/// Android runtime can find the class.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index e1de513ec6b..5146ba0398e 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -18,6 +18,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
public sealed class JavaPeerScanner : IDisposable
{
enum HashedPackageNamingPolicy {
+ Crc64,
XxHash64,
LowercaseCrc64,
}
@@ -1486,6 +1487,7 @@ string GetHashedPackageName (string ns, string assemblyName)
return packageNamingPolicy switch {
HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName),
+ HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
_ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName),
};
}
@@ -1495,8 +1497,11 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming
if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) {
return HashedPackageNamingPolicy.LowercaseCrc64;
}
+ if (string.Equals (packageNamingPolicy, "XxHash64", StringComparison.OrdinalIgnoreCase)) {
+ return HashedPackageNamingPolicy.XxHash64;
+ }
- return HashedPackageNamingPolicy.XxHash64;
+ return HashedPackageNamingPolicy.Crc64;
}
static string ExtractNamespace (string fullName)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
index c59b1956589..f30b5b41bab 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
@@ -37,6 +37,20 @@ internal static string ToXxHash64 (string ns, string assemblyName)
}
}
+ internal static string ToCrc64 (string ns, string assemblyName)
+ {
+ int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ Span hash = stackalloc byte [8];
+ System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
+ }
+
static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
{
return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName);
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
index 16d0e00c278..04135852229 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets
@@ -12,7 +12,7 @@
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps
<_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy)
- <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">XxHash64
+ <_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">Crc64
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index 1efe5f88b5b..b8fc18064d0 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -270,7 +270,7 @@ public void Build_PeerWithInvoker_CreatesProxy ()
public void Build_HashedRenamedPeer_StoresFinalJavaNameOnProxy (string managedName)
{
var peer = FindFixtureByManagedName (managedName);
- Assert.StartsWith ("xx64", peer.JavaName);
+ Assert.StartsWith ("crc64", peer.JavaName);
Assert.NotEqual (peer.CompatJniName, peer.JavaName);
var model = BuildModel (new [] { peer }, "MyTypeMap");
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
index 365cfaf4bef..ec8ba3e7ed0 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -114,7 +114,7 @@ public void Scan_CompatJniName (string javaName, string expectedCompat)
public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace ()
{
var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper");
- Assert.StartsWith ("xx64", unregistered.JavaName);
+ Assert.StartsWith ("crc64", unregistered.JavaName);
Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName);
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
index 2a772cbe98e..6db3290c015 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
@@ -22,7 +22,7 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered ()
Assert.Equal ("android/app/Activity", baseType.BaseJavaName);
var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase");
- Assert.StartsWith ("xx64", derived.JavaName);
+ Assert.StartsWith ("crc64", derived.JavaName);
}
[Theory]
@@ -56,7 +56,7 @@ public void Scan_EmptyNamespace_Handled ()
[InlineData ("MyApp.UnregisteredExporter")]
public void Scan_UnregisteredType_DiscoveredWithHashedName (string managedName)
{
- Assert.StartsWith ("xx64", FindFixtureByManagedName (managedName).JavaName);
+ Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName);
}
[Fact]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index c27f9b0c515..364784095d3 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -88,23 +88,31 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes ()
}
[Theory]
- [InlineData ("MyApp.PlainActivitySubclass", "xx6403e39dfcc696a727/PlainActivitySubclass")]
- [InlineData ("MyApp.UnregisteredClickListener", "xx6403e39dfcc696a727/UnregisteredClickListener")]
- [InlineData ("MyApp.UnregisteredExporter", "xx6403e39dfcc696a727/UnregisteredExporter")]
+ [InlineData ("MyApp.PlainActivitySubclass", "PlainActivitySubclass")]
+ [InlineData ("MyApp.UnregisteredClickListener", "UnregisteredClickListener")]
+ [InlineData ("MyApp.UnregisteredExporter", "UnregisteredExporter")]
public void Scan_UnregisteredType_UsesHashedPackageName (string managedName, string expectedJavaName)
{
- Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName);
+ Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/{expectedJavaName}",
+ FindFixtureByManagedName (managedName).JavaName);
}
[Fact]
- public void Scan_UnregisteredType_LowercaseCrc64Policy_UsesLegacyCrc64Hash ()
+ public void Scan_UnregisteredType_Crc64Default_DiffersFromLegacyLowercaseCrc64Policy ()
{
const string managedName = "MyApp.PlainActivitySubclass";
- var withXxHash64 = FindFixtureByManagedName (managedName).JavaName;
- var withCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName;
+ var withCrc64 = FindFixtureByManagedName (managedName).JavaName;
+ var withLowercaseCrc64 = FindFixtureByManagedName (managedName, "LowercaseCrc64").JavaName;
- Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withCrc64);
- Assert.StartsWith ("xx64", withXxHash64);
- Assert.NotEqual (withXxHash64, withCrc64);
+ Assert.Equal ($"crc64{ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures")}/PlainActivitySubclass", withCrc64);
+ Assert.Equal ("crc64ec59e927bc71f4d8/PlainActivitySubclass", withLowercaseCrc64);
+ Assert.NotEqual (withCrc64, withLowercaseCrc64);
+ }
+
+ [Fact]
+ public void Scan_UnregisteredType_XxHash64Policy_UsesXxHash64Hash ()
+ {
+ var withXxHash64 = FindFixtureByManagedName ("MyApp.PlainActivitySubclass", "XxHash64").JavaName;
+ Assert.Equal ("xx6403e39dfcc696a727/PlainActivitySubclass", withXxHash64);
}
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
index a17c6924ea5..81bcdc88964 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
@@ -18,4 +18,10 @@ public void ToXxHash64_KnownInput_HasStableOutput ()
{
Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures"));
}
+
+ [Fact]
+ public void ToCrc64_KnownInput_HasStableOutput ()
+ {
+ Assert.Equal ("eb3df85c64aa1af6", ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures"));
+ }
}
From fa8493df91a76213875e38c90980b421f8e1b66c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 27 Apr 2026 11:10:04 +0000
Subject: [PATCH 09/10] [TrimmableTypeMap] remove xxhash64 path and improve
Crc64 hashing buffers
Agent-Logs-Url: https://github.com/dotnet/android/sessions/3701f7a2-7d92-4e63-8d34-f4723682c1ba
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 8 +--
.../Scanner/ScannerHashingHelper.cs | 53 ++++++-------------
.../Scanner/JavaPeerScannerTests.cs | 6 ---
.../Scanner/ScannerHashingHelperTests.cs | 15 +++---
4 files changed, 24 insertions(+), 58 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 5146ba0398e..5cc08abf860 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -19,7 +19,6 @@ public sealed class JavaPeerScanner : IDisposable
{
enum HashedPackageNamingPolicy {
Crc64,
- XxHash64,
LowercaseCrc64,
}
@@ -1487,8 +1486,7 @@ string GetHashedPackageName (string ns, string assemblyName)
return packageNamingPolicy switch {
HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName),
- HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
- _ => "xx64" + ScannerHashingHelper.ToXxHash64 (ns, assemblyName),
+ _ => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
};
}
@@ -1497,10 +1495,6 @@ static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNaming
if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) {
return HashedPackageNamingPolicy.LowercaseCrc64;
}
- if (string.Equals (packageNamingPolicy, "XxHash64", StringComparison.OrdinalIgnoreCase)) {
- return HashedPackageNamingPolicy.XxHash64;
- }
-
return HashedPackageNamingPolicy.Crc64;
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
index f30b5b41bab..b855d02fc90 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
@@ -1,5 +1,4 @@
using System;
-using System.Buffers;
using Java.Interop.Tools.JavaCallableWrappers;
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
@@ -9,46 +8,28 @@ internal static class ScannerHashingHelper
internal static string ToLegacyCrc64 (string ns, string assemblyName)
{
int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] rented = ArrayPool.Shared.Rent (byteCount);
- try {
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
- ulong crc = ulong.MaxValue;
- ulong length = 0;
- Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
- Span hash = stackalloc byte [8];
- WriteUInt64LittleEndian (hash, crc ^ length);
- return ToHexString (hash, lowercase: true);
- } finally {
- ArrayPool.Shared.Return (rented);
- }
- }
-
- internal static string ToXxHash64 (string ns, string assemblyName)
- {
- int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] rented = ArrayPool.Shared.Rent (byteCount);
- try {
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
- Span hash = stackalloc byte [8];
- System.IO.Hashing.XxHash64.Hash (rented.AsSpan (0, bytesWritten), hash);
- return ToHexString (hash, lowercase: true);
- } finally {
- ArrayPool.Shared.Return (rented);
- }
+ byte[] utf8Buffer = new byte [byteCount];
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer);
+ ulong crc = ulong.MaxValue;
+ ulong length = 0;
+ Crc64Helper.HashCore (utf8Buffer, 0, bytesWritten, ref crc, ref length);
+ Span hash = stackalloc byte [8];
+ WriteUInt64LittleEndian (hash, crc ^ length);
+ return ToHexString (hash, lowercase: true);
}
internal static string ToCrc64 (string ns, string assemblyName)
{
+ const int stackallocThresholdBytes = 256;
int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] rented = ArrayPool.Shared.Rent (byteCount);
- try {
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
- Span hash = stackalloc byte [8];
- System.IO.Hashing.Crc64.Hash (rented.AsSpan (0, bytesWritten), hash);
- return ToHexString (hash, lowercase: true);
- } finally {
- ArrayPool.Shared.Return (rented);
- }
+ Span utf8Buffer = byteCount <= stackallocThresholdBytes
+ ? stackalloc byte [stackallocThresholdBytes]
+ : new byte [byteCount];
+
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount));
+ Span hash = stackalloc byte [8];
+ System.IO.Hashing.Crc64.Hash (utf8Buffer.Slice (0, bytesWritten), hash);
+ return ToHexString (hash, lowercase: true);
}
static int GetNamespaceAssemblyUtf8ByteCount (string ns, string assemblyName)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index 364784095d3..8be0d078cf4 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -109,10 +109,4 @@ public void Scan_UnregisteredType_Crc64Default_DiffersFromLegacyLowercaseCrc64Po
Assert.NotEqual (withCrc64, withLowercaseCrc64);
}
- [Fact]
- public void Scan_UnregisteredType_XxHash64Policy_UsesXxHash64Hash ()
- {
- var withXxHash64 = FindFixtureByManagedName ("MyApp.PlainActivitySubclass", "XxHash64").JavaName;
- Assert.Equal ("xx6403e39dfcc696a727/PlainActivitySubclass", withXxHash64);
- }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
index 81bcdc88964..d6fdb902b7a 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ScannerHashingHelperTests.cs
@@ -13,15 +13,12 @@ public void ToLegacyCrc64_KnownInputs_HaveStableOutput (string ns, string assemb
Assert.Equal (expected, ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName));
}
- [Fact]
- public void ToXxHash64_KnownInput_HasStableOutput ()
- {
- Assert.Equal ("03e39dfcc696a727", ScannerHashingHelper.ToXxHash64 ("MyApp", "TestFixtures"));
- }
-
- [Fact]
- public void ToCrc64_KnownInput_HasStableOutput ()
+ [Theory]
+ [InlineData ("MyApp", "TestFixtures", "eb3df85c64aa1af6")]
+ [InlineData ("System.Collections.Generic", "My.Assembly", "403b37c9b3a5014d")]
+ [InlineData ("Hello", "World", "4f2a517f331e7a2c")]
+ public void ToCrc64_KnownInputs_HaveStableOutput (string ns, string assemblyName, string expected)
{
- Assert.Equal ("eb3df85c64aa1af6", ScannerHashingHelper.ToCrc64 ("MyApp", "TestFixtures"));
+ Assert.Equal (expected, ScannerHashingHelper.ToCrc64 (ns, assemblyName));
}
}
From ab689ec8b36d6d7e361ea1adb1e0fd1b65b36657 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 27 Apr 2026 11:14:32 +0000
Subject: [PATCH 10/10] [TrimmableTypeMap] clarify Crc64 switch and keep pooled
legacy buffering
Agent-Logs-Url: https://github.com/dotnet/android/sessions/3701f7a2-7d92-4e63-8d34-f4723682c1ba
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
---
.../Scanner/JavaPeerScanner.cs | 3 ++-
.../Scanner/ScannerHashingHelper.cs | 21 ++++++++++++-------
2 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 5cc08abf860..0f2e782a200 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1486,7 +1486,8 @@ string GetHashedPackageName (string ns, string assemblyName)
return packageNamingPolicy switch {
HashedPackageNamingPolicy.LowercaseCrc64 => "crc64" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName),
- _ => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
+ HashedPackageNamingPolicy.Crc64 => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
+ _ => throw new InvalidOperationException ($"Unsupported package naming policy: {packageNamingPolicy}"),
};
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
index b855d02fc90..b57c78bbb82 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ScannerHashingHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using Java.Interop.Tools.JavaCallableWrappers;
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
@@ -8,14 +9,18 @@ internal static class ScannerHashingHelper
internal static string ToLegacyCrc64 (string ns, string assemblyName)
{
int byteCount = GetNamespaceAssemblyUtf8ByteCount (ns, assemblyName);
- byte[] utf8Buffer = new byte [byteCount];
- int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer);
- ulong crc = ulong.MaxValue;
- ulong length = 0;
- Crc64Helper.HashCore (utf8Buffer, 0, bytesWritten, ref crc, ref length);
- Span hash = stackalloc byte [8];
- WriteUInt64LittleEndian (hash, crc ^ length);
- return ToHexString (hash, lowercase: true);
+ byte[] rented = ArrayPool.Shared.Rent (byteCount);
+ try {
+ int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, rented.AsSpan (0, byteCount));
+ ulong crc = ulong.MaxValue;
+ ulong length = 0;
+ Crc64Helper.HashCore (rented, 0, bytesWritten, ref crc, ref length);
+ Span hash = stackalloc byte [8];
+ WriteUInt64LittleEndian (hash, crc ^ length);
+ return ToHexString (hash, lowercase: true);
+ } finally {
+ ArrayPool.Shared.Return (rented);
+ }
}
internal static string ToCrc64 (string ns, string assemblyName)