diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 8c4ed9c5b7b..5c520d8bc83 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -33,6 +33,7 @@ internal struct JnienvInitializeArgs { public bool marshalMethodsEnabled; public IntPtr grefGCUserPeerable; public uint runtimeType; + public bool managedMarshalMethodsLookupEnabled; } #pragma warning restore 0649 @@ -122,9 +123,17 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) } } + if (args->managedMarshalMethodsLookupEnabled) { + delegate* unmanaged getFunctionPointer = &ManagedMarshalMethodsLookupTable.GetFunctionPointer; + xamarin_app_init (args->env, getFunctionPointer); + } + SetSynchronizationContext (); } + [DllImport ("xamarin-app")] + static extern unsafe void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); + // NOTE: prevents Android.App.Application static ctor from running [MethodImpl (MethodImplOptions.NoInlining)] static void SetSynchronizationContext () => diff --git a/src/Mono.Android/Java.Interop/ManagedMarshalMethodsLookupTable.cs b/src/Mono.Android/Java.Interop/ManagedMarshalMethodsLookupTable.cs new file mode 100644 index 00000000000..cd15fb3e337 --- /dev/null +++ b/src/Mono.Android/Java.Interop/ManagedMarshalMethodsLookupTable.cs @@ -0,0 +1,33 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Android.Runtime; + +namespace Java.Interop; + +internal class ManagedMarshalMethodsLookupTable +{ + [UnmanagedCallersOnly] + internal static unsafe void GetFunctionPointer (int assemblyIndex, int classIndex, int methodIndex, IntPtr* target) + { + try { + IntPtr result = GetFunctionPointer (assemblyIndex, classIndex, methodIndex); + if (result == IntPtr.Zero || result == (IntPtr)(-1)) { + throw new InvalidOperationException ($"Failed to get function pointer for ({assemblyIndex}, {classIndex}, {methodIndex})"); + } + + *target = result; + } catch (Exception ex) { + AndroidEnvironment.UnhandledException (ex); + AndroidEnvironment.FailFast ("GetFunctionPointer failed: should not be reached"); + } + } + + static IntPtr GetFunctionPointer (int assemblyIndex, int classIndex, int methodIndex) + { + // ManagedMarshalMethodsLookupGenerator generates the body of this method is generated at app build time + throw new NotImplementedException (); + } +} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index c454b84895c..db116a2e27b 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -313,6 +313,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index e1922471db0..b2ac6622349 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -54,6 +54,7 @@ public class GenerateJavaStubs : AndroidTask public bool LinkingEnabled { get; set; } public bool HaveMultipleRIDs { get; set; } public bool EnableMarshalMethods { get; set; } + public bool EnableManagedMarshalMethodsLookup { get; set; } public string ManifestTemplate { get; set; } public string[] MergedManifestDocuments { get; set; } @@ -244,8 +245,16 @@ void Run (bool useMarshalMethods) foreach (var kvp in nativeCodeGenStates) { NativeCodeGenState state = kvp.Value; - RewriteMarshalMethods (state, brokenExceptionTransitionsEnabled); - state.Classifier.AddSpecialCaseMethods (); + if (!EnableManagedMarshalMethodsLookup) { + RewriteMarshalMethods (state, brokenExceptionTransitionsEnabled); + state.Classifier.AddSpecialCaseMethods (); + } else { + // We need to run `AddSpecialCaseMethods` before `RewriteMarshalMethods` so that we can see the special case + // methods (such as TypeManager.n_Activate_mm) when generating the managed lookup tables. + state.Classifier.AddSpecialCaseMethods (); + state.ManagedMarshalMethodsLookupInfo = new ManagedMarshalMethodsLookupInfo (Log); + RewriteMarshalMethods (state, brokenExceptionTransitionsEnabled); + } Log.LogDebugMessage ($"[{state.TargetArch}] Number of generated marshal methods: {state.Classifier.MarshalMethods.Count}"); if (state.Classifier.RejectedMethodCount > 0) { @@ -460,7 +469,7 @@ void RewriteMarshalMethods (NativeCodeGenState state, bool brokenExceptionTransi return; } - var rewriter = new MarshalMethodsAssemblyRewriter (Log, state.TargetArch, state.Classifier, state.Resolver); + var rewriter = new MarshalMethodsAssemblyRewriter (Log, state.TargetArch, state.Classifier, state.Resolver, state.ManagedMarshalMethodsLookupInfo); rewriter.Rewrite (brokenExceptionTransitionsEnabled); } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs index 1bb04982727..3c087a6ccec 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs @@ -62,6 +62,7 @@ public class GeneratePackageManagerJava : AndroidTask public bool EnablePreloadAssembliesDefault { get; set; } public bool EnableMarshalMethods { get; set; } + public bool EnableManagedMarshalMethodsLookup { get; set; } public string RuntimeConfigBinFilePath { get; set; } public string BoundExceptionType { get; set; } @@ -339,6 +340,7 @@ void AddEnvironment () JniRemappingReplacementTypeCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementTypeCount, JniRemappingReplacementMethodIndexEntryCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementMethodIndexEntryCount, MarshalMethodsEnabled = EnableMarshalMethods, + ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup, IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (), }; LLVMIR.LlvmIrModule appConfigModule = appConfigAsmGen.Construct (); @@ -367,7 +369,8 @@ void AddEnvironment () Log, assemblyCount, uniqueAssemblyNames, - EnsureCodeGenState (targetArch) + EnsureCodeGenState (targetArch), + EnableManagedMarshalMethodsLookup ); } else { marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator ( diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index e687e1e605d..ec8f5d45e73 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -469,7 +469,7 @@ public void XA1037PropertyDeprecatedWarning (string property, string value, bool XamarinAndroidProject proj = isBindingProject ? new XamarinAndroidBindingProject () : new XamarinAndroidApplicationProject (); proj.IsRelease = isRelease; proj.SetProperty (property, value); - + using (ProjectBuilder b = isBindingProject ? CreateDllBuilder (Path.Combine ("temp", TestName)) : CreateApkBuilder (Path.Combine ("temp", TestName))) { Assert.IsTrue (b.Build (proj), "Build should have succeeded."); Assert.IsTrue (StringAssertEx.ContainsText (b.LastBuildOutput, $"The '{property}' MSBuild property is deprecated and will be removed"), diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index 075f0154f98..7a86601ce43 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -65,9 +65,10 @@ public sealed class ApplicationConfig public uint jni_remapping_replacement_method_index_entry_count; public uint mono_components_mask; public string android_package_name = String.Empty; + public bool managed_marshal_methods_lookup_enabled; } - const uint ApplicationConfigFieldCount = 26; + const uint ApplicationConfigFieldCount = 27; const string ApplicationConfigSymbolName = "application_config"; const string AppEnvironmentVariablesSymbolName = "app_environment_variables"; @@ -335,6 +336,11 @@ static ApplicationConfig ReadApplicationConfig (EnvironmentFile envFile) Assert.IsTrue (expectedPointerTypes.Contains (field [0]), $"Unexpected pointer field type in '{envFile.Path}:{item.LineNumber}': {field [0]}"); pointers.Add (field [1].Trim ()); break; + + case 26: // managed_marshal_methods_lookup_enabled: bool / .byte + AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber); + ret.managed_marshal_methods_lookup_enabled = ConvertFieldToBool ("managed_marshal_methods_lookup_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]); + break; } fieldCount++; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs index 9a2335f9e75..40ea29ee665 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs @@ -58,5 +58,6 @@ sealed class ApplicationConfig [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] public uint mono_components_mask; public string android_package_name = String.Empty; + public bool managed_marshal_methods_lookup_enabled; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs index 119be4153b2..ae508371f8f 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs @@ -193,6 +193,7 @@ sealed class XamarinAndroidBundledAssembly public PackageNamingPolicy PackageNamingPolicy { get; set; } public List NativeLibraries { get; set; } public bool MarshalMethodsEnabled { get; set; } + public bool ManagedMarshalMethodsLookupEnabled { get; set; } public bool IgnoreSplitConfigs { get; set; } public ApplicationConfigNativeAssemblyGenerator (IDictionary environmentVariables, IDictionary systemProperties, TaskLoggingHelper log) @@ -237,6 +238,7 @@ protected override void Construct (LlvmIrModule module) have_runtime_config_blob = HaveRuntimeConfigBlob, have_assemblies_blob = HaveAssemblyStore, marshal_methods_enabled = MarshalMethodsEnabled, + managed_marshal_methods_lookup_enabled = ManagedMarshalMethodsLookupEnabled, ignore_split_configs = IgnoreSplitConfigs, bound_stream_io_exception_type = (byte)BoundExceptionType, package_naming_policy = (uint)PackageNamingPolicy, diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupGenerator.cs new file mode 100644 index 00000000000..17ed80a69cd --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupGenerator.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +class ManagedMarshalMethodsLookupGenerator +{ + readonly ManagedMarshalMethodsLookupInfo managedMarshalMethodsLookupInfo; + readonly TaskLoggingHelper log; + readonly AndroidTargetArch targetArch; + readonly TypeDefinition functionPointerLookup; + + readonly Dictionary> typeReferenceCache = new (); + + public ManagedMarshalMethodsLookupGenerator ( + TaskLoggingHelper log, + AndroidTargetArch targetArch, + ManagedMarshalMethodsLookupInfo managedMarshalMethodsLookupInfo, + TypeDefinition functionPointerLookup) + { + this.log = log; + this.targetArch = targetArch; + this.managedMarshalMethodsLookupInfo = managedMarshalMethodsLookupInfo; + this.functionPointerLookup = functionPointerLookup; + } + + public void Generate (IEnumerable> marshalMethods) + { + foreach (IList methodList in marshalMethods) { + foreach (MarshalMethodEntry method in methodList) { + managedMarshalMethodsLookupInfo.AddNativeCallbackWrapper (method.NativeCallbackWrapper); + } + } + + foreach (var assemblyInfo in managedMarshalMethodsLookupInfo.AssemblyLookup.Values) { + foreach (var classInfo in assemblyInfo.ClassLookup.Values) { + classInfo.GetFunctionPointerMethod = GenerateGetFunctionPointer (classInfo); + } + + assemblyInfo.GetFunctionPointerMethod = GenerateGetFunctionPointerPerAssembly (assemblyInfo); + } + + GenerateGlobalGetFunctionPointerMethod (); + } + + MethodDefinition GenerateGetFunctionPointer (ManagedMarshalMethodsLookupInfo.ClassLookupInfo classLookup) + { + var declaringType = classLookup.DeclaringType; + log.LogDebugMessage ($"Generating `GetFunctionPointer` for {declaringType.FullName} ({classLookup.MethodLookup.Count} UCO methods)"); + + var intType = ImportReference (declaringType.Module, typeof (int)); + var intPtrType = ImportReference (declaringType.Module, typeof (System.IntPtr)); + + // an "unspeakable" method name is used to avoid conflicts with user-defined methods + var getFunctionPointerMethod = classLookup.GetFunctionPointerMethod = new MethodDefinition ("GetFunctionPointer", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, intPtrType); + + getFunctionPointerMethod.DeclaringType = declaringType; + declaringType.Methods.Add (getFunctionPointerMethod); + + var methodIndexParameter = new ParameterDefinition ("methodIndex", ParameterAttributes.None, intType); + getFunctionPointerMethod.Parameters.Add (methodIndexParameter); + + // we're setting the index here as we are creating the actual switch targets array to guarantee the indexing will be in sync + var targets = new Instruction [classLookup.MethodLookup.Count]; + uint methodIndex = 0; + foreach (var methodLookup in classLookup.MethodLookup.Values) { + methodLookup.Index = methodIndex++; + targets [methodLookup.Index] = Instruction.Create (OpCodes.Ldftn, methodLookup.NativeCallbackWrapper); + } + + var invalidUcoTarget = Instruction.Create (OpCodes.Nop); + + var il = getFunctionPointerMethod.Body.GetILProcessor (); + il.Emit (OpCodes.Ldarg, methodIndexParameter); + il.Emit (OpCodes.Switch, targets); + + il.Emit (OpCodes.Br_S, invalidUcoTarget); + + for (var k = 0; k < targets.Length; k++) { + il.Append (targets [k]); + il.Emit (OpCodes.Ret); + } + + // no hit? this shouldn't happen + il.Append (invalidUcoTarget); + il.Emit (OpCodes.Ldc_I4_M1); + il.Emit (OpCodes.Conv_I); + il.Emit (OpCodes.Ret); + + // in the case of private/private protected/protected nested types, we need to generate proxy method(s) in the parent type(s) + // so that we can call the actual GetFunctionPointer method from our assembly-level GetFunctionPointer method + while (declaringType.IsNested && (declaringType.IsNestedPrivate || declaringType.IsNestedFamily || declaringType.IsNestedFamilyAndAssembly)) { + var parentType = declaringType.DeclaringType; + + // an "unspeakable" method name is used to avoid conflicts with user-defined methods + var parentProxyMethod = new MethodDefinition ($"GetFunctionPointer_{declaringType.Name}", MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, intPtrType); + parentProxyMethod.DeclaringType = parentType; + parentType.Methods.Add (parentProxyMethod); + + var parentMethodIndexParameter = new ParameterDefinition ("methodIndex", ParameterAttributes.None, intType); + parentProxyMethod.Parameters.Add (parentMethodIndexParameter); + + var proxyIl = parentProxyMethod.Body.GetILProcessor (); + proxyIl.Emit (OpCodes.Ldarg, parentMethodIndexParameter); + proxyIl.Emit (OpCodes.Call, getFunctionPointerMethod); + proxyIl.Emit (OpCodes.Ret); + + declaringType = parentType; + getFunctionPointerMethod = parentProxyMethod; + } + + return getFunctionPointerMethod; + } + + MethodDefinition GenerateGetFunctionPointerPerAssembly (ManagedMarshalMethodsLookupInfo.AssemblyLookupInfo assemblyInfo) + { + var module = assemblyInfo.Assembly.MainModule; + + var intType = ImportReference (module, typeof (int)); + var intPtrType = ImportReference (module, typeof (System.IntPtr)); + var objectType = ImportReference (module, typeof (object)); + + var lookupType = new TypeDefinition ("Java.Interop", "__ManagedMarshalMethodsLookupTable__", TypeAttributes.Public | TypeAttributes.Sealed, objectType); + module.Types.Add (lookupType); + + var getFunctionPointerMethod = new MethodDefinition ($"GetFunctionPointer", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, intPtrType); + getFunctionPointerMethod.DeclaringType = lookupType; + lookupType.Methods.Add (getFunctionPointerMethod); + + var classIndexParameter = new ParameterDefinition ("classIndex", ParameterAttributes.None, intType); + getFunctionPointerMethod.Parameters.Add (classIndexParameter); + + var methodIndexParameter = new ParameterDefinition ("methodIndex", ParameterAttributes.None, intType); + getFunctionPointerMethod.Parameters.Add (methodIndexParameter); + + // we're setting the index here as we are creating the actual switch targets array to guarantee the indexing will be in sync + var targets = new Instruction [assemblyInfo.ClassLookup.Count]; + uint classIndex = 0; + foreach (var classInfo in assemblyInfo.ClassLookup.Values) { + classInfo.Index = classIndex++; + targets [classInfo.Index] = Instruction.Create (OpCodes.Call, module.ImportReference (classInfo.GetFunctionPointerMethod)); + } + + var il = getFunctionPointerMethod.Body.GetILProcessor (); + il.Emit (OpCodes.Ldarg, methodIndexParameter); + + il.Emit (OpCodes.Ldarg, classIndexParameter); + il.Emit (OpCodes.Switch, targets); + + var defaultTarget = Instruction.Create (OpCodes.Nop); + il.Emit (OpCodes.Br, defaultTarget); + + for (int i = 0; i < targets.Length; i++) { + il.Append (targets [i]); // call + il.Emit (OpCodes.Ret); + } + + // no hit? this shouldn't happen + il.Append (defaultTarget); + il.Emit (OpCodes.Pop); // methodIndex + il.Emit (OpCodes.Ldc_I4_M1); + il.Emit (OpCodes.Conv_I); + il.Emit (OpCodes.Ret); + + return getFunctionPointerMethod; + } + + void GenerateGlobalGetFunctionPointerMethod () + { + var module = functionPointerLookup.Module; + + var intType = ImportReference (module, typeof (int)); + var intPtrType = ImportReference (module, typeof (System.IntPtr)); + + var getFunctionPointerMethod = FindMethod (functionPointerLookup, "GetFunctionPointer", parametersCount: 3, required: true); + getFunctionPointerMethod.Body.Instructions.Clear (); + + // we're setting the index here as we are creating the actual switch targets array to guarantee the indexing will be in sync + var targets = new Instruction [managedMarshalMethodsLookupInfo.AssemblyLookup.Count]; + uint assemblyIndex = 0; + foreach (var assemblyInfo in managedMarshalMethodsLookupInfo.AssemblyLookup.Values) { + assemblyInfo.Index = assemblyIndex++; + targets [assemblyInfo.Index] = Instruction.Create (OpCodes.Call, module.ImportReference (assemblyInfo.GetFunctionPointerMethod)); + } + + var il = getFunctionPointerMethod.Body.GetILProcessor (); + il.Emit (OpCodes.Ldarg_1); // classIndex + il.Emit (OpCodes.Ldarg_2); // methodIndex + + il.Emit (OpCodes.Ldarg_0); // assemblyIndex + il.Emit (OpCodes.Switch, targets); + + var defaultTarget = Instruction.Create (OpCodes.Nop); + il.Emit (OpCodes.Br_S, defaultTarget); + + for (int i = 0; i < targets.Length; i++) { + il.Append (targets [i]); // call + il.Emit (OpCodes.Ret); + } + + // no hit? this shouldn't happen + il.Append (defaultTarget); + il.Emit (OpCodes.Pop); // methodIndex + il.Emit (OpCodes.Pop); // classIndex + il.Emit (OpCodes.Ldc_I4_M1); + il.Emit (OpCodes.Conv_I); + il.Emit (OpCodes.Ret); + } + + TypeReference ImportReference (ModuleDefinition module, Type type) + { + if (!typeReferenceCache.TryGetValue (module, out var cache)) { + typeReferenceCache [module] = cache = new (); + } + + if (!cache.TryGetValue (type, out var typeReference)) { + cache [type] = typeReference = module.ImportReference (type); + } + + return typeReference; + } + + MethodDefinition? FindMethod (TypeDefinition type, string methodName, int parametersCount, bool required) + { + log.LogDebugMessage ($"[{targetArch}] Looking for method '{methodName}' with {parametersCount} params in type {type}"); + foreach (MethodDefinition method in type.Methods) { + log.LogDebugMessage ($"[{targetArch}] method: {method.Name}"); + if (method.Parameters.Count == parametersCount && String.Compare (methodName, method.Name, StringComparison.Ordinal) == 0) { + log.LogDebugMessage ($"[{targetArch}] match!"); + return method; + } + } + + if (required) { + throw new InvalidOperationException ($"[{targetArch}] Internal error: required method '{methodName}' in type {type} not found"); + } + + return null; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupInfo.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupInfo.cs new file mode 100644 index 00000000000..d6d856a89cc --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedMarshalMethodsLookupInfo.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; + +using Java.Interop.Tools.Cecil; +using Mono.Cecil; +using Xamarin.Android.Tools; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +class ManagedMarshalMethodsLookupInfo (TaskLoggingHelper log) +{ + readonly TaskLoggingHelper _log = log; + + public Dictionary AssemblyLookup { get; } = new (StringComparer.Ordinal); + + public (uint AssemblyIndex, uint ClassIndex, uint MethodIndex) GetIndex (MethodDefinition nativeCallbackWrapper) + { + var (assemblyName, className, methodName) = GetNames (nativeCallbackWrapper); + + if (!AssemblyLookup.TryGetValue (assemblyName, out var assemblyInfo)) { + throw new InvalidOperationException ($"Assembly '{assemblyName}' not found in the lookup indexes."); + } + + if (!assemblyInfo.ClassLookup.TryGetValue (className, out var classInfo)) { + throw new InvalidOperationException ($"Class '{className}' not found in the lookup indexes."); + } + + if (!classInfo.MethodLookup.TryGetValue (methodName, out var methodLookup)) { + throw new InvalidOperationException ($"Method '{methodName}' not found in the lookup indexes."); + } + + if (assemblyInfo.Index < 0 || classInfo.Index < 0 || methodLookup.Index < 0) { + throw new InvalidOperationException ($"Invalid index values for {assemblyName}/{className}/{methodName}: {assemblyInfo.Index}, {classInfo.Index}, {methodLookup.Index}"); + } + + return (assemblyInfo.Index, classInfo.Index, methodLookup.Index); + } + + public void AddNativeCallbackWrapper (MethodDefinition nativeCallbackWrapper) + { + var (assemblyName, className, methodName) = GetNames (nativeCallbackWrapper); + + if (!AssemblyLookup.TryGetValue (assemblyName, out var assemblyInfo)) { + AssemblyLookup [assemblyName] = assemblyInfo = new AssemblyLookupInfo (); + } + + if (!assemblyInfo.ClassLookup.TryGetValue (className, out var classInfo)) { + assemblyInfo.ClassLookup [className] = classInfo = new ClassLookupInfo (); + } + + if (!classInfo.MethodLookup.TryGetValue (methodName, out var methodInfo)) { + classInfo.MethodLookup [methodName] = methodInfo = new MethodLookupInfo (); + } else { + _log.LogDebugMessage ($"Method '{assemblyName}'/'{className}'/'{methodName}' already has an associated UnmanagedCallersOnly method."); + return; + } + + assemblyInfo.Assembly = nativeCallbackWrapper.DeclaringType.Module.Assembly; + classInfo.DeclaringType = nativeCallbackWrapper.DeclaringType; + methodInfo.NativeCallbackWrapper = nativeCallbackWrapper; + } + + private static (string, string, string) GetNames (MethodDefinition nativeCallback) + { + var type = nativeCallback.DeclaringType; + var assemblyName = type.Module.Assembly.Name.Name; + var className = type.FullName; + var methodName = nativeCallback.Name; + + return (assemblyName, className, methodName); + } + + internal sealed class AssemblyLookupInfo + { + public uint Index { get; set; } = uint.MaxValue; + public AssemblyDefinition Assembly { get; set; } + public MethodDefinition? GetFunctionPointerMethod { get; set; } + public Dictionary ClassLookup { get; } = new (StringComparer.Ordinal); + } + + internal sealed class ClassLookupInfo + { + public uint Index { get; set; } = uint.MaxValue; + public TypeDefinition DeclaringType { get; set; } + public MethodDefinition? GetFunctionPointerMethod { get; set; } + public Dictionary MethodLookup { get; } = new (StringComparer.Ordinal); + } + + internal sealed class MethodLookupInfo + { + public uint Index { get; set; } = uint.MaxValue; + public MethodDefinition NativeCallbackWrapper { get; set; } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs index 9f1034bd6b8..5de2f6392d8 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -25,13 +25,15 @@ sealed class AssemblyImports readonly MarshalMethodsClassifier classifier; readonly XAAssemblyResolver resolver; readonly AndroidTargetArch targetArch; + readonly ManagedMarshalMethodsLookupInfo? managedMarshalMethodsLookupInfo; - public MarshalMethodsAssemblyRewriter (TaskLoggingHelper log, AndroidTargetArch targetArch, MarshalMethodsClassifier classifier, XAAssemblyResolver resolver) + public MarshalMethodsAssemblyRewriter (TaskLoggingHelper log, AndroidTargetArch targetArch, MarshalMethodsClassifier classifier, XAAssemblyResolver resolver, ManagedMarshalMethodsLookupInfo? managedMarshalMethodsLookupInfo) { this.log = log ?? throw new ArgumentNullException (nameof (log)); this.targetArch = targetArch; this.classifier = classifier ?? throw new ArgumentNullException (nameof (classifier));; this.resolver = resolver ?? throw new ArgumentNullException (nameof (resolver));; + this.managedMarshalMethodsLookupInfo = managedMarshalMethodsLookupInfo; } // TODO: do away with broken exception transitions, there's no point in supporting them @@ -103,6 +105,15 @@ public void Rewrite (bool brokenExceptionTransitions) } } + if (managedMarshalMethodsLookupInfo is not null) { + // TODO the code should probably go to different assemblies than Mono.Android (to avoid recursive dependencies) + var rootAssembly = resolver.Resolve ("Mono.Android") ?? throw new InvalidOperationException ($"[{targetArch}] Internal error: unable to load the Mono.Android assembly"); + var managedMarshalMethodsLookupTableType = FindType (rootAssembly, "Java.Interop.ManagedMarshalMethodsLookupTable", required: true); + + var managedMarshalMethodLookupGenerator = new ManagedMarshalMethodsLookupGenerator (log, targetArch, managedMarshalMethodsLookupInfo, managedMarshalMethodsLookupTableType); + managedMarshalMethodLookupGenerator.Generate (classifier.MarshalMethods.Values); + } + foreach (AssemblyDefinition asm in classifier.Assemblies) { string? path = asm.MainModule.FileName; if (String.IsNullOrEmpty (path)) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs index cf2e1bf15e3..6b7e9d83ebc 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs @@ -237,6 +237,7 @@ public MarshalMethodAssemblyIndexValuePlaceholder (MarshalMethodInfo mmi, Assemb readonly LlvmIrCallMarker defaultCallMarker; readonly bool generateEmptyCode; + readonly bool managedMarshalMethodsLookupEnabled; readonly AndroidTargetArch targetArch; readonly NativeCodeGenState? codeGenState; @@ -256,12 +257,13 @@ public MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, AndroidTarg /// /// Constructor to be used ONLY when marshal methods are ENABLED /// - public MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, int numberOfAssembliesInApk, ICollection uniqueAssemblyNames, NativeCodeGenState codeGenState) + public MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, int numberOfAssembliesInApk, ICollection uniqueAssemblyNames, NativeCodeGenState codeGenState, bool managedMarshalMethodsLookupEnabled) : base (log) { this.numberOfAssembliesInApk = numberOfAssembliesInApk; this.uniqueAssemblyNames = uniqueAssemblyNames ?? throw new ArgumentNullException (nameof (uniqueAssemblyNames)); this.codeGenState = codeGenState ?? throw new ArgumentNullException (nameof (codeGenState)); + this.managedMarshalMethodsLookupEnabled = managedMarshalMethodsLookupEnabled; generateEmptyCode = false; defaultCallMarker = LlvmIrCallMarker.Tail; @@ -703,12 +705,16 @@ void WriteBody (LlvmIrFunctionBody body) LlvmIrLocalVariable getFuncPtrResult = func.CreateLocalVariable (typeof(IntPtr), "get_func_ptr"); body.Load (writeState.GetFunctionPtrVariable, getFuncPtrResult, tbaa: module.TbaaAnyPointer); - var placeholder = new MarshalMethodAssemblyIndexValuePlaceholder (method, writeState.AssemblyCacheState); - LlvmIrInstructions.Call call = body.Call ( - writeState.GetFunctionPtrFunction, - arguments: new List { placeholder, method.ClassCacheIndex, nativeCallback.MetadataToken.ToUInt32 (), backingField }, - funcPointer: getFuncPtrResult - ); + List getFunctionPointerArguments; + if (managedMarshalMethodsLookupEnabled) { + (uint assemblyIndex, uint classIndex, uint methodIndex) = codeGenState.ManagedMarshalMethodsLookupInfo.GetIndex (nativeCallback); + getFunctionPointerArguments = new List { assemblyIndex, classIndex, methodIndex, backingField }; + } else { + var placeholder = new MarshalMethodAssemblyIndexValuePlaceholder (method, writeState.AssemblyCacheState); + getFunctionPointerArguments = new List { placeholder, method.ClassCacheIndex, nativeCallback.MetadataToken.ToUInt32 (), backingField }; + } + + LlvmIrInstructions.Call call = body.Call (writeState.GetFunctionPtrFunction, arguments: getFunctionPointerArguments, funcPointer: getFuncPtrResult); LlvmIrLocalVariable cb2 = func.CreateLocalVariable (typeof(IntPtr), "cb2"); body.Load (backingField, cb2, tbaa: module.TbaaAnyPointer); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs index 5a541f2d09c..f89c8a501a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs @@ -35,6 +35,8 @@ class NativeCodeGenState public TypeDefinitionCache TypeCache { get; } public bool JniAddNativeMethodRegistrationAttributePresent { get; set; } + public ManagedMarshalMethodsLookupInfo? ManagedMarshalMethodsLookupInfo { get; set; } + public NativeCodeGenState (AndroidTargetArch arch, TypeDefinitionCache tdCache, XAAssemblyResolver resolver, List allJavaTypes, List javaTypesForJCW, MarshalMethodsClassifier? classifier) { TargetArch = arch; diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 94b8bb823b0..954f14c5401 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -339,6 +339,9 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. False <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' == 'True' ">False <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' != 'True' ">$(AndroidEnableMarshalMethods) + + <_AndroidUseManagedMarshalMethodsLookup Condition=" '$(_AndroidUseManagedMarshalMethodsLookup)' == '' and '$(_AndroidUseMarshalMethods)' == 'True' and '$(_AndroidRuntime)' != 'MonoVM' ">True + <_AndroidUseManagedMarshalMethodsLookup Condition=" '$(_AndroidUseManagedMarshalMethodsLookup)' == '' ">False @@ -1545,6 +1548,7 @@ because xbuild doesn't support framework reference assemblies. SkipJniAddNativeMethodRegistrationAttributeScan="$(_SkipJniAddNativeMethodRegistrationAttributeScan)" CheckedBuild="$(_AndroidCheckedBuild)" EnableMarshalMethods="$(_AndroidUseMarshalMethods)" + EnableManagedMarshalMethodsLookup="$(_AndroidUseManagedMarshalMethodsLookup)" LinkingEnabled="$(_LinkingEnabled)" HaveMultipleRIDs="$(_HaveMultipleRIDs)" IntermediateOutputDirectory="$(IntermediateOutputPath)" @@ -1778,6 +1782,7 @@ because xbuild doesn't support framework reference assemblies. RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)" UseAssemblyStore="$(AndroidUseAssemblyStore)" EnableMarshalMethods="$(_AndroidUseMarshalMethods)" + EnableManagedMarshalMethodsLookup="$(_AndroidUseManagedMarshalMethodsLookup)" CustomBundleConfigFile="$(AndroidBundleConfigurationFile)" > @@ -2160,7 +2165,7 @@ because xbuild doesn't support framework reference assemblies. <_ApkOutputPath>$(_BaseZipIntermediate) - + - + - + 0 || application_config.jni_remapping_replacement_method_index_entry_count > 0; init.marshalMethodsEnabled = application_config.marshal_methods_enabled; + init.managedMarshalMethodsLookupEnabled = application_config.managed_marshal_methods_lookup_enabled; java_System = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "java_lang_System", true); java_System_identityHashCode = env->GetStaticMethodID (java_System, "identityHashCode", "(Ljava/lang/Object;)I"); @@ -1551,7 +1558,11 @@ MonodroidRuntime::Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass kl #if defined (RELEASE) if (application_config.marshal_methods_enabled) { - xamarin_app_init (env, get_function_pointer_at_runtime); + if (!application_config.managed_marshal_methods_lookup_enabled) { + xamarin_app_init (env, get_function_pointer_at_runtime); + } else { + // xamarin_app_init is called via p/invoke from JNIEnvInit.Initialize + } } #endif // def RELEASE && def ANDROID && def NET MonodroidState::mark_startup_done (); diff --git a/src/native/mono/monodroid/xamarin-android-app-context.cc b/src/native/mono/monodroid/xamarin-android-app-context.cc index 9ef193ddeb7..33e3a023f37 100644 --- a/src/native/mono/monodroid/xamarin-android-app-context.cc +++ b/src/native/mono/monodroid/xamarin-android-app-context.cc @@ -132,3 +132,12 @@ MonodroidRuntime::get_function_pointer_at_runtime (uint32_t mono_image_index, ui { get_function_pointer (mono_image_index, class_index, method_token, target_ptr); } + +void +MonodroidRuntime::get_function_pointer_placeholder (uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr) noexcept +{ + target_ptr = nullptr; + Helpers::abort_application ( + std::format ("The callback for get_function_pointer has not been initialized yet. Failed to resolve ({}, {}, {})."sv, mono_image_index, class_index, method_token) + ); +} diff --git a/src/native/mono/xamarin-app-stub/application_dso_stub.cc b/src/native/mono/xamarin-app-stub/application_dso_stub.cc index c026be2b346..0e3b0d01ae0 100644 --- a/src/native/mono/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/mono/xamarin-app-stub/application_dso_stub.cc @@ -67,6 +67,7 @@ const ApplicationConfig application_config = { .jni_remapping_replacement_method_index_entry_count = 2, .mono_components_mask = MonoComponent::None, .android_package_name = android_package_name, + .managed_marshal_methods_lookup_enabled = false, }; const char* const mono_aot_mode_name = "normal"; diff --git a/src/native/mono/xamarin-app-stub/xamarin-app.hh b/src/native/mono/xamarin-app-stub/xamarin-app.hh index 8216cad0848..365df553c7b 100644 --- a/src/native/mono/xamarin-app-stub/xamarin-app.hh +++ b/src/native/mono/xamarin-app-stub/xamarin-app.hh @@ -258,6 +258,7 @@ struct ApplicationConfig uint32_t jni_remapping_replacement_method_index_entry_count; MonoComponent mono_components_mask; const char *android_package_name; + bool managed_marshal_methods_lookup_enabled; }; struct DSOApkEntry diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index df7e7d9241d..13b5d2a46c5 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1332,5 +1332,24 @@ public void NativeAOTSample () throw; } } + + [Test] + public void AppStartsWithManagedMarshalMethodsLookupEnabled () + { + var proj = new XamarinAndroidApplicationProject { IsRelease = true }; + proj.SetProperty ("AndroidUseMarshalMethods", "true"); + proj.SetProperty ("_AndroidUseManagedMarshalMethodsLookup", "true"); + + using var builder = CreateApkBuilder (); + builder.Save (proj); + + var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)); + Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); + Assert.IsTrue (dotnet.Run (), "`dotnet run --no-build` should succeed"); + + bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity", + Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), 30); + Assert.IsTrue (didLaunch, "Activity should have started."); + } } }