Skip to content

[nativeaot] fix typemap logic involving duplicates #9794

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Feb 18, 2025
Merged
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
46 changes: 40 additions & 6 deletions src/Microsoft.Android.Sdk.ILLink/TypeMappingStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class TypeMappingStep : BaseStep
{
const string AssemblyName = "Microsoft.Android.Runtime.NativeAOT";
const string TypeName = "Microsoft.Android.Runtime.NativeAotTypeManager";
readonly IDictionary<string, TypeDefinition> TypeMappings = new Dictionary<string, TypeDefinition> (StringComparer.Ordinal);
readonly IDictionary<string, List<TypeDefinition>> TypeMappings = new Dictionary<string, List<TypeDefinition>> (StringComparer.Ordinal);
AssemblyDefinition? MicrosoftAndroidRuntimeNativeAot;

protected override void ProcessAssembly (AssemblyDefinition assembly)
Expand Down Expand Up @@ -66,7 +66,7 @@ protected override void EndProcess ()
var il = method.Body.GetILProcessor ();
var addMethod = module.ImportReference (typeof (IDictionary<string, Type>).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<string, class [System.Runtime]System.Type> Microsoft.Android.Runtime.NativeAotTypeManager::TypeMappings
Expand All @@ -77,22 +77,56 @@ 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);
}

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

TypeDefinition SelectTypeDefinition (string javaName, List<TypeDefinition> 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<TypeDefinition> ());
}
list.Add (type);
}

if (!type.HasNestedTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> ();");

using var b = CreateApkBuilder ();
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
Expand Down Expand Up @@ -165,6 +170,43 @@ public void NativeAOT ()
Assert.IsNotNull (method, $"{linkedMonoAndroidAssembly} should contain {typeName}.{methodName}");
}

var typemap = new Dictionary<string, TypeReference> ();
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) {
Expand All @@ -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]
Expand Down
Loading