diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/NativeAotTypeManager.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/NativeAotTypeManager.cs index eda56b5aca1..b6e262cd86d 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/NativeAotTypeManager.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/NativeAotTypeManager.cs @@ -141,8 +141,8 @@ protected override IEnumerable GetSimpleReferences (Type type) yield return r; } - if (TypeMapping.TryGetJavaClassName (type, out var javaClassName)) { - yield return javaClassName; + if (TypeMapping.TryGetJniName (type, out var jniName)) { + yield return jniName; } } diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs index 892bbd93573..4a95ebf90ad 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/TypeMapping.cs @@ -10,67 +10,65 @@ namespace Microsoft.Android.Runtime; internal static class TypeMapping { - internal static bool TryGetType (string javaClassName, [NotNullWhen (true)] out Type? type) + internal static bool TryGetType (string jniName, [NotNullWhen (true)] out Type? type) { - ulong hash = Hash (javaClassName); + type = null; // the hashes array is sorted and all the hashes are unique - int typeIndex = MemoryExtensions.BinarySearch (JavaClassNameHashes, hash); - if (typeIndex < 0) { - type = null; + ulong jniNameHash = Hash (jniName); + int jniNameHashIndex = MemoryExtensions.BinarySearch (JniNameHashes, jniNameHash); + if (jniNameHashIndex < 0) { return false; } - type = GetTypeByIndex (typeIndex); - if (type is null) { - throw new InvalidOperationException ($"Type with hash {hash} not found."); + // we need to make sure if this is the right match or if it is a hash collision + if (jniName != GetJniNameByJniNameHashIndex (jniNameHashIndex)) { + return false; } - // ensure this is not a hash collision - var resolvedJavaClassName = GetJavaClassNameByIndex (TypeIndexToJavaClassNameIndex [typeIndex]); - if (resolvedJavaClassName != javaClassName) { - type = null; - return false; + type = GetTypeByJniNameHashIndex (jniNameHashIndex); + if (type is null) { + throw new InvalidOperationException ($"Type for {jniName} (hash: {jniNameHash}, index: {jniNameHashIndex}) not found."); } return true; } - internal static bool TryGetJavaClassName (Type type, [NotNullWhen (true)] out string? className) + internal static bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) { - string? fullName = type.FullName; - if (fullName is null) { - className = null; + jniName = null; + + string? assemblyQualifiedName = type.AssemblyQualifiedName; + if (assemblyQualifiedName is null) { + jniName = null; return false; } - ulong hash = Hash (fullName); + ReadOnlySpan typeName = GetSimplifiedAssemblyQualifiedTypeName (assemblyQualifiedName); // the hashes array is sorted and all the hashes are unique - int javaClassNameIndex = MemoryExtensions.BinarySearch (TypeNameHashes, hash); - if (javaClassNameIndex < 0) { - className = null; + ulong typeNameHash = Hash (typeName); + int typeNameHashIndex = MemoryExtensions.BinarySearch (TypeNameHashes, typeNameHash); + if (typeNameHashIndex < 0) { return false; } - className = GetJavaClassNameByIndex (javaClassNameIndex); - if (className is null) { - throw new InvalidOperationException ($"Java class name with hash {hash} not found."); + // we need to make sure if this is the match or if it is a hash collision + if (!typeName.SequenceEqual (GetTypeNameByTypeNameHashIndex (typeNameHashIndex))) { + return false; } - // ensure this is not a hash collision - var resolvedType = GetTypeByIndex (JavaClassNameIndexToTypeIndex [javaClassNameIndex]); - if (resolvedType?.FullName != type.FullName) { - className = null; - return false; + jniName = GetJniNameByTypeNameHashIndex (typeNameHashIndex); + if (jniName is null) { + throw new InvalidOperationException ($"JNI name for {typeName} (hash: {typeNameHash}, index: {typeNameHashIndex}) not found."); } return true; } - private static ulong Hash (string javaClassName) + private static ulong Hash (ReadOnlySpan value) { - ReadOnlySpan bytes = MemoryMarshal.AsBytes (javaClassName.AsSpan ()); + ReadOnlySpan bytes = MemoryMarshal.AsBytes (value); ulong hash = XxHash3.HashToUInt64 (bytes); // The bytes in the hashes array are stored as little endian. If the target platform is big endian, @@ -82,11 +80,26 @@ private static ulong Hash (string javaClassName) return hash; } + // This method keeps only the full type name and the simple assembly name. + // It drops the version, culture, and public key information. + // + // For example: "System.Int32, System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" + // becomes: "System.Int32, System.Private.CoreLib" + private static ReadOnlySpan GetSimplifiedAssemblyQualifiedTypeName(string assemblyQualifiedName) + { + var commaIndex = assemblyQualifiedName.IndexOf(','); + var secondCommaIndex = assemblyQualifiedName.IndexOf(',', startIndex: commaIndex + 1); + return secondCommaIndex < 0 + ? assemblyQualifiedName + : assemblyQualifiedName.AsSpan(0, secondCommaIndex); + } + // Replaced by src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs - private static ReadOnlySpan JavaClassNameHashes => throw new NotImplementedException (); private static ReadOnlySpan TypeNameHashes => throw new NotImplementedException (); - private static ReadOnlySpan JavaClassNameIndexToTypeIndex => throw new NotImplementedException (); - private static ReadOnlySpan TypeIndexToJavaClassNameIndex => throw new NotImplementedException (); - private static Type? GetTypeByIndex (int index) => throw new NotImplementedException (); - private static string? GetJavaClassNameByIndex (int index) => throw new NotImplementedException (); + private static Type? GetTypeByJniNameHashIndex (int jniNameHashIndex) => throw new NotImplementedException (); + private static string? GetJniNameByJniNameHashIndex (int jniNameHashIndex) => throw new NotImplementedException (); + + private static ReadOnlySpan JniNameHashes => throw new NotImplementedException (); + private static string? GetJniNameByTypeNameHashIndex (int typeNameHashIndex) => throw new NotImplementedException (); + private static string? GetTypeNameByTypeNameHashIndex (int typeNameHashIndex) => throw new NotImplementedException (); } diff --git a/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs b/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs index 6cc849d51d7..30494865759 100644 --- a/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs +++ b/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs @@ -61,34 +61,112 @@ protected override void EndProcess () throw new InvalidOperationException ($"Unable to find {TypeName} type"); } - // Java -> .NET mapping - KeyValuePair>[] orderedJavaToDotnetMapping = TypeMappings.OrderBy (kvp => Hash (kvp.Key)).ToArray (); + var typeMappingRecords = TypeMappings + .Select (kvp => + new TypeMapRecord { + JniName = kvp.Key, + Types = kvp.Value.ToArray (), + Context = Context, + }) + .ToArray (); - var javaClassNameHashes = orderedJavaToDotnetMapping.Select (kvp => Hash (kvp.Key)).ToArray (); - GenerateHashes (javaClassNameHashes, methodName: "get_JavaClassNameHashes"); + // Java -> .NET mapping + { + var orderedJavaToDotnetMapping = typeMappingRecords.OrderBy (record => Hash (record.JniName)) + .ToArray (); + var jniNames = orderedJavaToDotnetMapping.Select (record => record.JniName) + .ToArray (); + var jniNameHashes = jniNames.Select (Hash) + .ToArray (); + var types = orderedJavaToDotnetMapping.Select (record => record.SelectTypeDefinition ()) + .ToArray (); + + Context.LogMessage ("JNI -> .NET mappings"); + Context.LogMessage ($"Generated field {type.FullName}.s_get_JniNameHashes_data contains {jniNameHashes.Length} hashes:"); + for (int i = 0; i < jniNameHashes.Length; ++i ) { + var java = jniNames [i]; + var hash = jniNameHashes [i]; + Context.LogMessage ($"\t0x{hash.ToString ("x", System.Globalization.CultureInfo.InvariantCulture), -16} // {i,4}: {java}"); + } + Context.LogMessage ($"Generated method {type.FullName}.GetTypeByJniNameHashIndex contains {types.Length} mappings:"); + var maxAqtnLength = types.Max (t => TypeDefinitionRocks.GetAssemblyQualifiedName (t, Context).Length)+2; + for (int i = 0; i < types.Length; ++i ) { + var aqtn = TypeDefinitionRocks.GetAssemblyQualifiedName (types [i], Context); + var java = jniNames [i]; + var hash = jniNameHashes [i]; + Context.LogMessage ( + string.Format (System.Globalization.CultureInfo.InvariantCulture, + "\tindex {0,4} => Type.GetType({1,-" + maxAqtnLength + "}), // `{2}` hash=0x{3:x16}", i, $"\"{aqtn}\"", java, hash)); + } + Context.LogMessage ($"Generated method {type.FullName}.GetJniNameByJniNameHashIndex contains {jniNames.Length} mappings:"); + var maxJavaLength = jniNames.Max (s => s.Length)+2; + for (int i = 0; i < jniNames.Length; ++i ) { + var java = jniNames [i]; + var aqtn = TypeDefinitionRocks.GetAssemblyQualifiedName (types [i], Context); + var hash = jniNameHashes [i]; + Context.LogMessage ( + string.Format (System.Globalization.CultureInfo.InvariantCulture, + "\tindex {0,4} => {1,-" + maxJavaLength + "}, // `{2}` hash=0x{3:x16}", i, $"\"{java}\"", aqtn, hash)); + } - var types = orderedJavaToDotnetMapping.Select (kvp => SelectTypeDefinition (kvp.Key, kvp.Value)); - GenerateGetTypeByIndex (types); + GenerateHashes (jniNameHashes, methodName: "get_JniNameHashes"); + GenerateGetTypeByJniNameHashIndex (types); + GenerateStringSwitchMethod (type, "GetJniNameByJniNameHashIndex", jniNames); + } // .NET -> Java mapping - KeyValuePair>[] orderedManagedToJavaMapping = TypeMappings.OrderBy (kvp => Hash (SelectTypeDefinition(kvp.Key, kvp.Value).FullName)).ToArray (); - - var dotnetTypeNameHashes = orderedManagedToJavaMapping.Select (kvp => Hash (SelectTypeDefinition(kvp.Key, kvp.Value).FullName)).ToArray (); - GenerateHashes (dotnetTypeNameHashes, methodName: "get_TypeNameHashes"); - - string[] javaClassNames = orderedManagedToJavaMapping.Select (kvp => kvp.Key).ToArray (); - GenerateGetJavaClassNameByIndex (javaClassNames); + { + var orderedManagedToJavaMapping = typeMappingRecords + .SelectMany (record => record.Flatten ()) + .OrderBy (record => Hash (record.TypeName)) + .ToArray (); + + var typeNames = orderedManagedToJavaMapping + .Select (record => record.TypeName) + .ToArray (); + var typeNameHashes = typeNames.Select (Hash) + .ToArray (); + var jniNames = orderedManagedToJavaMapping.Select (record => record.JniName) + .ToArray (); + + Context.LogMessage (".NET -> JNI mappings"); + Context.LogMessage ($"Generated field {type.FullName}.s_get_TypeNameHashes_data contains {typeNameHashes.Length} hashes:"); + for (int i = 0; i < typeNameHashes.Length; ++i ) { + var aqtn = typeNames [i]; + var hash = typeNameHashes [i]; + Context.LogMessage ($"\t0x{hash.ToString ("x", System.Globalization.CultureInfo.InvariantCulture), -16} // {i,4}: {aqtn}"); + } + var maxJavaLength = jniNames.Max (s => s.Length) + 2; + Context.LogMessage ($"Generated method {type.FullName}.GetJniNameByTypeNameHashIndex contains {jniNames.Length} mappings:"); + for (int i = 0; i < jniNames.Length; ++i ) { + var java = jniNames [i]; + var aqtn = typeNames [i]; + var hash = typeNameHashes [i]; + Context.LogMessage ( + string.Format (System.Globalization.CultureInfo.InvariantCulture, + "\tindex {0,4} => {1,-" + maxJavaLength + "}, // `{2}` hash=0x{3:x16}", i, $"\"{java}\"", aqtn, hash)); + } + Context.LogMessage ($"Generated method {type.FullName}.GetTypeNameByTypeNameHashIndex contains {typeNames.Length} mappings:"); + var maxAqtnLength = typeNames.Max (s => s.Length) + 2; + for (int i = 0; i < typeNames.Length; ++i ) { + var java = jniNames [i]; + var aqtn = typeNames [i]; + var hash = typeNameHashes [i]; + Context.LogMessage ( + string.Format (System.Globalization.CultureInfo.InvariantCulture, + "\tindex {0,4} => {1,-" + maxAqtnLength + "}, // `{2}` hash=0x{3:x16}", i, $"\"{aqtn}\"", java, hash)); + } - // Generate remap arrays - var typeIndexKeys = orderedJavaToDotnetMapping.Select (kvp => kvp.Key).ToArray (); - var javaClassNameIndexKeys = orderedManagedToJavaMapping.Select (kvp => kvp.Key).ToArray (); - GenerateIndexRemapping (typeIndexKeys, javaClassNameIndexKeys); + GenerateHashes (typeNameHashes, methodName: "get_TypeNameHashes"); + GenerateStringSwitchMethod (type, "GetJniNameByTypeNameHashIndex", jniNames); + GenerateStringSwitchMethod (type, "GetTypeNameByTypeNameHashIndex", typeNames); + } - void GenerateGetTypeByIndex (IEnumerable types) + void GenerateGetTypeByJniNameHashIndex (IEnumerable types) { - var method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByIndex"); + var method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByJniNameHashIndex"); if (method is null) { - throw new InvalidOperationException ($"Unable to find {TypeName}.GetTypeByIndex() method"); + throw new InvalidOperationException ($"Unable to find {TypeName}.GetTypeByJniNameHashIndex() method"); } var getTypeFromHandle = module.ImportReference (typeof (Type).GetMethod ("GetTypeFromHandle")); @@ -122,11 +200,11 @@ void GenerateGetTypeByIndex (IEnumerable types) il.Emit (OpCodes.Ret); } - void GenerateGetJavaClassNameByIndex (string[] javaClassNames) + void GenerateStringSwitchMethod (TypeDefinition type, string methodName, string[] values) { - var method = type.Methods.FirstOrDefault (m => m.Name == "GetJavaClassNameByIndex"); + var method = type.Methods.FirstOrDefault (m => m.Name == methodName); if (method is null) { - throw new InvalidOperationException ($"Unable to find {TypeName}.GetJavaClassNameByIndex() method"); + throw new InvalidOperationException ($"Unable to find {type.FullName}.{methodName} method"); } // Clear IL in method body @@ -135,8 +213,8 @@ void GenerateGetJavaClassNameByIndex (string[] javaClassNames) var il = method.Body.GetILProcessor (); var targets = new List (); - foreach (var name in javaClassNames) { - targets.Add (il.Create (OpCodes.Ldstr, name)); + foreach (var value in values) { + targets.Add (il.Create (OpCodes.Ldstr, value)); } il.Emit (OpCodes.Ldarg_0); @@ -173,28 +251,6 @@ void GenerateHashes (ulong[] hashes, string methodName) GenerateReadOnlySpanGetter (type, methodName, hashes, sizeof (ulong), BitConverter.GetBytes); } - void GenerateIndexRemapping (string[] typeIndexKeys, string[] javaClassNameIndexKeys) - { - System.Diagnostics.Debug.Assert(typeIndexKeys.Length == javaClassNameIndexKeys.Length); - int length = typeIndexKeys.Length; - - var javaClassNameIndexToTypeIndex = new int[length]; - var typeIndexToJavaClassNameIndex = new int[length]; - - for (int i = 0; i < length; i++) { - for (int j = 0; j < length; j++) { - if (typeIndexKeys[i] == javaClassNameIndexKeys[j]) { - typeIndexToJavaClassNameIndex[i] = j; - javaClassNameIndexToTypeIndex[j] = i; - break; - } - } - } - - GenerateReadOnlySpanGetter (type, "get_JavaClassNameIndexToTypeIndex", javaClassNameIndexToTypeIndex, sizeof (int), BitConverter.GetBytes); - GenerateReadOnlySpanGetter (type, "get_TypeIndexToJavaClassNameIndex", typeIndexToJavaClassNameIndex, sizeof (int), BitConverter.GetBytes); - } - void GenerateReadOnlySpanGetter (TypeDefinition type, string name, T[] data, int sizeOfT, Func getBytes) where T : struct { @@ -245,37 +301,75 @@ TypeDefinition GetArrayType (TypeDefinition type, int size) } } - TypeDefinition SelectTypeDefinition (string javaName, List list) + class TypeMapRecord { - if (list.Count == 1) - return list[0]; - - var best = list[0]; - foreach (var type in list) { - if (type == best) - continue; - // Types in Mono.Android assembly should be first in the list - if (best.Module.Assembly.Name.Name != "Mono.Android" && - type.Module.Assembly.Name.Name == "Mono.Android") { - best = type; - continue; - } - // We found the `Invoker` type *before* the declared type - // Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate. - if ((type.IsAbstract || type.IsInterface) && - !best.IsAbstract && - !best.IsInterface && - type.IsAssignableFrom (best, Context)) { - best = type; - continue; + public required string JniName { get; init; } + public required TypeDefinition[] Types { get; init; } + public required LinkContext Context { get; init; } + + public string TypeName + { + get + { + // We need to drop the version, culture, and public key information from the AQN. + var type = SelectTypeDefinition (); + var assemblyQualifiedName = TypeDefinitionRocks.GetAssemblyQualifiedName (type, Context); + var commaIndex = assemblyQualifiedName.IndexOf(','); + var secondCommaIndex = assemblyQualifiedName.IndexOf(',', startIndex: commaIndex + 1); + return secondCommaIndex < 0 + ? assemblyQualifiedName + : assemblyQualifiedName.Substring (0, secondCommaIndex); } } - foreach (var type in list) { - if (type == best) - continue; - Context.LogMessage ($"Duplicate typemap entry for {javaName} => {type.FullName}"); + + public TypeDefinition SelectTypeDefinition () + { + if (Types.Length == 1) + return Types[0]; + + var best = Types[0]; + foreach (var type in Types) { + if (type == best) + continue; + // Types in Mono.Android assembly should be first in the list + if (best.Module.Assembly.Name.Name != "Mono.Android" && + type.Module.Assembly.Name.Name == "Mono.Android") { + best = type; + continue; + } + // We found the `Invoker` type *before* the declared type + // Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate. + if ((type.IsAbstract || type.IsInterface) && + !best.IsAbstract && + !best.IsInterface && + type.IsAssignableFrom (best, Context)) { + best = type; + continue; + } + + // we found a generic subclass of a non-generic type + if (type.IsGenericInstance && + !best.IsGenericInstance && + type.IsAssignableFrom (best, Context)) { + best = type; + continue; + } + } + foreach (var type in Types) { + if (type == best) + continue; + Context.LogMessage ($"Duplicate typemap entry for {JniName} => {type.FullName}"); + } + return best; } - return best; + + public IEnumerable Flatten () => + Types.Select (type => + new TypeMapRecord { + JniName = JniName, + Types = new[] { type }, + Context = Context, + }); } void ProcessType (AssemblyDefinition assembly, TypeDefinition type) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 9ddb5060bbb..c9e0ac98f36 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -172,17 +172,14 @@ public void NativeAOT () var javaClassNames = new List (); var types = new List (); - int[] typeIndexToJavaClassNameIndexRemapping; - int[] javaClassNameIndexToTypeIndexRemapping; - var linkedRuntimeAssembly = Path.Combine (intermediate, "android-arm64", "linked", "Microsoft.Android.Runtime.NativeAOT.dll"); FileAssert.Exists (linkedRuntimeAssembly); using (var assembly = AssemblyDefinition.ReadAssembly (linkedRuntimeAssembly)) { var type = assembly.MainModule.Types.FirstOrDefault (t => t.Name == "TypeMapping"); Assert.IsNotNull (type, $"{linkedRuntimeAssembly} should contain TypeMapping"); - var method = type.Methods.FirstOrDefault (m => m.Name == "GetJavaClassNameByIndex"); - Assert.IsNotNull (method, "TypeMapping should contain GetJavaClassNameByIndex"); + var method = type.Methods.FirstOrDefault (m => m.Name == "GetJniNameByTypeNameHashIndex"); + Assert.IsNotNull (method, "TypeMapping should contain GetJniNameByTypeNameHashIndex"); foreach (var i in method.Body.Instructions) { if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldstr) @@ -194,8 +191,8 @@ public void NativeAOT () javaClassNames.Add (javaName); } - method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByIndex"); - Assert.IsNotNull (method, "TypeMapping should contain GetTypeByIndex"); + method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByJniNameHashIndex"); + Assert.IsNotNull (method, "TypeMapping should contain GetTypeByJniNameHashIndex"); foreach (var i in method.Body.Instructions) { if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldtoken) @@ -209,9 +206,6 @@ public void NativeAOT () types.Add (typeReference); } - typeIndexToJavaClassNameIndexRemapping = MemoryMarshal.Cast (type.Fields.First (f => f.Name == "s_get_TypeIndexToJavaClassNameIndex_data").InitialValue).ToArray (); - javaClassNameIndexToTypeIndexRemapping = MemoryMarshal.Cast (type.Fields.First (f => f.Name == "s_get_JavaClassNameIndexToTypeIndex_data").InitialValue).ToArray (); - // Basic types AssertTypeMap ("java/lang/Object", "Java.Lang.Object"); AssertTypeMap ("java/lang/String", "Java.Lang.String"); @@ -259,10 +253,6 @@ void AssertTypeMap(string javaName, string managedName) Assert.Fail ($"TypeMapping should contain \"{javaName}\"!"); } else if (typeIndex < 0) { Assert.Fail ($"TypeMapping should contain \"{managedName}\"!"); - } else if (typeIndexToJavaClassNameIndexRemapping[typeIndex] != javaNameIndex) { - Assert.Fail ($"TypeMapping should contain \"{javaName}\" <-> \"{managedName}\""); - } else if (javaClassNameIndexToTypeIndexRemapping[javaNameIndex] != typeIndex) { - Assert.Fail ($"TypeMapping should contain \"{javaName}\" <-> \"{managedName}\""); } } }