Skip to content

[nativeaot] fix typemap logic involving duplicates #9794

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 38 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,21 +77,53 @@ 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;
// 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;
}
}
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> ());
}
// Types in Mono.Android assembly should be first in the list
if (assembly.Name.Name == "Mono.Android") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will result in an ordering that is potentially different from what MonoVM has.

The problem is "aliases": Mono.Android.dll has them, too! For example, there are (at least) three bindings for java/util/ArrayList:

  • Java.Util.ArrayList
  • Android.Runtime.JavaList
  • Android.Runtime.JavaList<T>

Which one "wins" currently? Android.Runtime.JavaList (non-generic). Why? TypeDefinition ordering within the assembly:

% monodis --typedef src/Mono.Android/obj/Debug/net10.0/android-35/Mono.Android.dll | grep 'Java.Util.ArrayList\|Android.Runtime.JavaList'
4951: Android.Runtime.JavaList (flist=36347, mlist=83422, flags=0x100001, extends=0x7c28)
4952: Android.Runtime.JavaList`1 (flist=36348, mlist=83468, flags=0x100001, extends=0x4d5c)
7399: Java.Util.ArrayList (flist=55415, mlist=134737, flags=0x100001, extends=0x7370)

Is this a problem? ¯\(ツ)/¯. It's just what it is now, and generally the default doesn't necessarily matter, because you'll be going through an Object.GetObject<T>() invocation which will specify the appropriate type.

Compare to what is done in the PR here: we have 3 candidate types, all in Mono.Android.dll, meaning we'll reverse the order. Thus, instead of preferring Android.Runtime.JavaList, this code will instead prefer Java.Util.ArrayList.

Again, is this a problem? I don't know, probably not? But it will be a change.

Is there a way to control the order of assemblies processed by the linker pipeline?

Looking at: https://github.com/dotnet/sdk/blob/a5393731b5b7b225692fff121f747fbbc9e8b140/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets#L124-L156

It feels like if we change @(ManagedAssemblyToLink) so that Mono.Android.dll is first, we'll get a similar result as what is done for MonoVM. Is it possible to reorder item group elements in that manner?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the logic, so these assertions work now:

AssertTypeMap ("java/util/ArrayList", "Android.Runtime.JavaList");
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!");

They did not previously work, but now I think the logic for NativeAOT is the same as described above.

list.Insert (0, type);
} else {
list.Add (type);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public void NativeAOT ()
proj.SetProperty ("PublishAot", "true");
proj.SetProperty ("PublishAotUsingRuntimePack", "true");
proj.SetProperty ("AndroidNdkDirectory", AndroidNdkPath);
proj.SetProperty ("_ExtraTrimmerArgs", "--verbose");

using var b = CreateApkBuilder ();
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
Expand Down Expand Up @@ -165,6 +166,38 @@ 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 ("android/app/Activity", "Android.App.Activity");
AssertTypeMap ("android/widget/Button", "Android.Widget.Button");

// Special *Invoker case
Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput,
"ILLink: Duplicate typemap entry for android/view/View$OnClickListener => Android.Views.View/IOnClickListenerInvoker"),
"Should get log message about duplicate IOnClickListenerInvoker!");
AssertTypeMap ("android/view/View$OnClickListener", "Android.Views.View/IOnClickListener");
}

var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex");
FileAssert.Exists (dexFile);
foreach (var className in mono_classes) {
Expand All @@ -180,6 +213,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
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy