Skip to content

Commit cc6bfa3

Browse files
[nativeaot] fix typemap logic involving duplicates
70bd636 introduced an "MVP" for a managed typemap for NativeAOT. Unfortunately, a `dotnet new maui` project template still crashes with: AndroidRuntime: Process: net.dot.hellonativeaot, PID: 16075 AndroidRuntime: net.dot.jni.internal.JavaProxyThrowable: System.InvalidCastException: Arg_InvalidCastException AndroidRuntime: at Java.Lang.Object._GetObject[T](IntPtr, JniHandleOwnership) + 0x64 AndroidRuntime: at Microsoft.Maui.WindowOverlay.Initialize() + 0x168 AndroidRuntime: at Microsoft.Maui.Handlers.WindowHandler.OnRootViewChanged(Object sender, EventArgs e) + 0x8c AndroidRuntime: at Microsoft.Maui.Platform.NavigationRootManager.SetContentView(IView) + 0x17c AndroidRuntime: at Microsoft.Maui.Platform.NavigationRootManager.Connect(IView, IMauiContext) + 0xf0 AndroidRuntime: at Microsoft.Maui.Handlers.WindowHandler.CreateRootViewFromContent(IWindowHandler, IWindow) + 0x4c AndroidRuntime: at Microsoft.Maui.Handlers.WindowHandler.MapContent(IWindowHandler handler, IWindow window) + 0x20 AndroidRuntime: at Microsoft.Maui.PropertyMapper`2.<>c__DisplayClass5_0.<Add>b__0(IElementHandler h, IElement v) + 0x130 AndroidRuntime: at Microsoft.Maui.PropertyMapper.UpdatePropertyCore(String, IElementHandler, IElement) + 0x58 AndroidRuntime: at Microsoft.Maui.PropertyMapper.UpdateProperties(IElementHandler, IElement) + 0x74 AndroidRuntime: at Microsoft.Maui.Handlers.ElementHandler.SetVirtualView(IElement) + 0x15c AndroidRuntime: at Microsoft.Maui.Controls.Element.SetHandler(IElementHandler) + 0x124 AndroidRuntime: at Microsoft.Maui.Controls.Element.set_Handler(IElementHandler value) + 0xc AndroidRuntime: at Microsoft.Maui.Platform.ElementExtensions.SetHandler(Context, IElement, IMauiContext) + 0xfc AndroidRuntime: at Microsoft.Maui.Platform.ApplicationExtensions.CreatePlatformWindow(Activity, IApplication, Bundle) + 0x184 AndroidRuntime: at Microsoft.Maui.MauiAppCompatActivity.OnCreate(Bundle) + 0x74 AndroidRuntime: at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + 0xc0 AndroidRuntime: at my.MainActivity.n_onCreate(Native Method) AndroidRuntime: at my.MainActivity.onCreate(MainActivity.java:36) AndroidRuntime: at android.app.Activity.performCreate(Activity.java:8960) AndroidRuntime: at android.app.Activity.performCreate(Activity.java:8938) AndroidRuntime: at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1526) AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3975) AndroidRuntime: at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4173) AndroidRuntime: at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:114) AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:231) AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:152) AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:93) AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2595) AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:107) AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) AndroidRuntime: at android.os.Looper.loop(Looper.java:317) AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8592) AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580) AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878) One cause may be that `Java.Interop.JavaObject` is in the typemap in favor of `Java.Lang.Object`! This could cause `InvalidCastException`. The typemap implementation is missing two edge cases: 1. Types in `Mono.Android.dll` should be preferred over types in other assemblies. 2. Abstract or interface types should be preferred over their equivalent `*Invoker` types. There was also some missing logic regarding `*Invoker` types and abstract types. I fixed these cases in `TypeMappingStep.cs` and added a test that can validate the typemap during a build.
1 parent a6b0f13 commit cc6bfa3

File tree

2 files changed

+80
-6
lines changed

2 files changed

+80
-6
lines changed

src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class TypeMappingStep : BaseStep
1616
{
1717
const string AssemblyName = "Microsoft.Android.Runtime.NativeAOT";
1818
const string TypeName = "Microsoft.Android.Runtime.NativeAotTypeManager";
19-
readonly IDictionary<string, TypeDefinition> TypeMappings = new Dictionary<string, TypeDefinition> (StringComparer.Ordinal);
19+
readonly IDictionary<string, List<TypeDefinition>> TypeMappings = new Dictionary<string, List<TypeDefinition>> (StringComparer.Ordinal);
2020
AssemblyDefinition? MicrosoftAndroidRuntimeNativeAot;
2121

2222
protected override void ProcessAssembly (AssemblyDefinition assembly)
@@ -66,7 +66,7 @@ protected override void EndProcess ()
6666
var il = method.Body.GetILProcessor ();
6767
var addMethod = module.ImportReference (typeof (IDictionary<string, Type>).GetMethod ("Add"));
6868
var getTypeFromHandle = module.ImportReference (typeof (Type).GetMethod ("GetTypeFromHandle"));
69-
foreach (var (javaKey, typeDefinition) in TypeMappings) {
69+
foreach (var (javaName, list) in TypeMappings) {
7070
/*
7171
* IL_0000: ldarg.0
7272
* IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IDictionary`2<string, class [System.Runtime]System.Type> Microsoft.Android.Runtime.NativeAotTypeManager::TypeMappings
@@ -77,21 +77,53 @@ protected override void EndProcess ()
7777
*/
7878
il.Emit (Mono.Cecil.Cil.OpCodes.Ldarg_0);
7979
il.Emit (Mono.Cecil.Cil.OpCodes.Ldfld, field);
80-
il.Emit (Mono.Cecil.Cil.OpCodes.Ldstr, javaKey);
81-
il.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (typeDefinition));
80+
il.Emit (Mono.Cecil.Cil.OpCodes.Ldstr, javaName);
81+
il.Emit (Mono.Cecil.Cil.OpCodes.Ldtoken, module.ImportReference (SelectTypeDefinition (javaName, list)));
8282
il.Emit (Mono.Cecil.Cil.OpCodes.Call, getTypeFromHandle);
8383
il.Emit (Mono.Cecil.Cil.OpCodes.Callvirt, addMethod);
8484
}
8585

