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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,23 +148,23 @@ XDocument CreateDefaultManifest ()

/// <summary>
/// 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.
/// </summary>
void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> allPeers)
{
// Build mapping: fully-qualified compat Java name → CRC Java name
var compatToCrc = new Dictionary<string, string> (allPeers.Count, StringComparer.Ordinal);
// Build mapping: fully-qualified compat Java name → hashed Java name
var compatToHashed = new Dictionary<string, string> (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;
}

Expand All @@ -173,7 +173,7 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> 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") ?? "";

Expand All @@ -187,8 +187,8 @@ void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> 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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<TargetFramework>$(TargetFrameworkNETStandard)</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RootNamespace>Microsoft.Android.Sdk.TrimmableTypeMap</RootNamespace>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
Expand All @@ -18,6 +19,8 @@

<ItemGroup>
<Compile Include="..\..\src-ThirdParty\System.Runtime.CompilerServices\CompilerFeaturePolyfills.cs" Link="CompilerFeaturePolyfills.cs" />
<Compile Include="..\..\external\Java.Interop\src\Java.Interop.Tools.JavaCallableWrappers\Java.Interop.Tools.JavaCallableWrappers\Crc64Helper.cs" Link="Crc64Helper.cs" />
<Compile Include="..\..\external\Java.Interop\src\Java.Interop.Tools.JavaCallableWrappers\Java.Interop.Tools.JavaCallableWrappers\Crc64.Table.cs" Link="Crc64.Table.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand All @@ -16,8 +17,19 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// </summary>
public sealed class JavaPeerScanner : IDisposable
{
enum HashedPackageNamingPolicy {
XxHash64,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I think we should drop xxhash64, it doesn't bring any value after all. Let's keep the compat crc64 + the new "fast" crc64

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fa8493d. I removed the trimmable XxHash64 path and now keep only legacy LowercaseCrc64 + fast Crc64. No UI changes (screenshot N/A).

LowercaseCrc64,
}

readonly Dictionary<string, AssemblyIndex> 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);
}

/// <summary>
/// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1331,12 +1343,12 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)

/// <summary>
/// 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 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 _.
/// </summary>
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);

Expand All @@ -1345,7 +1357,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
return (name, name);
}

var packageName = GetCrc64PackageName (ns, index.AssemblyName);
var packageName = GetHashedPackageName (ns, index.AssemblyName);
var jniName = $"{packageName}/{typeName}";

string compatName = ns.Length == 0
Expand All @@ -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 CRC64 from the namespace.
/// as prefix instead of computing hashed package names from the namespace.
/// </summary>
static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index)
{
Expand Down Expand Up @@ -1465,16 +1477,51 @@ 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)
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}");
var hash = System.IO.Hashing.Crc64.Hash (data);
return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
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 BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot we should take inspiration from dotnet/android-tools#335 and avoid so many allocations here - use stackalloc where possible and non-allocating method overloads. Make sure to reuse the ToHexString method which is a lot more efficient than what we have right now. Also I don't think we need to hash UTF-8, we can simply do ReadOnlySpan<byte> utf16Bytes = MemoryMarshal.AsBytes(text.AsSpan()); , right? We hash a lot of strings during the build process, so let's make sure we don't unnecessarily slow down builds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 8d5709b: reduced allocations in JavaPeerScanner by switching to span-based hex formatting (with stackalloc), using non-allocating XxHash64.Hash(ReadOnlySpan<byte>, Span<byte>), and removing intermediate "{ns}:{assembly}" string allocations via pooled UTF-8 buffers. I kept UTF-8 input for hash compatibility with existing outputs/policies.

}

static char GetHexLowerChar (int value)
{
return (char) (value < 10 ? ('0' + value) : ('a' + value - 10));
}

static string ExtractNamespace (string fullName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ public TrimmableTypeMapResult Execute (
HashSet<string> 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);
Expand Down Expand Up @@ -104,9 +105,9 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []);
}

(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
(List<JavaPeerInfo> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
Debug="$(AndroidIncludeDebugSymbols)"
NeedsInternet="$(AndroidNeedsInternetPermission)"
EmbedAssemblies="$(EmbedAssembliesIntoApk)"
PackageNamingPolicy="$(AndroidPackageNamingPolicy)"
ManifestPlaceholders="$(AndroidManifestPlaceholders)"
CheckedBuild="$(_AndroidCheckedBuild)"
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -131,7 +132,8 @@ public override bool RunTask ()
frameworkAssemblyNames,
useSharedTypemapUniverse: !Debug,
manifestConfig,
manifestTemplate);
manifestTemplate,
PackageNamingPolicy);

GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths);
GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<AndroidVersionCodePattern Condition=" '$(AndroidUseLegacyVersionCode)' != 'True' And '$(AndroidVersionCodePattern)' == '' ">{abi}{versionCode:D5}</AndroidVersionCodePattern>
<AndroidResourceGeneratorTargetName>UpdateGeneratedFiles</AndroidResourceGeneratorTargetName>
<AndroidUseApkSigner Condition=" '$(AndroidUseApkSigner)' == '' ">True</AndroidUseApkSigner>
<AndroidPackageNamingPolicy Condition=" '$(AndroidPackageNamingPolicy)' == '' ">LowercaseCrc64</AndroidPackageNamingPolicy>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the XxHash64 should be the default just for the trimmable typemap

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 05cebc7: reverted the global AndroidPackageNamingPolicy default and scoped XxHash64 as the default specifically for trimmable typemap generation.

<AndroidPackageNamingPolicy Condition=" '$(AndroidPackageNamingPolicy)' == '' ">XxHash64</AndroidPackageNamingPolicy>
<AndroidUseManagedDesignTimeResourceGenerator Condition=" '$(AndroidUseManagedDesignTimeResourceGenerator)' == '' And '$(OS)' != 'Windows_NT' ">False</AndroidUseManagedDesignTimeResourceGenerator>
<BundleToolVersion Condition="'$(BundleToolVersion)' == ''">@BUNDLETOOL_VERSION@</BundleToolVersion>
<_XamarinAndroidMSBuildDirectory>$(MSBuildThisFileDirectory)</_XamarinAndroidMSBuildDirectory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefixed crc64 so the algorithm was clear. Should we pick something less generic than hash?

}
}
return javaName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ private protected static string TestFixtureAssemblyPath {

private protected static List<JavaPeerInfo> ScanFixtures () => _cachedScanResult.Value.peers;

private protected static List<JavaPeerInfo> 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)
Expand All @@ -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 ('.')) : "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -54,9 +54,9 @@ 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);
Assert.StartsWith ("xx64", FindFixtureByManagedName (managedName).JavaName);
}

[Fact]
Expand Down
Loading
Loading