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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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., "crc64.../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 {
Crc64,
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,25 @@ 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" + ScannerHashingHelper.ToLegacyCrc64 (ns, assemblyName),
_ => "crc64" + ScannerHashingHelper.ToCrc64 (ns, assemblyName),
};
}

static HashedPackageNamingPolicy ParsePackageNamingPolicy (string? packageNamingPolicy)
{
if (string.Equals (packageNamingPolicy, "LowercaseCrc64", StringComparison.OrdinalIgnoreCase)) {
return HashedPackageNamingPolicy.LowercaseCrc64;
}
return HashedPackageNamingPolicy.Crc64;
}

static string ExtractNamespace (string fullName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
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[] 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<byte> 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);
Span<byte> utf8Buffer = byteCount <= stackallocThresholdBytes
? stackalloc byte [stackallocThresholdBytes]
: new byte [byteCount];

int bytesWritten = GetNamespaceAssemblyUtf8Bytes (ns, assemblyName, utf8Buffer.Slice (0, byteCount));
Span<byte> 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)
{
return System.Text.Encoding.UTF8.GetByteCount (ns) + 1 + System.Text.Encoding.UTF8.GetByteCount (assemblyName);
}

static unsafe int GetNamespaceAssemblyUtf8Bytes (string ns, string assemblyName, Span<byte> 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<byte> hash, bool lowercase)
{
const int maxStackCharLength = 128;
int charLength = hash.Length * 2;
Span<char> 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<char>) chars).ToString ();
}

static void WriteUInt64LittleEndian (Span<byte> 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'));
}
}
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 @@ -11,6 +11,8 @@

<PropertyGroup>
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps</_TypeMapAssemblyName>
<_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_AndroidPackageNamingPolicySetByUser)' == 'true' ">$(AndroidPackageNamingPolicy)</_TrimmableTypeMapPackageNamingPolicy>
<_TrimmableTypeMapPackageNamingPolicy Condition=" '$(_TrimmableTypeMapPackageNamingPolicy)' == '' ">Crc64</_TrimmableTypeMapPackageNamingPolicy>
<!-- Use the outer IntermediateOutputPath when available (inner per-RID builds set
_OuterIntermediateOutputPath) so that generated manifests/JCWs are written
where the packaging phase expects them. -->
Expand Down Expand Up @@ -73,6 +75,7 @@
Debug="$(AndroidIncludeDebugSymbols)"
NeedsInternet="$(AndroidNeedsInternetPermission)"
EmbedAssemblies="$(EmbedAssembliesIntoApk)"
PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)"
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,6 +21,7 @@
<AndroidVersionCodePattern Condition=" '$(AndroidUseLegacyVersionCode)' != 'True' And '$(AndroidVersionCodePattern)' == '' ">{abi}{versionCode:D5}</AndroidVersionCodePattern>
<AndroidResourceGeneratorTargetName>UpdateGeneratedFiles</AndroidResourceGeneratorTargetName>
<AndroidUseApkSigner Condition=" '$(AndroidUseApkSigner)' == '' ">True</AndroidUseApkSigner>
<_AndroidPackageNamingPolicySetByUser Condition=" '$(AndroidPackageNamingPolicy)' != '' ">true</_AndroidPackageNamingPolicySetByUser>
<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.

<AndroidUseManagedDesignTimeResourceGenerator Condition=" '$(AndroidUseManagedDesignTimeResourceGenerator)' == '' And '$(OS)' != 'Windows_NT' ">False</AndroidUseManagedDesignTimeResourceGenerator>
<BundleToolVersion Condition="'$(BundleToolVersion)' == ''">@BUNDLETOOL_VERSION@</BundleToolVersion>
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);
}
}
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
Loading