From a760281bb1c86baeedc54e3bb5333063bfcd48aa Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 7 Sep 2022 16:02:42 +0200 Subject: [PATCH] Marshal methods runtime support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: 903ba37ce70d2840983774e1d6fb55f8002561e2 Context: e1af9587bb98d4c249bbc392ceccc2b53ffff155 Context: https://github.com/xamarin/java.interop/issues/1027 Commit 903ba37c mentioned a TODO: > Update/rewrite infrastructure to focus on implementing the runtime > side of marshal methods, making it possible to actually run > applications which use marshal methods. Implement the necessary runtime elements to enable running of applications with marshal methods. It is now possible, if LLVM marshal methods are enabled/`ENABLE_MARSHAL_METHODS` is defined, to run both plain .NET SDK for Android and MAUI apps. Update `src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.InteropServices.xml` so that `System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute` is always preserved, as it is required by LLVM Marshal Methods. The `[UnmanagedCallersOnly]` attribute used by marshal methods requires that the invoked method have [blittable types][0] for the method return type and all parameter types. Unfortunately, `bool` is *not* a blittable type. Implement generation of wrapper methods which replace `bool` with `byte` and convert the values appropriately before calling the actual target method. In a "hello world" MAUI test app there are 133 such methods (out of around 180 total). Implement code that enables us to show error messages with the proper assembly, class and method names on failure to look up or obtain pointer to native callback methods. TODO: * Process *all* assemblies, including `Mono.Android.dll`, for Java Callable Wrapper generation. This is necessary so that we can find and emit LLVM marshal methods for types defined within `Mono.Android.dll`. * Remove the `ENABLE_MARSHAL_METHODS` define, and enable LLVM marshal methods for everyone. * Update `` to rewrite all assemblies for all Supported ABIs. Currently, we don't support `Java.Lang.Object` & `Java.Lang.Throwable` subclasses being located in per-ABI assemblies. * How do `Java_…` native functions interact with `JNIEnv::RegisterNatives()`? https://github.com/xamarin/xamarin-android/pull/7285#discussion_r951755971 * *Can* JNI `native` methods contain "non-printable" characters such as `\n`, or "non-representable in ELF symbols" characters such as `-` (e.g. Kotlin mangled methods)? https://github.com/xamarin/xamarin-android/pull/7285#discussion_r951789869 * Cleanup, cleanup, cleanup [0]: https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types --- .../System.Runtime.InteropServices.xml | 6 + .../Android.Runtime/AndroidRuntime.cs | 91 ++++- src/Mono.Android/Mono.Android.csproj | 4 + .../Tasks/GenerateJavaStubs.cs | 23 +- .../Tasks/GeneratePackageManagerJava.cs | 11 +- .../PackagingTest.cs | 1 + .../LlvmIrGenerator/LlvmIrGenerator.cs | 4 +- .../MarshalMethodsAssemblyRewriter.cs | 188 ++++++++- .../Utilities/MarshalMethodsClassifier.cs | 243 ++++++++++-- .../Utilities/MarshalMethodsHelpers.cs | 35 ++ .../MarshalMethodsNativeAssemblyGenerator.cs | 361 ++++++++++++++---- src/monodroid/jni/application_dso_stub.cc | 17 + src/monodroid/jni/monodroid-glue-internal.hh | 8 +- src/monodroid/jni/monodroid-glue.cc | 9 +- .../jni/xamarin-android-app-context.cc | 120 +++++- src/monodroid/jni/xamarin-app.hh | 18 + 16 files changed, 1008 insertions(+), 131 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.InteropServices.xml create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.InteropServices.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.InteropServices.xml new file mode 100644 index 00000000000..653e1295da5 --- /dev/null +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/System.Runtime.InteropServices.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 74b08e63670..3bfffb9001b 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -473,8 +473,28 @@ static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments argu public override void RegisterNativeMembers (JniType nativeClass, Type type, string? methods) => RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); +#if ENABLE_MARSHAL_METHODS + // Temporary hack, see comments in RegisterNativeMembers below + static readonly Dictionary dynamicRegistrationMethods = new Dictionary (StringComparer.Ordinal) { + {"Android.Views.View+IOnLayoutChangeListenerImplementor", new string[] { "GetOnLayoutChange_Landroid_view_View_IIIIIIIIHandler" }}, + {"Android.Views.View+IOnLayoutChangeListenerInvoker", new string[] { "GetOnLayoutChange_Landroid_view_View_IIIIIIIIHandler" }}, + {"Java.Interop.TypeManager+JavaTypeManager", new string[] { "GetActivateHandler" }}, + }; +#endif + public void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { +#if ENABLE_MARSHAL_METHODS + Logger.Log (LogLevel.Info, "monodroid-mm", $"RegisterNativeMembers ('{nativeClass?.Name}', '{type?.FullName}', '{methods.ToString ()}')"); + Logger.Log (LogLevel.Info, "monodroid-mm", "RegisterNativeMembers called from:"); + var st = new StackTrace (true); + Logger.Log (LogLevel.Info, "monodroid-mm", st.ToString ()); + + if (methods.IsEmpty) { + Logger.Log (LogLevel.Info, "monodroid-mm", "No methods to register, returning"); + return; + } +#endif try { if (FastRegisterNativeMembers (nativeClass, type, methods)) return; @@ -497,6 +517,9 @@ public void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan< MethodInfo []? typeMethods = null; ReadOnlySpan methodsSpan = methods; +#if ENABLE_MARSHAL_METHODS + bool needToRegisterNatives = false; +#endif while (!methodsSpan.IsEmpty) { int newLineIndex = methodsSpan.IndexOf ('\n'); @@ -508,7 +531,7 @@ public void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan< out ReadOnlySpan callbackString, out ReadOnlySpan callbackDeclaringTypeString); - Delegate callback; + Delegate? callback = null; if (callbackString.SequenceEqual ("__export__")) { var mname = name.Slice (2); MethodInfo? minfo = null; @@ -522,6 +545,9 @@ public void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan< if (minfo == null) throw new InvalidOperationException (String.Format ("Specified managed method '{0}' was not found. Signature: {1}", mname.ToString (), signature.ToString ())); callback = CreateDynamicCallback (minfo); +#if ENABLE_MARSHAL_METHODS + needToRegisterNatives = true; +#endif } else { Type callbackDeclaringType = type; if (!callbackDeclaringTypeString.IsEmpty) { @@ -530,20 +556,73 @@ public void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan< while (callbackDeclaringType.ContainsGenericParameters) { callbackDeclaringType = callbackDeclaringType.BaseType!; } - GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), - callbackDeclaringType, callbackString.ToString ()); - callback = connector (); +#if ENABLE_MARSHAL_METHODS + // TODO: this is temporary hack, it needs a full fledged registration mechanism for methods like these (that is, ones which + // aren't registered with [Register] but are baked into Mono.Android's managed and Java code) + bool createCallback = false; + string declaringTypeName = callbackDeclaringType.FullName; + string callbackName = callbackString.ToString (); + + foreach (var kvp in dynamicRegistrationMethods) { + string dynamicTypeName = kvp.Key; + + foreach (string dynamicCallbackMethodName in kvp.Value) { + if (ShouldRegisterDynamically (declaringTypeName, callbackName, dynamicTypeName, dynamicCallbackMethodName)) { + createCallback = true; + break; + } + } + + if (createCallback) { + break; + } + } + + if (createCallback) { + Logger.Log (LogLevel.Info, "monodroid-mm", $" creating delegate for: '{callbackString.ToString()}' in type {callbackDeclaringType.FullName}"); +#endif + GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), + callbackDeclaringType, callbackString.ToString ()); + callback = connector (); +#if ENABLE_MARSHAL_METHODS + } else { + Logger.Log (LogLevel.Warn, "monodroid-mm", $" would try to create delegate for: '{callbackString.ToString()}' in type {callbackDeclaringType.FullName}"); + } +#endif + } + + if (callback != null) { +#if ENABLE_MARSHAL_METHODS + needToRegisterNatives = true; +#endif + natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); } - natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); } methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; } - JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, natives.Length); +#if ENABLE_MARSHAL_METHODS + if (needToRegisterNatives) { +#endif + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, nativesIndex); +#if ENABLE_MARSHAL_METHODS + } +#endif } catch (Exception e) { JniEnvironment.Runtime.RaisePendingException (e); } + +#if ENABLE_MARSHAL_METHODS + bool ShouldRegisterDynamically (string callbackTypeName, string callbackString, string typeName, string callbackName) + { + if (String.Compare (typeName, callbackTypeName, StringComparison.Ordinal) != 0) { + return false; + } + + return String.Compare (callbackName, callbackString, StringComparison.Ordinal) == 0; + } +#endif } static int CountMethods (ReadOnlySpan methodsSpan) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 47c4842f26f..0572ca73865 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -49,6 +49,10 @@ $([System.IO.Path]::GetFullPath ('$(OutputPath)$(AssemblyName).dll')) + + $(DefineConstants);ENABLE_MARSHAL_METHODS + + $(OutputPath)..\v1.0\mscorlib.dll diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 14a17a1c295..6ff7428ad7e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -217,6 +217,9 @@ void Run (DirectoryAssemblyResolver res) #if ENABLE_MARSHAL_METHODS if (!Debug) { + // TODO: we must rewrite assemblies for all SupportedAbis. Alternatively, we need to copy the ones that are identical + // Cecil does **not** guarantee that the same assembly modified twice in the same will yield the same result - tokens may differ, so can + // MVID. var rewriter = new MarshalMethodsAssemblyRewriter (classifier.MarshalMethods, classifier.Assemblies, marshalMethodsAssemblyPaths, Log); rewriter.Rewrite (res); } @@ -349,6 +352,11 @@ void Run (DirectoryAssemblyResolver res) regCallsWriter.WriteLine ("\t\t// Application and Instrumentation ACWs must be registered first."); foreach (var type in javaTypes) { if (JavaNativeTypeManager.IsApplication (type, cache) || JavaNativeTypeManager.IsInstrumentation (type, cache)) { +#if ENABLE_MARSHAL_METHODS + if (!classifier.FoundDynamicallyRegisteredMethods (type)) { + continue; + } +#endif string javaKey = JavaNativeTypeManager.ToJniName (type, cache).Replace ('/', '.'); regCallsWriter.WriteLine ("\t\tmono.android.Runtime.register (\"{0}\", {1}.class, {1}.__md_methods);", type.GetAssemblyQualifiedName (cache), javaKey); @@ -362,12 +370,25 @@ void Run (DirectoryAssemblyResolver res) template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ())); #if ENABLE_MARSHAL_METHODS + if (!Debug) { + Log.LogDebugMessage ($"Number of generated marshal methods: {classifier.MarshalMethods.Count}"); + + if (classifier.RejectedMethodCount > 0) { + Log.LogWarning ($"Number of methods in the project that will be registered dynamically: {classifier.RejectedMethodCount}"); + } + + if (classifier.WrappedMethodCount > 0) { + Log.LogWarning ($"Number of methods in the project that need marshal method wrappers: {classifier.WrappedMethodCount}"); + } + } + void StoreMarshalAssemblyPath (string name, ITaskItem asm) { if (Debug) { return; } + // TODO: we need to keep paths to ALL the assemblies, we need to rewrite them for all RIDs eventually. Right now we rewrite them just for one RID if (!marshalMethodsAssemblyPaths.TryGetValue (name, out HashSet assemblyPaths)) { assemblyPaths = new HashSet (); marshalMethodsAssemblyPaths.Add (name, assemblyPaths); @@ -406,7 +427,7 @@ bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCac jti.Generate (writer); #if ENABLE_MARSHAL_METHODS if (!Debug) { - if (classifier.FoundDynamicallyRegisteredMethods) { + if (classifier.FoundDynamicallyRegisteredMethods (t)) { Log.LogWarning ($"Type '{t.GetAssemblyQualifiedName ()}' will register some of its Java override methods dynamically. This may adversely affect runtime performance. See preceding warnings for names of dynamically registered methods."); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs index 16105940dd2..e06e300a462 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs @@ -444,11 +444,12 @@ void AddEnvironment () #if ENABLE_MARSHAL_METHODS var marshalMethodsState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (GenerateJavaStubs.MarshalMethodsRegisterTaskKey, RegisteredTaskObjectLifetime.Build); - var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator { - NumberOfAssembliesInApk = assemblyCount, - UniqueAssemblyNames = uniqueAssemblyNames, - MarshalMethods = marshalMethodsState?.MarshalMethods, - }; + var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator ( + assemblyCount, + uniqueAssemblyNames, + marshalMethodsState?.MarshalMethods, + Log + ); marshalMethodsAsmGen.Init (); #endif foreach (string abi in SupportedAbis) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 6a4b9917b44..b239c6b7d76 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -91,6 +91,7 @@ public void CheckIncludedAssemblies ([Values (false, true)] bool usesAssemblySto "System.Console.dll", "System.Private.CoreLib.dll", "System.Runtime.dll", + "System.Runtime.InteropServices.dll", "System.Linq.dll", "UnnamedProject.dll", //NOTE: appeared in .NET 7.0.100-rc.1.22423.7 diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 48f5db5964d..eafeb39e173 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -588,10 +588,10 @@ public void WriteStructureArray (StructureInfo info, IList (info, instances, LlvmIrVariableOptions.Default, symbolName, writeFieldComment, initialComment); } - public void WriteArray (IList values, string symbolName) + public void WriteArray (IList values, string symbolName, string? initialComment = null) { WriteEOL (); - WriteEOL (symbolName); + WriteEOL (initialComment ?? symbolName); ulong arrayStringCounter = 0; var strings = new List (); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs index 24e841e5252..8af1d3878ea 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -7,6 +7,7 @@ using Microsoft.Android.Build.Tasks; using Microsoft.Build.Utilities; using Mono.Cecil; +using Mono.Cecil.Cil; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks @@ -34,13 +35,40 @@ public void Rewrite (DirectoryAssemblyResolver resolver) unmanagedCallersOnlyAttributes.Add (asm, CreateImportedUnmanagedCallersOnlyAttribute (asm, unmanagedCallersOnlyAttributeCtor)); } + Console.WriteLine (); + Console.WriteLine ("Modifying assemblies"); + + var processedMethods = new Dictionary (StringComparer.Ordinal); Console.WriteLine ("Adding the [UnmanagedCallersOnly] attribute to native callback methods and removing unneeded fields+methods"); foreach (IList methodList in methods.Values) { foreach (MarshalMethodEntry method in methodList) { - Console.WriteLine ($"\t{method.NativeCallback.FullName} (token: 0x{method.NativeCallback.MetadataToken.RID:x})"); - method.NativeCallback.CustomAttributes.Add (unmanagedCallersOnlyAttributes [method.NativeCallback.Module.Assembly]); - method.Connector.DeclaringType.Methods.Remove (method.Connector); - method.CallbackField?.DeclaringType.Fields.Remove (method.CallbackField); + string fullNativeCallbackName = method.NativeCallback.FullName; + if (processedMethods.TryGetValue (fullNativeCallbackName, out MethodDefinition nativeCallbackWrapper)) { + method.NativeCallbackWrapper = nativeCallbackWrapper; + continue; + } + + Console.WriteLine ($"\t{fullNativeCallbackName} (token: 0x{method.NativeCallback.MetadataToken.RID:x})"); + Console.WriteLine ($"\t Top type == '{method.DeclaringType}'"); + Console.WriteLine ($"\t NativeCallback == '{method.NativeCallback}'"); + Console.WriteLine ($"\t Connector == '{method.Connector}'"); + Console.WriteLine ($"\t method.NativeCallback.CustomAttributes == {ToStringOrNull (method.NativeCallback.CustomAttributes)}"); + Console.WriteLine ($"\t method.Connector.DeclaringType == {ToStringOrNull (method.Connector?.DeclaringType)}"); + Console.WriteLine ($"\t method.Connector.DeclaringType.Methods == {ToStringOrNull (method.Connector.DeclaringType?.Methods)}"); + Console.WriteLine ($"\t method.CallbackField == {ToStringOrNull (method.CallbackField)}"); + Console.WriteLine ($"\t method.CallbackField?.DeclaringType == {ToStringOrNull (method.CallbackField?.DeclaringType)}"); + Console.WriteLine ($"\t method.CallbackField?.DeclaringType.Fields == {ToStringOrNull (method.CallbackField?.DeclaringType?.Fields)}"); + + if (method.NeedsBlittableWorkaround) { + method.NativeCallbackWrapper = GenerateBlittableWrapper (method, unmanagedCallersOnlyAttributes); + } else { + method.NativeCallback.CustomAttributes.Add (unmanagedCallersOnlyAttributes [method.NativeCallback.Module.Assembly]); + } + + method.Connector?.DeclaringType?.Methods?.Remove (method.Connector); + method.CallbackField?.DeclaringType?.Fields?.Remove (method.CallbackField); + + processedMethods.Add (fullNativeCallbackName, method.NativeCallback); } } @@ -97,7 +125,159 @@ void MoveFile (string source, string target) File.Delete (source); } catch (Exception ex) { log.LogWarning ($"Unable to delete source file '{source}' when moving it to '{target}'"); + log.LogDebugMessage (ex.ToString ()); + } + } + + string ToStringOrNull (object? o) + { + if (o == null) { + return "'null'"; + } + + return o.ToString (); + } + } + + MethodDefinition GenerateBlittableWrapper (MarshalMethodEntry method, Dictionary unmanagedCallersOnlyAttributes) + { + Console.WriteLine ($"\t Generating blittable wrapper for: {method.NativeCallback.FullName}"); + MethodDefinition callback = method.NativeCallback; + string wrapperName = $"{callback.Name}_mm_wrapper"; + TypeReference retType = MapToBlittableTypeIfNecessary (callback.ReturnType, out bool returnTypeMapped); + bool hasReturnValue = String.Compare ("System.Void", retType.FullName, StringComparison.Ordinal) != 0; + var wrapperMethod = new MethodDefinition (wrapperName, callback.Attributes, retType); + callback.DeclaringType.Methods.Add (wrapperMethod); + wrapperMethod.CustomAttributes.Add (unmanagedCallersOnlyAttributes [callback.Module.Assembly]); + + MethodBody body = wrapperMethod.Body; + int nparam = 0; + + foreach (ParameterDefinition pdef in callback.Parameters) { + TypeReference newType = MapToBlittableTypeIfNecessary (pdef.ParameterType, out _); + wrapperMethod.Parameters.Add (new ParameterDefinition (pdef.Name, pdef.Attributes, newType)); + + OpCode ldargOp; + bool paramRef = false; + switch (nparam++) { + case 0: + ldargOp = OpCodes.Ldarg_0; + break; + + case 1: + ldargOp = OpCodes.Ldarg_1; + break; + + case 2: + ldargOp = OpCodes.Ldarg_2; + break; + + case 3: + ldargOp = OpCodes.Ldarg_3; + break; + + default: + ldargOp = OpCodes.Ldarg_S; + paramRef = true; + break; + } + + Instruction ldarg; + + if (!paramRef) { + ldarg = Instruction.Create (ldargOp); + } else { + ldarg = Instruction.Create (ldargOp, pdef); + } + + body.Instructions.Add (ldarg); + + if (!pdef.ParameterType.IsBlittable ()) { + GenerateNonBlittableConversion (pdef.ParameterType, newType); + } + } + + body.Instructions.Add (Instruction.Create (OpCodes.Call, callback)); + + if (hasReturnValue && returnTypeMapped) { + GenerateRetValCast (callback.ReturnType, retType); + } + + body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + Console.WriteLine ($"\t New method: {wrapperMethod.FullName}"); + return wrapperMethod; + + void GenerateNonBlittableConversion (TypeReference sourceType, TypeReference targetType) + { + if (IsBooleanConversion (sourceType, targetType)) { + // We output equivalent of the `param != 0` C# code + body.Instructions.Add (Instruction.Create (OpCodes.Ldc_I4_0)); + body.Instructions.Add (Instruction.Create (OpCodes.Cgt_Un)); + return; + } + + ThrowUnsupportedType (sourceType); + } + + void GenerateRetValCast (TypeReference sourceType, TypeReference targetType) + { + if (IsBooleanConversion (sourceType, targetType)) { + var insLoadOne = Instruction.Create (OpCodes.Ldc_I4_1); + var insConvert = Instruction.Create (OpCodes.Conv_U1); + + body.Instructions.Add (Instruction.Create (OpCodes.Brtrue_S, insLoadOne)); + body.Instructions.Add (Instruction.Create (OpCodes.Ldc_I4_0)); + body.Instructions.Add (Instruction.Create (OpCodes.Br_S, insConvert)); + body.Instructions.Add (insLoadOne); + body.Instructions.Add (insConvert); + return; } + + ThrowUnsupportedType (sourceType); + } + + bool IsBooleanConversion (TypeReference sourceType, TypeReference targetType) + { + if (String.Compare ("System.Boolean", sourceType.FullName, StringComparison.Ordinal) == 0) { + if (String.Compare ("System.Byte", targetType.FullName, StringComparison.Ordinal) != 0) { + throw new InvalidOperationException ($"Unexpected conversion from '{sourceType.FullName}' to '{targetType.FullName}'"); + } + + return true; + } + + return false; + } + + void ThrowUnsupportedType (TypeReference type) + { + throw new InvalidOperationException ($"Unsupported non-blittable type '{type.FullName}'"); + } + } + + TypeReference MapToBlittableTypeIfNecessary (TypeReference type, out bool typeMapped) + { + if (type.IsBlittable () || String.Compare ("System.Void", type.FullName, StringComparison.Ordinal) == 0) { + typeMapped = false; + return type; + } + + if (String.Compare ("System.Boolean", type.FullName, StringComparison.Ordinal) == 0) { + // Maps to Java JNI's jboolean which is an unsigned 8-bit type + typeMapped = true; + return ReturnValid (typeof(byte)); + } + + throw new NotSupportedException ($"Cannot map unsupported blittable type '{type.FullName}'"); + + TypeReference ReturnValid (Type typeToLookUp) + { + TypeReference? mappedType = type.Module.Assembly.MainModule.ImportReference (typeToLookUp); + if (mappedType == null) { + throw new InvalidOperationException ($"Unable to obtain reference to type '{typeToLookUp.FullName}'"); + } + + return mappedType; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs index a6801fa6d13..478c52389d0 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Java.Interop.Tools.Cecil; using Java.Interop.Tools.JavaCallableWrappers; @@ -13,22 +14,36 @@ namespace Xamarin.Android.Tasks #if ENABLE_MARSHAL_METHODS public sealed class MarshalMethodEntry { - public TypeDefinition DeclaringType { get; } - public MethodDefinition NativeCallback { get; } - public MethodDefinition Connector { get; } - public MethodDefinition RegisteredMethod { get; } - public MethodDefinition ImplementedMethod { get; } - public FieldDefinition CallbackField { get; } - public string JniTypeName { get; } - public string JniMethodName { get; } - public string JniMethodSignature { get; } + /// + /// The "real" native callback, used if it doesn't contain any non-blittable types in its parameters + /// or return type. + /// + MethodDefinition nativeCallbackReal; + + /// + /// Used only when is true. This wrapper is generated by + /// when rewriting assemblies, for methods which have either + /// a non-blittable return type or a parameter of a non-blittable type. + /// + public MethodDefinition? NativeCallbackWrapper { get; set; } + public TypeDefinition DeclaringType { get; } + public MethodDefinition Connector { get; } + public MethodDefinition RegisteredMethod { get; } + public MethodDefinition ImplementedMethod { get; } + public FieldDefinition CallbackField { get; } + public string JniTypeName { get; } + public string JniMethodName { get; } + public string JniMethodSignature { get; } + public bool NeedsBlittableWorkaround { get; } + + public MethodDefinition NativeCallback => NativeCallbackWrapper ?? nativeCallbackReal; public MarshalMethodEntry (TypeDefinition declaringType, MethodDefinition nativeCallback, MethodDefinition connector, MethodDefinition registeredMethod, MethodDefinition implementedMethod, FieldDefinition callbackField, string jniTypeName, - string jniName, string jniSignature) + string jniName, string jniSignature, bool needsBlittableWorkaround) { DeclaringType = declaringType ?? throw new ArgumentNullException (nameof (declaringType)); - NativeCallback = nativeCallback ?? throw new ArgumentNullException (nameof (nativeCallback)); + nativeCallbackReal = nativeCallback ?? throw new ArgumentNullException (nameof (nativeCallback)); Connector = connector ?? throw new ArgumentNullException (nameof (connector)); RegisteredMethod = registeredMethod ?? throw new ArgumentNullException (nameof (registeredMethod)); ImplementedMethod = implementedMethod ?? throw new ArgumentNullException (nameof (implementedMethod)); @@ -36,6 +51,7 @@ public MarshalMethodEntry (TypeDefinition declaringType, MethodDefinition native JniTypeName = EnsureNonEmpty (jniTypeName, nameof (jniTypeName)); JniMethodName = EnsureNonEmpty (jniName, nameof (jniName)); JniMethodSignature = EnsureNonEmpty (jniSignature, nameof (jniSignature)); + NeedsBlittableWorkaround = needsBlittableWorkaround; } string EnsureNonEmpty (string s, string argName) @@ -105,21 +121,38 @@ sealed class NativeCallbackSignature : IMethodSignatureMatcher public NativeCallbackSignature (MethodDefinition target, TaskLoggingHelper log) { this.log = log; - returnType = MapType (target.ReturnType.FullName); + returnType = MapType (target.ReturnType); paramTypes = new List { "System.IntPtr", // jnienv "System.IntPtr", // native__this }; foreach (ParameterDefinition pd in target.Parameters) { - paramTypes.Add (MapType (pd.ParameterType.FullName)); + paramTypes.Add (MapType (pd.ParameterType)); } } - string MapType (string type) + string MapType (TypeReference typeRef) { - if (verbatimTypes.Contains (type)) { - return type; + string? typeName = null; + if (!typeRef.IsGenericParameter && !typeRef.IsArray) { + TypeDefinition typeDef = typeRef.Resolve (); + if (typeDef == null) { + throw new InvalidOperationException ($"Unable to resolve type '{typeRef.FullName}'"); + } + + if (typeDef.IsEnum) { + // TODO: get the underlying type + return "System.Int32"; + } + } + + if (String.IsNullOrEmpty (typeName)) { + typeName = typeRef.FullName; + } + + if (verbatimTypes.Contains (typeName)) { + return typeName; } return "System.IntPtr"; @@ -128,19 +161,27 @@ string MapType (string type) public bool Matches (MethodDefinition method) { if (method.Parameters.Count != paramTypes.Count || !method.IsStatic) { - log.LogDebugMessage ($"Method '{method.FullName}' doesn't match native callback signature (invalid parameter count or not static)"); + log.LogWarning ($"Method '{method.FullName}' doesn't match native callback signature (invalid parameter count or not static)"); return false; } if (String.Compare (returnType, method.ReturnType.FullName, StringComparison.Ordinal) != 0) { - log.LogDebugMessage ($"Method '{method.FullName}' doesn't match native callback signature (invalid return type)"); + log.LogWarning ($"Method '{method.FullName}' doesn't match native callback signature (invalid return type)"); return false; } for (int i = 0; i < method.Parameters.Count; i++) { ParameterDefinition pd = method.Parameters[i]; - if (String.Compare (pd.ParameterType.FullName, paramTypes[i], StringComparison.Ordinal) != 0) { - log.LogDebugMessage ($"Method '{method.FullName}' doesn't match native callback signature, expected parameter type '{paramTypes[i]}' at position {i}, found '{pd.ParameterType.FullName}'"); + string parameterTypeName; + + if (pd.ParameterType.IsArray) { + parameterTypeName = $"{pd.ParameterType.FullName}[]"; + } else { + parameterTypeName = pd.ParameterType.FullName; + } + + if (String.Compare (parameterTypeName, paramTypes[i], StringComparison.Ordinal) != 0) { + log.LogWarning ($"Method '{method.FullName}' doesn't match native callback signature, expected parameter type '{paramTypes[i]}' at position {i}, found '{parameterTypeName}'"); return false; } } @@ -154,11 +195,14 @@ public bool Matches (MethodDefinition method) Dictionary> marshalMethods; HashSet assemblies; TaskLoggingHelper log; - bool haveDynamicMethods; + HashSet typesWithDynamicallyRegisteredMethods; + ulong rejectedMethodCount = 0; + ulong wrappedMethodCount = 0; public IDictionary> MarshalMethods => marshalMethods; public ICollection Assemblies => assemblies; - public bool FoundDynamicallyRegisteredMethods => haveDynamicMethods; + public ulong RejectedMethodCount => rejectedMethodCount; + public ulong WrappedMethodCount => wrappedMethodCount; #endif public MarshalMethodsClassifier (TypeDefinitionCache tdCache, DirectoryAssemblyResolver res, TaskLoggingHelper log) @@ -169,6 +213,7 @@ public MarshalMethodsClassifier (TypeDefinitionCache tdCache, DirectoryAssemblyR resolver = res ?? throw new ArgumentNullException (nameof (tdCache)); marshalMethods = new Dictionary> (StringComparer.Ordinal); assemblies = new HashSet (); + typesWithDynamicallyRegisteredMethods = new HashSet (); #endif } @@ -191,12 +236,17 @@ public override bool ShouldBeDynamicallyRegistered (TypeDefinition topType, Meth return false; } - haveDynamicMethods = true; + typesWithDynamicallyRegisteredMethods.Add (topType); #endif // def ENABLE_MARSHAL_METHODS return true; } #if ENABLE_MARSHAL_METHODS + public bool FoundDynamicallyRegisteredMethods (TypeDefinition type) + { + return typesWithDynamicallyRegisteredMethods.Contains (type); + } + bool IsDynamicallyRegistered (TypeDefinition topType, MethodDefinition registeredMethod, MethodDefinition implementedMethod, CustomAttribute registerAttribute) { Console.WriteLine ($"Classifying:\n\tmethod: {implementedMethod.FullName}\n\tregistered method: {registeredMethod.FullName})\n\tAttr: {registerAttribute.AttributeType.FullName} (parameter count: {registerAttribute.ConstructorArguments.Count})"); @@ -215,6 +265,7 @@ bool IsDynamicallyRegistered (TypeDefinition topType, MethodDefinition registere } log.LogWarning ($"Method '{registeredMethod.FullName}' will be registered dynamically"); + rejectedMethodCount++; return true; } @@ -232,6 +283,9 @@ bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodD return false; } + // TODO: if we can't find native callback and/or delegate field using `callbackNameCore`, fall back to `jniName` (which is the first argument to the `[Register]` + // attribute). Or simply use `jniName` at once - needs testing. + string callbackNameCore = connectorName.Substring (HandlerNameStart.Length, connectorName.Length - HandlerNameStart.Length - HandlerNameEnd.Length); string nativeCallbackName = $"n_{callbackNameCore}"; string delegateFieldName = $"cb_{Char.ToLowerInvariant (callbackNameCore[0])}{callbackNameCore.Substring (1)}"; @@ -253,7 +307,11 @@ bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodD var ncbs = new NativeCallbackSignature (registeredMethod, log); MethodDefinition nativeCallbackMethod = FindMethod (connectorDeclaringType, nativeCallbackName, ncbs); if (nativeCallbackMethod == null) { - log.LogWarning ($"\tUnable to find native callback method matching the '{registeredMethod.FullName}' signature"); + log.LogWarning ($"\tUnable to find native callback method '{nativeCallbackName}' in type '{connectorDeclaringType.FullName}', matching the '{registeredMethod.FullName}' signature (jniName: '{jniName}')"); + return false; + } + + if (!EnsureIsValidUnmanagedCallersOnlyTarget (nativeCallbackMethod, out bool needsBlittableWorkaround)) { return false; } @@ -267,8 +325,40 @@ bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodD } } + // TODO: check where DeclaringType is lost between here and rewriter, for: + // + // Classifying: + // method: Java.Lang.Object Microsoft.Maui.Controls.Platform.Compatibility.ShellSearchViewAdapter::GetItem(System.Int32) + // registered method: Java.Lang.Object Android.Widget.BaseAdapter::GetItem(System.Int32)) + // Attr: Android.Runtime.RegisterAttribute (parameter count: 3) + // Top type: Microsoft.Maui.Controls.Platform.Compatibility.ShellSearchViewAdapter + // Managed type: Android.Widget.BaseAdapter, Mono.Android + // connector: GetGetItem_IHandler (from spec: 'GetGetItem_IHandler') + // connector name: GetGetItem_IHandler + // native callback name: n_GetItem_I + // delegate field name: cb_getItem_I + // ##G1: Microsoft.Maui.Controls.Platform.Compatibility.ShellSearchViewAdapter -> crc640ec207abc449b2ca/ShellSearchViewAdapter + // ##G1: top type: Microsoft.Maui.Controls.Platform.Compatibility.ShellSearchViewAdapter -> crc640ec207abc449b2ca/ShellSearchViewAdapter + // ##G1: connectorMethod: System.Delegate Android.Widget.BaseAdapter::GetGetItem_IHandler() + // ##G1: delegateField: System.Delegate Android.Widget.BaseAdapter::cb_getItem_I + // + // And in the rewriter: + // + // System.IntPtr Android.Widget.BaseAdapter::n_GetItem_I(System.IntPtr,System.IntPtr,System.Int32) (token: 0x5fe3) + // Top type == 'Microsoft.Maui.Controls.Platform.Compatibility.ShellSearchViewAdapter' + // NativeCallback == 'System.IntPtr Android.Widget.BaseAdapter::n_GetItem_I(System.IntPtr,System.IntPtr,System.Int32)' + // Connector == 'System.Delegate GetGetItem_IHandler()' + // method.NativeCallback.CustomAttributes == Mono.Collections.Generic.Collection`1[Mono.Cecil.CustomAttribute] + // method.Connector.DeclaringType == 'null' + // method.Connector.DeclaringType.Methods == 'null' + // method.CallbackField == System.Delegate cb_getItem_I + // method.CallbackField?.DeclaringType == 'null' + // method.CallbackField?.DeclaringType.Fields == 'null' + Console.WriteLine ($"##G1: {implementedMethod.DeclaringType.FullName} -> {JavaNativeTypeManager.ToJniName (implementedMethod.DeclaringType, tdCache)}"); Console.WriteLine ($"##G1: top type: {topType.FullName} -> {JavaNativeTypeManager.ToJniName (topType, tdCache)}"); + Console.WriteLine ($"##G1: connectorMethod: {connectorMethod?.FullName}"); + Console.WriteLine ($"##G1: delegateField: {delegateField?.FullName}"); StoreMethod ( connectorName, @@ -282,7 +372,9 @@ bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodD delegateField, JavaNativeTypeManager.ToJniName (topType, tdCache), jniName, - jniSignature) + jniSignature, + needsBlittableWorkaround + ) ); StoreAssembly (connectorMethod.Module.Assembly); @@ -294,6 +386,92 @@ bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodD return true; } + bool EnsureIsValidUnmanagedCallersOnlyTarget (MethodDefinition method, out bool needsBlittableWorkaround) + { + needsBlittableWorkaround = false; + + // Requirements: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0#remarks + if (!method.IsStatic) { + return LogReasonWhyAndReturnFailure ($"is not static"); + } + + if (method.HasGenericParameters) { + return LogReasonWhyAndReturnFailure ($"has generic parameters"); + } + + TypeReference type; + bool needsWrapper = false; + if (String.Compare ("System.Void", method.ReturnType.FullName, StringComparison.Ordinal) != 0) { + type = GetRealType (method.ReturnType); + if (!IsAcceptable (type)) { + needsBlittableWorkaround = true; + WarnWhy ($"has a non-blittable return type '{type.FullName}'"); + needsWrapper = true; + } + } + + if (method.DeclaringType.HasGenericParameters) { + return LogReasonWhyAndReturnFailure ($"is declared in a type with generic parameters"); + } + + if (!method.HasParameters) { + return UpdateWrappedCountAndReturn (true); + } + + foreach (ParameterDefinition pdef in method.Parameters) { + type = GetRealType (pdef.ParameterType); + + if (!IsAcceptable (type)) { + needsBlittableWorkaround = true; + WarnWhy ($"has a parameter ({pdef.Name}) of non-blittable type '{type.FullName}'"); + needsWrapper = true; + } + } + + return UpdateWrappedCountAndReturn (true); + + bool UpdateWrappedCountAndReturn (bool retval) + { + if (needsWrapper) { + wrappedMethodCount++; + } + + return retval; + } + + bool IsAcceptable (TypeReference type) + { + if (type.IsArray) { + var array = new ArrayType (type); + if (array.Rank > 1) { + return false; + } + } + + return type.IsBlittable (); + } + + TypeReference GetRealType (TypeReference type) + { + if (type.IsArray) { + return type.GetElementType (); + } + + return type; + } + + bool LogReasonWhyAndReturnFailure (string why) + { + log.LogWarning ($"Method '{method.FullName}' {why}. It cannot be used with the `[UnmanagedCallersOnly]` attribute"); + return false; + } + + void WarnWhy (string why) + { + log.LogWarning ($"Method '{method.FullName}' {why}. A workaround is required, this may make the application slower"); + } + } + TypeDefinition FindType (AssemblyDefinition asm, string typeName) { foreach (ModuleDefinition md in asm.Modules) { @@ -370,18 +548,19 @@ FieldDefinition FindField (TypeDefinition type, string fieldName, bool lookForIn void StoreMethod (string connectorName, MethodDefinition registeredMethod, MarshalMethodEntry entry) { string typeName = registeredMethod.DeclaringType.FullName.Replace ('/', '+'); - string key = $"{typeName}, {registeredMethod.DeclaringType.GetPartialAssemblyName (tdCache)}\t{connectorName}"; - - // Several classes can override the same method, we need to generate the marshal method only once - if (marshalMethods.ContainsKey (key)) { - return; - } + string key = $"{typeName}, {registeredMethod.DeclaringType.GetPartialAssemblyName (tdCache)}\t{registeredMethod.Name}"; + // Several classes can override the same method, we need to generate the marshal method only once, at the same time + // keeping track of overloads if (!marshalMethods.TryGetValue (key, out IList list) || list == null) { list = new List (); marshalMethods.Add (key, list); } - list.Add (entry); + + string registeredName = $"{entry.DeclaringType.FullName}::{entry.ImplementedMethod.Name}"; + if (list.Count == 0 || !list.Any (me => String.Compare (registeredName, me.ImplementedMethod.FullName, StringComparison.Ordinal) == 0)) { + list.Add (entry); + } } void StoreAssembly (AssemblyDefinition asm) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs new file mode 100644 index 00000000000..84b5e488253 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +using Mono.Cecil; + +namespace Xamarin.Android.Tasks +{ + static class MarshalMethodsHelpers + { + // From: https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types + static readonly HashSet blittableTypes = new HashSet (StringComparer.Ordinal) { + "System.Byte", + "System.SByte", + "System.Int16", + "System.UInt16", + "System.Int32", + "System.UInt32", + "System.Int64", + "System.UInt64", + "System.IntPtr", + "System.UIntPtr", + "System.Single", + "System.Double", + }; + + public static bool IsBlittable (this TypeReference type) + { + if (type == null) { + throw new ArgumentNullException (nameof (type)); + } + + return blittableTypes.Contains (type.FullName); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs index 7ff24962a15..e34a3395f44 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs @@ -9,11 +9,15 @@ using Java.Interop.Tools.TypeNameMappings; using Java.Interop.Tools.JavaCallableWrappers; +using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Xamarin.Android.Tasks.LLVMIR; +using CecilMethodDefinition = global::Mono.Cecil.MethodDefinition; +using CecilParameterDefinition = global::Mono.Cecil.ParameterDefinition; + namespace Xamarin.Android.Tasks { class MarshalMethodsNativeAssemblyGenerator : LlvmIrComposer @@ -26,7 +30,7 @@ internal sealed class MonoClass sealed class _JNIEnv {} - // TODO: figure out why opaque classes like these have one byte field in clang's output + // Empty class must have at least one member so that the class address can be obtained [NativeClass] class _jobject { @@ -75,7 +79,7 @@ sealed class _jdoubleArray : _jarray sealed class MarshalMethodInfo { public MarshalMethodEntry Method { get; } - public string NativeSymbolName { get; } + public string NativeSymbolName { get; set; } public List Parameters { get; } public Type ReturnType { get; } public uint ClassCacheIndex { get; } @@ -128,6 +132,28 @@ sealed class MarshalMethodsManagedClass public string ClassName; }; + sealed class MarshalMethodNameDataProvider : NativeAssemblerStructContextDataProvider + { + public override string GetComment (object data, string fieldName) + { + var methodName = EnsureType (data); + + if (String.Compare ("id", fieldName, StringComparison.Ordinal) == 0) { + return $"id 0x{methodName.id:x}; name: {methodName.name}"; + } + + return String.Empty; + } + } + + [NativeAssemblerStructContextDataProvider (typeof(MarshalMethodNameDataProvider))] + sealed class MarshalMethodName + { + [NativeAssembler (UsesDataProvider = true)] + public ulong id; + public string name; + } + static readonly Dictionary jniSimpleTypeMap = new Dictionary { { 'Z', typeof(bool) }, { 'B', typeof(byte) }, @@ -151,12 +177,14 @@ sealed class MarshalMethodsManagedClass { 'L', typeof(_jobjectArray) }, }; - public ICollection UniqueAssemblyNames { get; set; } - public int NumberOfAssembliesInApk { get; set; } - public IDictionary> MarshalMethods { get; set; } + ICollection uniqueAssemblyNames; + int numberOfAssembliesInApk; + IDictionary> marshalMethods; + TaskLoggingHelper logger; StructureInfo monoImage; StructureInfo marshalMethodsClass; + StructureInfo marshalMethodName; StructureInfo monoClass; StructureInfo<_JNIEnv> _jniEnvSI; StructureInfo<_jobject> _jobjectSI; @@ -177,40 +205,109 @@ sealed class MarshalMethodsManagedClass List methods; List> classes = new List> (); + public MarshalMethodsNativeAssemblyGenerator (int numberOfAssembliesInApk, ICollection uniqueAssemblyNames, IDictionary> marshalMethods, TaskLoggingHelper logger) + { + this.numberOfAssembliesInApk = numberOfAssembliesInApk; + this.uniqueAssemblyNames = uniqueAssemblyNames ?? throw new ArgumentNullException (nameof (uniqueAssemblyNames)); + this.marshalMethods = marshalMethods; + this.logger = logger ?? throw new ArgumentNullException (nameof (logger)); + + if (uniqueAssemblyNames.Count != numberOfAssembliesInApk) { + throw new InvalidOperationException ("Internal error: number of assemblies in the apk doesn't match the number of unique assembly names"); + } + } + public override void Init () { - Console.WriteLine ($"Marshal methods count: {MarshalMethods?.Count ?? 0}"); - if (MarshalMethods == null || MarshalMethods.Count == 0) { + Console.WriteLine ($"Marshal methods count: {marshalMethods?.Count ?? 0}"); + if (marshalMethods == null || marshalMethods.Count == 0) { return; } var seenClasses = new Dictionary (StringComparer.Ordinal); - methods = new List (); - foreach (IList entryList in MarshalMethods.Values) { + var allMethods = new List (); + + // It's possible that several otherwise different methods (from different classes, but with the same + // names and similar signatures) will actually share the same **short** native symbol name. In this case we must + // ensure that they all use long symbol names. This has to be done as a post-processing step, after we + // have already iterated over the entire method collection. + // + // A handful of examples from the Hello World MAUI app: + // + // Overloaded MM: Java_crc64e1fb321c08285b90_CellAdapter_n_1onActionItemClicked + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean Android.Views.ActionMode/ICallback::OnActionItemClicked(Android.Views.ActionMode,Android.Views.IMenuItem)) + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean AndroidX.AppCompat.View.ActionMode/ICallback::OnActionItemClicked(AndroidX.AppCompat.View.ActionMode,Android.Views.IMenuItem)) + // new native symbol name: Java_crc64e1fb321c08285b90_CellAdapter_n_1onActionItemClicked__Landroidx_appcompat_view_ActionMode_2Landroid_view_MenuItem_2 + // + // Overloaded MM: Java_crc64e1fb321c08285b90_CellAdapter_n_1onCreateActionMode + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean Android.Views.ActionMode/ICallback::OnCreateActionMode(Android.Views.ActionMode,Android.Views.IMenu)) + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean AndroidX.AppCompat.View.ActionMode/ICallback::OnCreateActionMode(AndroidX.AppCompat.View.ActionMode,Android.Views.IMenu)) + // new native symbol name: Java_crc64e1fb321c08285b90_CellAdapter_n_1onCreateActionMode__Landroidx_appcompat_view_ActionMode_2Landroid_view_Menu_2 + // + // Overloaded MM: Java_crc64e1fb321c08285b90_CellAdapter_n_1onDestroyActionMode + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Void Android.Views.ActionMode/ICallback::OnDestroyActionMode(Android.Views.ActionMode)) + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Void AndroidX.AppCompat.View.ActionMode/ICallback::OnDestroyActionMode(AndroidX.AppCompat.View.ActionMode)) + // new native symbol name: Java_crc64e1fb321c08285b90_CellAdapter_n_1onDestroyActionMode__Landroidx_appcompat_view_ActionMode_2 + // + // Overloaded MM: Java_crc64e1fb321c08285b90_CellAdapter_n_1onPrepareActionMode + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean Android.Views.ActionMode/ICallback::OnPrepareActionMode(Android.Views.ActionMode,Android.Views.IMenu)) + // implemented in: Microsoft.Maui.Controls.Handlers.Compatibility.CellAdapter (System.Boolean AndroidX.AppCompat.View.ActionMode/ICallback::OnPrepareActionMode(AndroidX.AppCompat.View.ActionMode,Android.Views.IMenu)) + // new native symbol name: Java_crc64e1fb321c08285b90_CellAdapter_n_1onPrepareActionMode__Landroidx_appcompat_view_ActionMode_2Landroid_view_Menu_2 + // + var overloadedNativeSymbolNames = new Dictionary> (StringComparer.Ordinal); + foreach (IList entryList in marshalMethods.Values) { bool useFullNativeSignature = entryList.Count > 1; foreach (MarshalMethodEntry entry in entryList) { - ProcessAndAddMethod (entry, useFullNativeSignature, seenClasses); + ProcessAndAddMethod (allMethods, entry, useFullNativeSignature, seenClasses, overloadedNativeSymbolNames); + } + } + + foreach (List mmiList in overloadedNativeSymbolNames.Values) { + if (mmiList.Count <= 1) { + continue; + } + + Console.WriteLine ($"Overloaded MM: {mmiList[0].NativeSymbolName}"); + foreach (MarshalMethodInfo overloadedMethod in mmiList) { + Console.WriteLine ($" implemented in: {overloadedMethod.Method.DeclaringType.FullName} ({overloadedMethod.Method.RegisteredMethod.FullName})"); + overloadedMethod.NativeSymbolName = MakeNativeSymbolName (overloadedMethod.Method, useFullNativeSignature: true); + Console.WriteLine ($" new native symbol name: {overloadedMethod.NativeSymbolName}"); + } + } + + // In some cases it's possible that a single type implements two different interfaces which have methods with the same native signature: + // + // Microsoft.Maui.Controls.Handlers.TabbedPageManager/Listeners + // System.Void AndroidX.ViewPager.Widget.ViewPager/IOnPageChangeListener::OnPageSelected(System.Int32) + // System.Void AndroidX.ViewPager2.Widget.ViewPager2/OnPageChangeCallback::OnPageSelected(System.Int32) + // + // Both of the above methods will have the same native implementation and symbol name. e.g. (Java type name being `crc649ff77a65592e7d55/TabbedPageManager_Listeners`): + // Java_crc649ff77a65592e7d55_TabbedPageManager_1Listeners_n_1onPageSelected__I + // + // We need to de-duplicate the entries or the generated native code will fail to build. + var seenNativeSymbols = new HashSet (StringComparer.Ordinal); + methods = new List (); + + foreach (MarshalMethodInfo method in allMethods) { + if (seenNativeSymbols.Contains (method.NativeSymbolName)) { + logger.LogDebugMessage ($"Removed MM duplicate '{method.NativeSymbolName}' (implemented: {method.Method.ImplementedMethod.FullName}; registered: {method.Method.RegisteredMethod.FullName}"); + continue; } + + seenNativeSymbols.Add (method.NativeSymbolName); + methods.Add (method); } } - void ProcessAndAddMethod (MarshalMethodEntry entry, bool useFullNativeSignature, Dictionary seenClasses) + string MakeNativeSymbolName (MarshalMethodEntry entry, bool useFullNativeSignature) { - Console.WriteLine ("marshal method:"); - Console.WriteLine ($" top type: {entry.DeclaringType.FullName}"); - Console.WriteLine ($" registered method: [{entry.RegisteredMethod.DeclaringType.FullName}] {entry.RegisteredMethod.FullName}"); - Console.WriteLine ($" implemented method: [{entry.ImplementedMethod.DeclaringType.FullName}] {entry.ImplementedMethod.FullName}"); - Console.WriteLine ($" native callback: {entry.NativeCallback.FullName}"); - Console.WriteLine ($" connector: {entry.Connector.FullName}"); - Console.WriteLine ($" JNI name: {entry.JniMethodName}"); - Console.WriteLine ($" JNI signature: {entry.JniMethodSignature}"); - var sb = new StringBuilder ("Java_"); sb.Append (MangleForJni (entry.JniTypeName)); sb.Append ('_'); sb.Append (MangleForJni ($"n_{entry.JniMethodName}")); if (useFullNativeSignature) { + Console.WriteLine (" Using FULL signature"); string signature = entry.JniMethodSignature; if (signature.Length < 2) { ThrowInvalidSignature (signature, "must be at least two characters long"); @@ -232,20 +329,47 @@ void ProcessAndAddMethod (MarshalMethodEntry entry, bool useFullNativeSignature, } } - string klass = $"{entry.NativeCallback.DeclaringType.FullName}, {entry.NativeCallback.Module.Assembly.FullName}"; + return sb.ToString (); + + void ThrowInvalidSignature (string signature, string reason) + { + throw new InvalidOperationException ($"Invalid JNI signature '{signature}': {reason}"); + } + } + + void ProcessAndAddMethod (List allMethods, MarshalMethodEntry entry, bool useFullNativeSignature, Dictionary seenClasses, Dictionary> overloadedNativeSymbolNames) + { + Console.WriteLine ("marshal method:"); + Console.WriteLine ($" top type: {entry.DeclaringType.FullName} (token: 0x{entry.DeclaringType.MetadataToken.ToUInt32 ():x})"); + Console.WriteLine ($" registered method: [{entry.RegisteredMethod.DeclaringType.FullName}] {entry.RegisteredMethod.FullName}"); + Console.WriteLine ($" implemented method: [{entry.ImplementedMethod.DeclaringType.FullName}] {entry.ImplementedMethod.FullName}"); + Console.WriteLine ($" native callback: {entry.NativeCallback.FullName} (token: 0x{entry.NativeCallback.MetadataToken.ToUInt32 ():x})"); + Console.WriteLine ($" native callback wrapper: {entry.NativeCallbackWrapper}"); + Console.WriteLine ($" connector: {entry.Connector.FullName}"); + Console.WriteLine ($" JNI name: {entry.JniMethodName}"); + Console.WriteLine ($" JNI signature: {entry.JniMethodSignature}"); + + CecilMethodDefinition nativeCallback = entry.NativeCallback; + string nativeSymbolName = MakeNativeSymbolName (entry, useFullNativeSignature); + string klass = $"{nativeCallback.DeclaringType.FullName}, {nativeCallback.Module.Assembly.FullName}"; + Console.WriteLine ($" klass == {klass}"); if (!seenClasses.TryGetValue (klass, out int classIndex)) { - seenClasses.Add (klass, classes.Count); + classIndex = classes.Count; + seenClasses.Add (klass, classIndex); var mc = new MarshalMethodsManagedClass { - token = entry.NativeCallback.DeclaringType.MetadataToken.ToUInt32 (), + token = nativeCallback.DeclaringType.MetadataToken.ToUInt32 (), ClassName = klass, }; classes.Add (new StructureInstance (mc)); } + Console.WriteLine (" about to parse JNI sig"); (Type returnType, List? parameters) = ParseJniSignature (entry.JniMethodSignature, entry.ImplementedMethod); - var method = new MarshalMethodInfo (entry, returnType, nativeSymbolName: sb.ToString (), classIndex); + Console.WriteLine (" parsed!"); + + var method = new MarshalMethodInfo (entry, returnType, nativeSymbolName: nativeSymbolName, classIndex); if (parameters != null && parameters.Count > 0) { method.Parameters.AddRange (parameters); } @@ -260,24 +384,52 @@ void ProcessAndAddMethod (MarshalMethodEntry entry, bool useFullNativeSignature, } Console.WriteLine (); - methods.Add (method); - - void ThrowInvalidSignature (string signature, string reason) - { - throw new InvalidOperationException ($"Invalid JNI signature '{signature}': {reason}"); + if (!overloadedNativeSymbolNames.TryGetValue (method.NativeSymbolName, out List overloadedMethods)) { + overloadedMethods = new List (); + overloadedNativeSymbolNames.Add (method.NativeSymbolName, overloadedMethods); } + overloadedMethods.Add (method); + + allMethods.Add (method); } string MangleForJni (string name) { Console.WriteLine ($" mangling '{name}'"); - var sb = new StringBuilder (name); + var sb = new StringBuilder (); + + foreach (char ch in name) { + switch (ch) { + case '_': + sb.Append ("_1"); + break; + + case '/': + sb.Append ('_'); + break; - sb.Replace ("_", "_1"); - sb.Replace ('/', '_'); - sb.Replace (";", "_2"); - sb.Replace ("[", "_3"); - // TODO: process unicode chars + case ';': + sb.Append ("_2"); + break; + + case '[': + sb.Append ("_3"); + break; + + case '$': + sb.Append ("_00024"); + break; + + default: + if ((int)ch > 127) { + sb.Append ("_0"); + sb.Append (((int)ch).ToString ("x04")); + } else { + sb.Append (ch); + } + break; + } + } return sb.ToString (); } @@ -290,12 +442,6 @@ string MangleForJni (string name) int idx = 0; while (!paramsDone && idx < signature.Length) { char jniType = signature[idx]; - Type? managedType = JniTypeToManaged (jniType); - - if (managedType != null) { - AddParameter (managedType); - continue; - } if (jniType == '(') { idx++; @@ -307,6 +453,12 @@ string MangleForJni (string name) continue; } + Type? managedType = JniTypeToManaged (jniType); + if (managedType != null) { + AddParameter (managedType); + continue; + } + throw new InvalidOperationException ($"Unsupported JNI type '{jniType}' at position {idx} of signature '{signature}'"); } @@ -325,8 +477,10 @@ string MangleForJni (string name) Type? JniTypeToManaged (char jniType) { + Console.WriteLine ($" turning JNI type '{jniType}' into managed type"); if (jniSimpleTypeMap.TryGetValue (jniType, out Type managedType)) { idx++; + Console.WriteLine ($" will return {managedType}"); return managedType; } @@ -335,16 +489,24 @@ string MangleForJni (string name) } if (jniType == '[') { + Console.WriteLine (" an array"); idx++; jniType = signature[idx]; if (jniArrayTypeMap.TryGetValue (jniType, out managedType)) { - JavaClassToManaged (justSkip: true); + if (jniType == 'L') { + Console.WriteLine (" skipping"); + JavaClassToManaged (justSkip: true); + } else { + idx++; + } + Console.WriteLine ($" will return {managedType}"); return managedType; } throw new InvalidOperationException ($"Unsupported JNI array type '{jniType}' at index {idx} of signature '{signature}'"); } + Console.WriteLine (" returning NULL managed type"); return null; } @@ -366,7 +528,8 @@ string MangleForJni (string name) break; } - sb?.Append (signature[idx++]); + sb?.Append (signature[idx]); + idx++; } if (justSkip) { @@ -414,6 +577,7 @@ protected override void MapStructures (LlvmIrGenerator generator) monoImage = generator.MapStructure (); monoClass = generator.MapStructure (); marshalMethodsClass = generator.MapStructure (); + marshalMethodName = generator.MapStructure (); _jniEnvSI = generator.MapStructure<_JNIEnv> (); _jobjectSI = generator.MapStructure<_jobject> (); _jclassSI = generator.MapStructure<_jclass> (); @@ -433,10 +597,77 @@ protected override void MapStructures (LlvmIrGenerator generator) protected override void Write (LlvmIrGenerator generator) { - Dictionary asmNameToIndex = WriteAssemblyImageCache (generator); + WriteAssemblyImageCache (generator, out Dictionary asmNameToIndex); WriteClassCache (generator); LlvmIrVariableReference get_function_pointer_ref = WriteXamarinAppInitFunction (generator); WriteNativeMethods (generator, asmNameToIndex, get_function_pointer_ref); + + var mm_class_names = new List (); + foreach (StructureInstance klass in classes) { + mm_class_names.Add (klass.Obj.ClassName); + } + generator.WriteArray (mm_class_names, "mm_class_names", "Names of classes in which marshal methods reside"); + + var uniqueMethods = new Dictionary (); + foreach (MarshalMethodInfo mmi in methods) { + string asmName = Path.GetFileName (mmi.Method.NativeCallback.Module.Assembly.MainModule.FileName); + if (!asmNameToIndex.TryGetValue (asmName, out uint idx)) { + throw new InvalidOperationException ($"Internal error: failed to match assembly name '{asmName}' to cache array index"); + } + + ulong id = ((ulong)idx << 32) | (ulong)mmi.Method.NativeCallback.MetadataToken.ToUInt32 (); + if (uniqueMethods.ContainsKey (id)) { + continue; + } + uniqueMethods.Add (id, mmi); + } + + MarshalMethodName name; + var methodName = new StringBuilder (); + var mm_method_names = new List> (); + foreach (var kvp in uniqueMethods) { + ulong id = kvp.Key; + MarshalMethodInfo mmi = kvp.Value; + + RenderMethodNameWithParams (mmi.Method.NativeCallback, methodName); + name = new MarshalMethodName { + // Tokens are unique per assembly + id = id, + name = methodName.ToString (), + }; + mm_method_names.Add (new StructureInstance (name)); + } + + // Must terminate with an "invalid" entry + name = new MarshalMethodName { + id = 0, + name = String.Empty, + }; + mm_method_names.Add (new StructureInstance (name)); + + generator.WriteStructureArray (marshalMethodName, mm_method_names, LlvmIrVariableOptions.GlobalConstant, "mm_method_names"); + + void RenderMethodNameWithParams (CecilMethodDefinition md, StringBuilder buffer) + { + buffer.Clear (); + buffer.Append (md.Name); + buffer.Append ('('); + + if (md.HasParameters) { + bool first = true; + foreach (CecilParameterDefinition pd in md.Parameters) { + if (!first) { + buffer.Append (','); + } else { + first = false; + } + + buffer.Append (pd.ParameterType.Name); + } + } + + buffer.Append (')'); + } } void WriteNativeMethods (LlvmIrGenerator generator, Dictionary asmNameToIndex, LlvmIrVariableReference get_function_pointer_ref) @@ -445,18 +676,19 @@ void WriteNativeMethods (LlvmIrGenerator generator, Dictionary asm return; } + var usedBackingFields = new HashSet (StringComparer.Ordinal); foreach (MarshalMethodInfo mmi in methods) { - string asmName = mmi.Method.NativeCallback.DeclaringType.Module.Assembly.Name.Name; + CecilMethodDefinition nativeCallback = mmi.Method.NativeCallback; + string asmName = nativeCallback.DeclaringType.Module.Assembly.Name.Name; if (!asmNameToIndex.TryGetValue (asmName, out uint asmIndex)) { throw new InvalidOperationException ($"Unable to translate assembly name '{asmName}' to its index"); } mmi.AssemblyCacheIndex = asmIndex; - - WriteMarshalMethod (generator, mmi, get_function_pointer_ref); + WriteMarshalMethod (generator, mmi, get_function_pointer_ref, usedBackingFields); } } - void WriteMarshalMethod (LlvmIrGenerator generator, MarshalMethodInfo method, LlvmIrVariableReference get_function_pointer_ref) + void WriteMarshalMethod (LlvmIrGenerator generator, MarshalMethodInfo method, LlvmIrVariableReference get_function_pointer_ref, HashSet usedBackingFields) { var backingFieldSignature = new LlvmNativeFunctionSignature ( returnType: method.ReturnType, @@ -465,10 +697,14 @@ void WriteMarshalMethod (LlvmIrGenerator generator, MarshalMethodInfo method, Ll FieldValue = "null", }; - string backingFieldName = $"native_cb_{method.Method.JniMethodName}_{method.AssemblyCacheIndex}_{method.ClassCacheIndex}_{method.Method.NativeCallback.MetadataToken.ToUInt32():x}"; + CecilMethodDefinition nativeCallback = method.Method.NativeCallback; + string backingFieldName = $"native_cb_{method.Method.JniMethodName}_{method.AssemblyCacheIndex}_{method.ClassCacheIndex}_{nativeCallback.MetadataToken.ToUInt32():x}"; var backingFieldRef = new LlvmIrVariableReference (backingFieldSignature, backingFieldName, isGlobal: true); - generator.WriteVariable (backingFieldName, backingFieldSignature, LlvmIrVariableOptions.LocalWritableInsignificantAddr); + if (!usedBackingFields.Contains (backingFieldName)) { + generator.WriteVariable (backingFieldName, backingFieldSignature, LlvmIrVariableOptions.LocalWritableInsignificantAddr); + usedBackingFields.Add (backingFieldName); + } var func = new LlvmIrFunction ( name: method.NativeSymbolName, @@ -501,7 +737,7 @@ void WriteMarshalMethod (LlvmIrGenerator generator, MarshalMethodInfo method, Ll new List { new LlvmIrFunctionArgument (typeof(uint), method.AssemblyCacheIndex), new LlvmIrFunctionArgument (typeof(uint), method.ClassCacheIndex), - new LlvmIrFunctionArgument (typeof(uint), method.Method.NativeCallback.MetadataToken.ToUInt32 ()), + new LlvmIrFunctionArgument (typeof(uint), nativeCallback.MetadataToken.ToUInt32 ()), new LlvmIrFunctionArgument (typeof(LlvmIrVariableReference), backingFieldRef), } ); @@ -574,36 +810,31 @@ LlvmIrVariableReference WriteXamarinAppInitFunction (LlvmIrGenerator generator) void WriteClassCache (LlvmIrGenerator generator) { + uint marshal_methods_number_of_classes = (uint)classes.Count; + + generator.WriteVariable (nameof (marshal_methods_number_of_classes), marshal_methods_number_of_classes); generator.WriteStructureArray (marshalMethodsClass, classes, LlvmIrVariableOptions.GlobalWritable, "marshal_methods_class_cache"); } - Dictionary WriteAssemblyImageCache (LlvmIrGenerator generator) + void WriteAssemblyImageCache (LlvmIrGenerator generator, out Dictionary asmNameToIndex) { - if (UniqueAssemblyNames == null) { - throw new InvalidOperationException ("Internal error: unique assembly names not provided"); - } - - if (UniqueAssemblyNames.Count != NumberOfAssembliesInApk) { - throw new InvalidOperationException ("Internal error: number of assemblies in the apk doesn't match the number of unique assembly names"); - } - bool is64Bit = generator.Is64Bit; - generator.WriteStructureArray (monoImage, (ulong)NumberOfAssembliesInApk, "assembly_image_cache", isArrayOfPointers: true); + generator.WriteStructureArray (monoImage, (ulong)numberOfAssembliesInApk, "assembly_image_cache", isArrayOfPointers: true); - var asmNameToIndex = new Dictionary (StringComparer.Ordinal); + var asmNameToIndexData = new Dictionary (StringComparer.Ordinal); if (is64Bit) { WriteHashes (); } else { WriteHashes (); } - return asmNameToIndex; + asmNameToIndex = asmNameToIndexData; void WriteHashes () where T: struct { var hashes = new Dictionary (); uint index = 0; - foreach (string name in UniqueAssemblyNames) { + foreach (string name in uniqueAssemblyNames) { string clippedName = Path.GetFileNameWithoutExtension (name); ulong hashFull = HashName (name, is64Bit); ulong hashClipped = HashName (clippedName, is64Bit); @@ -632,7 +863,7 @@ void WriteHashes () where T: struct for (int i = 0; i < keys.Count; i++) { (string name, uint idx) = hashes[keys[i]]; indices.Add (idx); - asmNameToIndex.Add (name, idx); + asmNameToIndexData.Add (name, idx); } generator.WriteArray ( indices, diff --git a/src/monodroid/jni/application_dso_stub.cc b/src/monodroid/jni/application_dso_stub.cc index 42196ff6113..5cbc623260a 100644 --- a/src/monodroid/jni/application_dso_stub.cc +++ b/src/monodroid/jni/application_dso_stub.cc @@ -186,6 +186,23 @@ MarshalMethodsManagedClass marshal_methods_class_cache[] = { }, }; +const char* const mm_class_names[2] = { + "one", + "two", +}; + +const MarshalMethodName mm_method_names[] = { + { + .id = 1, + .name = "one", + }, + + { + .id = 2, + .name = "two", + }, +}; + void xamarin_app_init ([[maybe_unused]] get_function_pointer_fn fn) noexcept { // Dummy diff --git a/src/monodroid/jni/monodroid-glue-internal.hh b/src/monodroid/jni/monodroid-glue-internal.hh index 8b0ef579bee..c90a69c57fe 100644 --- a/src/monodroid/jni/monodroid-glue-internal.hh +++ b/src/monodroid/jni/monodroid-glue-internal.hh @@ -350,7 +350,13 @@ namespace xamarin::android::internal static void monodroid_debugger_unhandled_exception (MonoException *ex); #if defined (RELEASE) && defined (ANDROID) - static void get_function_pointer (uint32_t mono_image_index, uint32_t class_token, uint32_t method_token, void*& target_ptr) noexcept; + static const char* get_method_name (uint32_t mono_image_index, uint32_t method_token) noexcept; + static const char* get_class_name (uint32_t class_index) noexcept; + + template + static void get_function_pointer (uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr) noexcept; + static void get_function_pointer_at_startup (uint32_t mono_image_index, uint32_t class_token, uint32_t method_token, void*& target_ptr) noexcept; + static void get_function_pointer_at_runtime (uint32_t mono_image_index, uint32_t class_token, uint32_t method_token, void*& target_ptr) noexcept; #endif // def RELEASE && def ANDROID #endif // def NET diff --git a/src/monodroid/jni/monodroid-glue.cc b/src/monodroid/jni/monodroid-glue.cc index 655dac4d223..883f4ac8a27 100644 --- a/src/monodroid/jni/monodroid-glue.cc +++ b/src/monodroid/jni/monodroid-glue.cc @@ -874,9 +874,9 @@ MonodroidRuntime::mono_runtime_init ([[maybe_unused]] dynamic_local_string(mono_image_index) << 32) | method_token; - // TODO: implement MonoClassLoader with caching. Best to use indexes instead of keying on tokens. - MonoClass *method_klass = mono_class_get (image, class_index); - MonoMethod *method = mono_get_method (image, method_token, method_klass); + log_debug (LOG_ASSEMBLY, "Looking for name of method with id 0x%llx, in mono image at index %u", id, mono_image_index); + size_t i = 0; + while (mm_method_names[i].id != 0) { + if (mm_method_names[i].id == id) { + return mm_method_names[i].name; + } + i++; + } - MonoError error; - void *ret = mono_method_get_unmanaged_callers_only_ftnptr (method, &error); - if (ret == nullptr || error.error_code != MONO_ERROR_NONE) { - // TODO: make the error message friendlier somehow (class, method and assembly names) + return Unknown; +} + +const char* +MonodroidRuntime::get_class_name (uint32_t class_index) noexcept +{ + if (class_index >= marshal_methods_number_of_classes) { + return Unknown; + } + + return mm_class_names[class_index]; +} + +template +force_inline void +MonodroidRuntime::get_function_pointer (uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr) noexcept +{ + log_warn (LOG_DEFAULT, __PRETTY_FUNCTION__); + log_debug ( + LOG_ASSEMBLY, + "MM: Trying to look up pointer to method '%s' (token 0x%x) in class '%s' (index %u)", + get_method_name (mono_image_index, method_token), method_token, + get_class_name (class_index), class_index + ); + + if (XA_UNLIKELY (class_index >= marshal_methods_number_of_classes)) { log_fatal (LOG_DEFAULT, - "Failed to obtain function pointer to method with token 0x%x; class token: 0x%x; assembly index: %u", - method_token, class_index, mono_images_cleanup + "Internal error: invalid index for class cache (expected at most %u, got %u)", + marshal_methods_number_of_classes - 1, + class_index ); abort (); } - target_ptr = ret; + // We don't check for valid return values from image loader, class and method lookup because if any + // of them fails to find the requested entity, they will return `null`. In consequence, we can pass + // these pointers without checking all the way to `mono_method_get_unmanaged_callers_only_ftnptr`, after + // which call we check for errors. This saves some time (not much, but definitely more than zero) + MonoImage *image = MonoImageLoader::get_from_index (mono_image_index); + MarshalMethodsManagedClass &klass = marshal_methods_class_cache[class_index]; + if (klass.klass == nullptr) { + klass.klass = mono_class_get (image, klass.token); + } + + MonoMethod *method = mono_get_method (image, method_token, klass.klass); + MonoError error; + void *ret = mono_method_get_unmanaged_callers_only_ftnptr (method, &error); + + if (XA_LIKELY (ret != nullptr)) { + if constexpr (NeedsLocking) { + __atomic_store_n (&target_ptr, ret, __ATOMIC_RELEASE); + } else { + target_ptr = ret; + } + + log_debug (LOG_ASSEMBLY, "Loaded pointer to method %s", mono_method_get_name (method)); + return; + } + + log_fatal ( + LOG_DEFAULT, + "Failed to obtain function pointer to method '%s' in class '%s'", + get_method_name (mono_image_index, method_token), + get_class_name (class_index) + ); + + log_fatal ( + LOG_DEFAULT, + "Looked for image index %u, class index %u, method token 0x%x", + mono_image_index, + class_index, + method_token + ); + + if (image == nullptr) { + log_fatal (LOG_DEFAULT, "Failed to load MonoImage for the assembly"); + } else if (method == nullptr) { + log_fatal (LOG_DEFAULT, "Failed to load class from the assembly"); + } + + if (error.error_code != MONO_ERROR_NONE) { + const char *msg = mono_error_get_message (&error); + if (msg != nullptr) { + log_fatal (LOG_DEFAULT, msg); + } + } + + abort (); +} + +void +MonodroidRuntime::get_function_pointer_at_startup (uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr) noexcept +{ + get_function_pointer (mono_image_index, class_index, method_token, target_ptr); +} + +void +MonodroidRuntime::get_function_pointer_at_runtime (uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr) noexcept +{ + get_function_pointer (mono_image_index, class_index, method_token, target_ptr); } diff --git a/src/monodroid/jni/xamarin-app.hh b/src/monodroid/jni/xamarin-app.hh index bd1076a2ae0..23ef37cba33 100644 --- a/src/monodroid/jni/xamarin-app.hh +++ b/src/monodroid/jni/xamarin-app.hh @@ -330,6 +330,24 @@ MONO_API MONO_API_EXPORT const xamarin::android::hash_t assembly_image_cache_has MONO_API MONO_API_EXPORT uint32_t marshal_methods_number_of_classes; MONO_API MONO_API_EXPORT MarshalMethodsManagedClass marshal_methods_class_cache[]; +// +// These tables store names of classes and managed callback methods used in the generated marshal methods +// code. They are used just for error reporting. +// +// Class names are found at the same indexes as their corresponding entries in the `marshal_methods_class_cache` array +// above. Method names are stored as token:name pairs and the array must end with an "invalid" terminator entry (token +// == 0; name == nullptr) +// +struct MarshalMethodName +{ + // combination of assembly index (high 32 bits) and method token (low 32 bits) + const uint64_t id; + const char *name; +}; + +MONO_API MONO_API_EXPORT const char* const mm_class_names[]; +MONO_API MONO_API_EXPORT const MarshalMethodName mm_method_names[]; + using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr); MONO_API MONO_API_EXPORT void xamarin_app_init (get_function_pointer_fn fn) noexcept;