diff --git a/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs b/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs index 9b82e500fd8..0288433f8f3 100644 --- a/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs +++ b/src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs @@ -16,7 +16,7 @@ public class TypeMappingStep : BaseStep { const string AssemblyName = "Microsoft.Android.Runtime.NativeAOT"; const string TypeName = "Microsoft.Android.Runtime.NativeAotTypeManager"; - readonly IDictionary TypeMappings = new Dictionary (StringComparer.Ordinal); + readonly IDictionary> TypeMappings = new Dictionary> (StringComparer.Ordinal); AssemblyDefinition? MicrosoftAndroidRuntimeNativeAot; protected override void ProcessAssembly (AssemblyDefinition assembly) @@ -66,7 +66,7 @@ protected override void EndProcess () var il = method.Body.GetILProcessor (); var addMethod = module.ImportReference (typeof (IDictionary).GetMethod ("Add")); var getTypeFromHandle = module.ImportReference (typeof (Type).GetMethod ("GetTypeFromHandle")); - foreach (var (javaKey, typeDefinition) in TypeMappings) { + foreach (var (javaName, list) in TypeMappings) { /* * IL_0000: ldarg.0 * IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IDictionary`2 Microsoft.Android.Runtime.NativeAotTypeManager::TypeMappings @@ -77,8 +77,8 @@ protected override void EndProcess () */ il.Emit (Mono.Cecil.Cil.OpCodes.Ldarg_0); il.Emit (Mono.Cecil.Cil.OpCodes.Ldfld, field); - il.Emit (Mono.Cecil.Cil.OpCodes.Ldstr, javaKey); - il.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (typeDefinition)); + il.Emit (Mono.Cecil.Cil.OpCodes.Ldstr, javaName); + il.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (SelectTypeDefinition (javaName, list))); il.Emit (Mono.Cecil.Cil.OpCodes.Call, getTypeFromHandle); il.Emit (Mono.Cecil.Cil.OpCodes.Callvirt, addMethod); } @@ -86,13 +86,47 @@ protected override void EndProcess () il.Emit (Mono.Cecil.Cil.OpCodes.Ret); } + TypeDefinition SelectTypeDefinition (string javaName, List list) + { + 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; + } + } + foreach (var type in list) { + if (type == best) + continue; + Context.LogMessage ($"Duplicate typemap entry for {javaName} => {type.FullName}"); + } + return best; + } + void ProcessType (AssemblyDefinition assembly, TypeDefinition type) { if (type.HasJavaPeer (Context)) { var javaName = JavaNativeTypeManager.ToJniName (type, Context); - if (!TypeMappings.TryAdd (javaName, type)) { - Context.LogMessage ($"Duplicate typemap entry for {javaName}"); + if (!TypeMappings.TryGetValue (javaName, out var list)) { + TypeMappings.Add (javaName, list = new List ()); } + list.Add (type); } if (!type.HasNestedTypes) 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 77045ab29c0..d24cbec9d06 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 @@ -135,6 +135,11 @@ public void NativeAOT () proj.SetProperty ("PublishAot", "true"); proj.SetProperty ("PublishAotUsingRuntimePack", "true"); proj.SetProperty ("AndroidNdkDirectory", AndroidNdkPath); + proj.SetProperty ("_ExtraTrimmerArgs", "--verbose"); + + // Required for java/util/ArrayList assertion below + proj.MainActivity = proj.DefaultMainActivity + .Replace ("//${AFTER_ONCREATE}", "new Android.Runtime.JavaList (); new Android.Runtime.JavaList ();"); using var b = CreateApkBuilder (); Assert.IsTrue (b.Build (proj), "Build should have succeeded."); @@ -165,6 +170,43 @@ public void NativeAOT () Assert.IsNotNull (method, $"{linkedMonoAndroidAssembly} should contain {typeName}.{methodName}"); } + var typemap = new Dictionary (); + var linkedRuntimeAssembly = Path.Combine (intermediate, "linked", "Microsoft.Android.Runtime.NativeAOT.dll"); + FileAssert.Exists (linkedRuntimeAssembly); + using (var assembly = AssemblyDefinition.ReadAssembly (linkedRuntimeAssembly)) { + var type = assembly.MainModule.Types.FirstOrDefault (t => t.Name == "NativeAotTypeManager"); + Assert.IsNotNull (type, $"{linkedRuntimeAssembly} should contain NativeAotTypeManager"); + var method = type.Methods.FirstOrDefault (m => m.Name == "InitializeTypeMappings"); + Assert.IsNotNull (method, "NativeAotTypeManager should contain InitializeTypeMappings"); + + foreach (var i in method.Body.Instructions) { + if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldstr) + continue; + if (i.Operand is not string javaName) + continue; + if (i.Next.Operand is not TypeReference t) + continue; + typemap.Add (javaName, t); + } + + // Basic types + AssertTypeMap ("java/lang/Object", "Java.Lang.Object"); + AssertTypeMap ("java/lang/String", "Java.Lang.String"); + AssertTypeMap ("[Ljava/lang/Object;", "Java.Interop.JavaArray`1"); + AssertTypeMap ("java/util/ArrayList", "Android.Runtime.JavaList"); + AssertTypeMap ("android/app/Activity", "Android.App.Activity"); + AssertTypeMap ("android/widget/Button", "Android.Widget.Button"); + Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, + "Duplicate typemap entry for java/util/ArrayList => Android.Runtime.JavaList`1"), + "Should get log message about duplicate Android.Runtime.JavaList`1!"); + + // Special *Invoker case + AssertTypeMap ("android/view/View$OnClickListener", "Android.Views.View/IOnClickListener"); + Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, + "Duplicate typemap entry for android/view/View$OnClickListener => Android.Views.View/IOnClickListenerInvoker"), + "Should get log message about duplicate IOnClickListenerInvoker!"); + } + var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex"); FileAssert.Exists (dexFile); foreach (var className in mono_classes) { @@ -180,6 +222,15 @@ public void NativeAOT () foreach (var nativeaot_file in nativeaot_files) { Assert.IsTrue (zip.ContainsEntry (nativeaot_file, caseSensitive: true), $"APK must contain `{nativeaot_file}`."); } + + void AssertTypeMap(string javaName, string managedName) + { + if (typemap.TryGetValue (javaName, out var reference)) { + Assert.AreEqual (managedName, reference.ToString ()); + } else { + Assert.Fail ($"InitializeTypeMappings should contain Ldstr \"{javaName}\"!"); + } + } } [Test]