8686
il.Emit (Mono.Cecil.Cil.OpCodes.Ret);
8787
}
8888

89+
TypeDefinition SelectTypeDefinition (string javaName, List<TypeDefinition> list)
90+
{
91+
if (list.Count == 1)
92+
return list[0];
93+
94+
var best = list[0];
95+
foreach (var type in list) {
96+
if (type == best)
97+
continue;
98+
// We found the `Invoker` type *before* the declared type
99+
// Fix things up so the abstract type is first, and the `Invoker` is considered a duplicate.
100+
if ((type.IsAbstract || type.IsInterface) &&
101+
!best.IsAbstract &&
102+
!best.IsInterface &&
103+
type.IsAssignableFrom (best, Context)) {
104+
best = type;
105+
}
106+
}
107+
foreach (var type in list) {
108+
if (type == best)
109+
continue;
110+
Context.LogMessage ($"Duplicate typemap entry for {javaName} => {type.FullName}");
111+
}
112+
return best;
113+
}
114+
89115
void ProcessType (AssemblyDefinition assembly, TypeDefinition type)
90116
{
91117
if (type.HasJavaPeer (Context)) {
92118
var javaName = JavaNativeTypeManager.ToJniName (type, Context);
93-
if (!TypeMappings.TryAdd (javaName, type)) {
94-
Context.LogMessage ($"Duplicate typemap entry for {javaName}");
119+
if (!TypeMappings.TryGetValue (javaName, out var list)) {
120+
TypeMappings.Add (javaName, list = new List<TypeDefinition> ());
121+
}
122+
// Types in Mono.Android assembly should be first in the list
123+
if (assembly.Name.Name == "Mono.Android") {
124+
list.Insert (0, type);
125+
} else {
126+
list.Add (type);
95127
}
96128
}
97129

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public void NativeAOT ()
135135
proj.SetProperty ("PublishAot", "true");
136136
proj.SetProperty ("PublishAotUsingRuntimePack", "true");
137137
proj.SetProperty ("AndroidNdkDirectory", AndroidNdkPath);
138+
proj.SetProperty ("_ExtraTrimmerArgs", "--verbose");
138139

139140
using var b = CreateApkBuilder ();
140141
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
@@ -165,6 +166,38 @@ public void NativeAOT ()
165166
Assert.IsNotNull (method, $"{linkedMonoAndroidAssembly} should contain {typeName}.{methodName}");
166167
}
167168

169+
var typemap = new Dictionary<string, TypeReference> ();
170+
var linkedRuntimeAssembly = Path.Combine (intermediate, "linked", "Microsoft.Android.Runtime.NativeAOT.dll");
171+
FileAssert.Exists (linkedRuntimeAssembly);
172+
using (var assembly = AssemblyDefinition.ReadAssembly (linkedRuntimeAssembly)) {
173+
var type = assembly.MainModule.Types.FirstOrDefault (t => t.Name == "NativeAotTypeManager");
174+
Assert.IsNotNull (type, $"{linkedRuntimeAssembly} should contain NativeAotTypeManager");
175+
var method = type.Methods.FirstOrDefault (m => m.Name == "InitializeTypeMappings");
176+
Assert.IsNotNull (method, "NativeAotTypeManager should contain InitializeTypeMappings");
177+
178+
foreach (var i in method.Body.Instructions) {
179+
if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldstr)
180+
continue;
181+
if (i.Operand is not string javaName)
182+
continue;
183+
if (i.Next.Operand is not TypeReference t)
184+
continue;
185+
typemap.Add (javaName, t);
186+
}
187+
188+
// Basic types
189+
AssertTypeMap ("java/lang/Object", "Java.Lang.Object");
190+
AssertTypeMap ("java/lang/String", "Java.Lang.String");
191+
AssertTypeMap ("android/app/Activity", "Android.App.Activity");
192+
AssertTypeMap ("android/widget/Button", "Android.Widget.Button");
193+
194+
// Special *Invoker case
195+
Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput,
196+
"ILLink: Duplicate typemap entry for android/view/View$OnClickListener => Android.Views.View/IOnClickListenerInvoker"),
197+
"Should get log message about duplicate IOnClickListenerInvoker!");
198+
AssertTypeMap ("android/view/View$OnClickListener", "Android.Views.View/IOnClickListener");
199+
}
200+
168201
var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
169202
FileAssert.Exists (dexFile);
170203
foreach (var className in mono_classes) {
@@ -180,6 +213,15 @@ public void NativeAOT ()
180213
foreach (var nativeaot_file in nativeaot_files) {
181214
Assert.IsTrue (zip.ContainsEntry (nativeaot_file, caseSensitive: true), $"APK must contain `{nativeaot_file}`.");
182215
}
216+
217+
void AssertTypeMap(string javaName, string managedName)
218+
{
219+
if (typemap.TryGetValue (javaName, out var reference)) {
220+
Assert.AreEqual (managedName, reference.ToString ());
221+
} else {
222+
Assert.Fail ($"InitializeTypeMappings should contain Ldstr \"{javaName}\"!");
223+
}
224+
}
183225
}
184226

185227
[Test]

0 commit comments

Comments
 (0)