diff --git a/external/Java.Interop b/external/Java.Interop index c942ab683c1..fadbb82c3b8 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit c942ab683c12e88e0527ed584a13b411e005ea57 +Subproject commit fadbb82c3b8ab7979c19e9f139bdf2589e47549e diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 00e08ff4305..52317659f12 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -124,7 +124,12 @@ void Run (DirectoryAssemblyResolver res) if (Directory.Exists (dir.ItemSpec)) res.SearchDirectories.Add (dir.ItemSpec); } - +#if ENABLE_MARSHAL_METHODS + Dictionary> marshalMethodsAssemblyPaths = null; + if (!Debug) { + marshalMethodsAssemblyPaths = new Dictionary> (StringComparer.Ordinal); + } +#endif // Put every assembly we'll need in the resolver bool hasExportReference = false; bool haveMonoAndroid = false; @@ -159,6 +164,11 @@ void Run (DirectoryAssemblyResolver res) } res.Load (assembly.ItemSpec); +#if ENABLE_MARSHAL_METHODS + if (!Debug) { + StoreMarshalAssemblyPath (Path.GetFileNameWithoutExtension (assembly.ItemSpec), assembly); + } +#endif } // However we only want to look for JLO types in user code for Java stub code generation @@ -172,6 +182,9 @@ void Run (DirectoryAssemblyResolver res) string name = Path.GetFileNameWithoutExtension (asm.ItemSpec); if (!userAssemblies.ContainsKey (name)) userAssemblies.Add (name, asm.ItemSpec); +#if ENABLE_MARSHAL_METHODS + StoreMarshalAssemblyPath (name, asm); +#endif } // Step 1 - Find all the JLO types @@ -182,10 +195,6 @@ void Run (DirectoryAssemblyResolver res) List allJavaTypes = scanner.GetJavaTypes (allTypemapAssemblies, res); - // Step 2 - Generate type maps - // Type mappings need to use all the assemblies, always. - WriteTypeMappings (allJavaTypes, cache); - var javaTypes = new List (); foreach (TypeDefinition td in allJavaTypes) { if (!userAssemblies.ContainsKey (td.Module.Assembly.Name.Name) || JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (td, cache)) { @@ -195,11 +204,27 @@ void Run (DirectoryAssemblyResolver res) javaTypes.Add (td); } - // Step 3 - Generate Java stub code - var success = CreateJavaSources (javaTypes, cache); + MarshalMethodsClassifier classifier = null; +#if ENABLE_MARSHAL_METHODS + if (!Debug) { + classifier = new MarshalMethodsClassifier (cache, res, Log); + } +#endif + // Step 2 - Generate Java stub code + var success = CreateJavaSources (javaTypes, cache, classifier); if (!success) return; +#if ENABLE_MARSHAL_METHODS + if (!Debug) { + var rewriter = new MarshalMethodsAssemblyRewriter (classifier.MarshalMethods, classifier.Assemblies, marshalMethodsAssemblyPaths, Log); + rewriter.Rewrite (res); + } +#endif + // Step 3 - Generate type maps + // Type mappings need to use all the assemblies, always. + WriteTypeMappings (allJavaTypes, cache); + // We need to save a map of .NET type -> ACW type for resource file fixups var managed = new Dictionary (javaTypes.Count, StringComparer.Ordinal); var java = new Dictionary (javaTypes.Count, StringComparer.Ordinal); @@ -210,7 +235,7 @@ void Run (DirectoryAssemblyResolver res) using (var acw_map = MemoryStreamPool.Shared.CreateStreamWriter ()) { foreach (TypeDefinition type in javaTypes) { string managedKey = type.FullName.Replace ('/', '.'); - string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.'); + string javaKey = JavaNativeTypeManager.ToJniName (type, cache).Replace ('/', '.'); acw_map.Write (type.GetPartialAssemblyQualifiedName (cache)); acw_map.Write (';'); @@ -332,17 +357,30 @@ void Run (DirectoryAssemblyResolver res) string applicationTemplateFile = "ApplicationRegistration.java"; SaveResource (applicationTemplateFile, applicationTemplateFile, real_app_dir, template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ())); + +#if ENABLE_MARSHAL_METHODS + void StoreMarshalAssemblyPath (string name, ITaskItem asm) + { + if (Debug) { + return; + } + + if (!marshalMethodsAssemblyPaths.TryGetValue (name, out HashSet assemblyPaths)) { + assemblyPaths = new HashSet (); + marshalMethodsAssemblyPaths.Add (name, assemblyPaths); + } + + assemblyPaths.Add (asm.ItemSpec); + } +#endif } - bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCache cache) + bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCache cache, MarshalMethodsClassifier classifier) { string outputPath = Path.Combine (OutputDirectory, "src"); string monoInit = GetMonoInitSource (AndroidSdkPlatform); bool hasExportReference = ResolvedAssemblies.Any (assembly => Path.GetFileName (assembly.ItemSpec) == "Mono.Android.Export.dll"); bool generateOnCreateOverrides = int.Parse (AndroidSdkPlatform) <= 10; -#if ENABLE_MARSHAL_METHODS - var overriddenMethodDescriptors = new List (); -#endif bool ok = true; foreach (var t in javaTypes) { @@ -353,7 +391,7 @@ bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCac using (var writer = MemoryStreamPool.Shared.CreateStreamWriter ()) { try { - var jti = new JavaCallableWrapperGenerator (t, Log.LogWarning, cache) { + var jti = new JavaCallableWrapperGenerator (t, Log.LogWarning, cache, classifier) { GenerateOnCreateOverrides = generateOnCreateOverrides, ApplicationJavaClass = ApplicationJavaClass, MonoRuntimeInitialization = monoInit, @@ -361,7 +399,11 @@ bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCac jti.Generate (writer); #if ENABLE_MARSHAL_METHODS - overriddenMethodDescriptors.AddRange (jti.OverriddenMethodDescriptors); + if (!Debug) { + if (classifier.FoundDynamicallyRegisteredMethods) { + 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."); + } + } #endif writer.Flush (); @@ -397,7 +439,9 @@ bool CreateJavaSources (IEnumerable javaTypes, TypeDefinitionCac } } #if ENABLE_MARSHAL_METHODS - BuildEngine4.RegisterTaskObjectAssemblyLocal (MarshalMethodsRegisterTaskKey, overriddenMethodDescriptors, RegisteredTaskObjectLifetime.Build); + if (!Debug) { + BuildEngine4.RegisterTaskObjectAssemblyLocal (MarshalMethodsRegisterTaskKey, new MarshalMethodsState (classifier.MarshalMethods), RegisteredTaskObjectLifetime.Build); + } #endif return ok; } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs index 5b67610cb6e..16105940dd2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs @@ -442,10 +442,12 @@ void AddEnvironment () }; appConfigAsmGen.Init (); #if ENABLE_MARSHAL_METHODS - var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator () { + var marshalMethodsState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (GenerateJavaStubs.MarshalMethodsRegisterTaskKey, RegisteredTaskObjectLifetime.Build); + + var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator { NumberOfAssembliesInApk = assemblyCount, UniqueAssemblyNames = uniqueAssemblyNames, - OverriddenMethodDescriptors = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal> (GenerateJavaStubs.MarshalMethodsRegisterTaskKey, RegisteredTaskObjectLifetime.Build) + MarshalMethods = marshalMethodsState?.MarshalMethods, }; marshalMethodsAsmGen.Init (); #endif diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs new file mode 100644 index 00000000000..24e841e5252 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -0,0 +1,153 @@ +#if ENABLE_MARSHAL_METHODS +using System; +using System.Collections.Generic; +using System.IO; + +using Java.Interop.Tools.Cecil; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks +{ + class MarshalMethodsAssemblyRewriter + { + IDictionary> methods; + ICollection uniqueAssemblies; + IDictionary > assemblyPaths; + TaskLoggingHelper log; + + public MarshalMethodsAssemblyRewriter (IDictionary> methods, ICollection uniqueAssemblies, IDictionary > assemblyPaths, TaskLoggingHelper log) + { + this.methods = methods ?? throw new ArgumentNullException (nameof (methods)); + this.uniqueAssemblies = uniqueAssemblies ?? throw new ArgumentNullException (nameof (uniqueAssemblies)); + this.assemblyPaths = assemblyPaths ?? throw new ArgumentNullException (nameof (assemblyPaths)); + this.log = log ?? throw new ArgumentNullException (nameof (log)); + } + + public void Rewrite (DirectoryAssemblyResolver resolver) + { + MethodDefinition unmanagedCallersOnlyAttributeCtor = GetUnmanagedCallersOnlyAttributeConstructor (resolver); + var unmanagedCallersOnlyAttributes = new Dictionary (); + foreach (AssemblyDefinition asm in uniqueAssemblies) { + unmanagedCallersOnlyAttributes.Add (asm, CreateImportedUnmanagedCallersOnlyAttribute (asm, unmanagedCallersOnlyAttributeCtor)); + } + + 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); + } + } + + Console.WriteLine (); + Console.WriteLine ("Rewriting assemblies"); + + var newAssemblyPaths = new List (); + foreach (AssemblyDefinition asm in uniqueAssemblies) { + foreach (string path in GetAssemblyPaths (asm)) { + var writerParams = new WriterParameters { + WriteSymbols = (File.Exists (path + ".mdb") || File.Exists (Path.ChangeExtension (path, ".pdb"))), + }; + + string output = $"{path}.new"; + Console.WriteLine ($"\t{asm.Name} => {output}"); + asm.Write (output, writerParams); + newAssemblyPaths.Add (output); + } + } + + // Replace old versions of the assemblies only after we've finished rewriting without issues, otherwise leave the new + // versions around. + foreach (string path in newAssemblyPaths) { + string target = Path.Combine (Path.GetDirectoryName (path), Path.GetFileNameWithoutExtension (path)); + MoveFile (path, target); + + string source = Path.ChangeExtension (path, ".pdb"); + if (File.Exists (source)) { + target = Path.ChangeExtension (Path.Combine (Path.GetDirectoryName (source), Path.GetFileNameWithoutExtension (source)), ".pdb"); + + MoveFile (source, target); + } + + source = $"{path}.mdb"; + if (File.Exists (source)) { + target = Path.ChangeExtension (path, ".mdb"); + MoveFile (source, target); + } + } + + Console.WriteLine (); + Console.WriteLine ("Method tokens:"); + foreach (IList methodList in methods.Values) { + foreach (MarshalMethodEntry method in methodList) { + Console.WriteLine ($"\t{method.NativeCallback.FullName} (token: 0x{method.NativeCallback.MetadataToken.RID:x})"); + } + } + + void MoveFile (string source, string target) + { + Console.WriteLine ($"Moving '{source}' => '{target}'"); + Files.CopyIfChanged (source, target); + try { + File.Delete (source); + } catch (Exception ex) { + log.LogWarning ($"Unable to delete source file '{source}' when moving it to '{target}'"); + } + } + } + + ICollection GetAssemblyPaths (AssemblyDefinition asm) + { + if (!assemblyPaths.TryGetValue (asm.Name.Name, out HashSet paths)) { + throw new InvalidOperationException ($"Unable to determine file path for assembly '{asm.Name.Name}'"); + } + + return paths; + } + + MethodDefinition GetUnmanagedCallersOnlyAttributeConstructor (DirectoryAssemblyResolver resolver) + { + AssemblyDefinition asm = resolver.Resolve ("System.Runtime.InteropServices"); + TypeDefinition unmanagedCallersOnlyAttribute = null; + foreach (ModuleDefinition md in asm.Modules) { + foreach (ExportedType et in md.ExportedTypes) { + if (!et.IsForwarder) { + continue; + } + + if (String.Compare ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", et.FullName, StringComparison.Ordinal) != 0) { + continue; + } + + unmanagedCallersOnlyAttribute = et.Resolve (); + break; + } + } + + if (unmanagedCallersOnlyAttribute == null) { + throw new InvalidOperationException ("Unable to find the System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute type"); + } + + foreach (MethodDefinition md in unmanagedCallersOnlyAttribute.Methods) { + if (!md.IsConstructor) { + continue; + } + + return md; + } + + throw new InvalidOperationException ("Unable to find the System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute type constructor"); + } + + CustomAttribute CreateImportedUnmanagedCallersOnlyAttribute (AssemblyDefinition targetAssembly, MethodDefinition unmanagedCallersOnlyAtributeCtor) + { + return new CustomAttribute (targetAssembly.MainModule.ImportReference (unmanagedCallersOnlyAtributeCtor)); + } + } +} +#endif diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs new file mode 100644 index 00000000000..a6801fa6d13 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs @@ -0,0 +1,397 @@ +using System; +using System.Collections.Generic; + +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.JavaCallableWrappers; +using Java.Interop.Tools.TypeNameMappings; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; + +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; } + + public MarshalMethodEntry (TypeDefinition declaringType, MethodDefinition nativeCallback, MethodDefinition connector, MethodDefinition + registeredMethod, MethodDefinition implementedMethod, FieldDefinition callbackField, string jniTypeName, + string jniName, string jniSignature) + { + DeclaringType = declaringType ?? throw new ArgumentNullException (nameof (declaringType)); + NativeCallback = 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)); + CallbackField = callbackField; // we don't require the callback field to exist + JniTypeName = EnsureNonEmpty (jniTypeName, nameof (jniTypeName)); + JniMethodName = EnsureNonEmpty (jniName, nameof (jniName)); + JniMethodSignature = EnsureNonEmpty (jniSignature, nameof (jniSignature)); + } + + string EnsureNonEmpty (string s, string argName) + { + if (String.IsNullOrEmpty (s)) { + throw new ArgumentException ("must not be null or empty", argName); + } + + return s; + } + } +#endif + + class MarshalMethodsClassifier : JavaCallableMethodClassifier + { +#if ENABLE_MARSHAL_METHODS + sealed class ConnectorInfo + { + public string MethodName { get; } + public string TypeName { get; } + public AssemblyNameReference AssemblyName { get; } + + public ConnectorInfo (string spec) + { + string[] connectorSpec = spec.Split (':'); + MethodName = connectorSpec[0]; + + if (connectorSpec.Length < 2) { + return; + } + + string fullTypeName = connectorSpec[1]; + int comma = fullTypeName.IndexOf (','); + TypeName = fullTypeName.Substring (0, comma); + AssemblyName = AssemblyNameReference.Parse (fullTypeName.Substring (comma + 1).Trim ()); + } + } + + interface IMethodSignatureMatcher + { + bool Matches (MethodDefinition method); + } + + sealed class NativeCallbackSignature : IMethodSignatureMatcher + { + static readonly HashSet verbatimTypes = new HashSet (StringComparer.Ordinal) { + "System.Boolean", + "System.Byte", + "System.Char", + "System.Double", + "System.Int16", + "System.Int32", + "System.Int64", + "System.IntPtr", + "System.SByte", + "System.Single", + "System.UInt16", + "System.UInt32", + "System.UInt64", + "System.Void", + }; + + readonly List paramTypes; + readonly string returnType; + readonly TaskLoggingHelper log; + + public NativeCallbackSignature (MethodDefinition target, TaskLoggingHelper log) + { + this.log = log; + returnType = MapType (target.ReturnType.FullName); + paramTypes = new List { + "System.IntPtr", // jnienv + "System.IntPtr", // native__this + }; + + foreach (ParameterDefinition pd in target.Parameters) { + paramTypes.Add (MapType (pd.ParameterType.FullName)); + } + } + + string MapType (string type) + { + if (verbatimTypes.Contains (type)) { + return type; + } + + return "System.IntPtr"; + } + + 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)"); + 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)"); + 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}'"); + return false; + } + } + + return true; + } + } + + TypeDefinitionCache tdCache; + DirectoryAssemblyResolver resolver; + Dictionary> marshalMethods; + HashSet assemblies; + TaskLoggingHelper log; + bool haveDynamicMethods; + + public IDictionary> MarshalMethods => marshalMethods; + public ICollection Assemblies => assemblies; + public bool FoundDynamicallyRegisteredMethods => haveDynamicMethods; +#endif + + public MarshalMethodsClassifier (TypeDefinitionCache tdCache, DirectoryAssemblyResolver res, TaskLoggingHelper log) + { +#if ENABLE_MARSHAL_METHODS + this.log = log ?? throw new ArgumentNullException (nameof (log)); + this.tdCache = tdCache ?? throw new ArgumentNullException (nameof (tdCache)); + resolver = res ?? throw new ArgumentNullException (nameof (tdCache)); + marshalMethods = new Dictionary> (StringComparer.Ordinal); + assemblies = new HashSet (); +#endif + } + + public override bool ShouldBeDynamicallyRegistered (TypeDefinition topType, MethodDefinition registeredMethod, MethodDefinition implementedMethod, CustomAttribute? registerAttribute) + { +#if ENABLE_MARSHAL_METHODS + if (registeredMethod == null) { + throw new ArgumentNullException (nameof (registeredMethod)); + } + + if (implementedMethod == null) { + throw new ArgumentNullException (nameof (registeredMethod)); + } + + if (registerAttribute == null) { + throw new ArgumentNullException (nameof (registerAttribute)); + } + + if (!IsDynamicallyRegistered (topType, registeredMethod, implementedMethod, registerAttribute)) { + return false; + } + + haveDynamicMethods = true; +#endif // def ENABLE_MARSHAL_METHODS + return true; + } + +#if ENABLE_MARSHAL_METHODS + 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})"); + Console.WriteLine ($"\tTop type: {topType.FullName}\n\tManaged type: {registeredMethod.DeclaringType.FullName}, {registeredMethod.DeclaringType.GetPartialAssemblyName (tdCache)}"); + if (registerAttribute.ConstructorArguments.Count != 3) { + log.LogWarning ($"Method '{registeredMethod.FullName}' will be registered dynamically, not enough arguments to the [Register] attribute to generate marshal method."); + return true; + } + + var connector = new ConnectorInfo ((string)registerAttribute.ConstructorArguments[2].Value); + + Console.WriteLine ($"\tconnector: {connector.MethodName} (from spec: '{(string)registerAttribute.ConstructorArguments[2].Value}')"); + + if (IsStandardHandler (topType, connector, registeredMethod, implementedMethod, jniName: (string)registerAttribute.ConstructorArguments[0].Value, jniSignature: (string)registerAttribute.ConstructorArguments[1].Value)) { + return false; + } + + log.LogWarning ($"Method '{registeredMethod.FullName}' will be registered dynamically"); + return true; + } + + // TODO: Probably should check if all the methods and fields are private and static - only then it is safe(ish) to remove them + bool IsStandardHandler (TypeDefinition topType, ConnectorInfo connector, MethodDefinition registeredMethod, MethodDefinition implementedMethod, string jniName, string jniSignature) + { + const string HandlerNameStart = "Get"; + const string HandlerNameEnd = "Handler"; + + string connectorName = connector.MethodName; + if (connectorName.Length < HandlerNameStart.Length + HandlerNameEnd.Length + 1 || + !connectorName.StartsWith (HandlerNameStart, StringComparison.Ordinal) || + !connectorName.EndsWith (HandlerNameEnd, StringComparison.Ordinal)) { + log.LogWarning ($"\tConnector name '{connectorName}' must start with '{HandlerNameStart}', end with '{HandlerNameEnd}' and have at least one character between the two parts."); + return false; + } + + 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)}"; + + TypeDefinition connectorDeclaringType = connector.AssemblyName == null ? registeredMethod.DeclaringType : FindType (resolver.Resolve (connector.AssemblyName), connector.TypeName); + Console.WriteLine ($"\tconnector name: {connectorName}\n\tnative callback name: {nativeCallbackName}\n\tdelegate field name: {delegateFieldName}"); + + MethodDefinition connectorMethod = FindMethod (connectorDeclaringType, connectorName); + if (connectorMethod == null) { + log.LogWarning ($"\tConnector method '{connectorName}' not found in type '{connectorDeclaringType.FullName}'"); + return false; + } + + if (String.Compare ("System.Delegate", connectorMethod.ReturnType.FullName, StringComparison.Ordinal) != 0) { + log.LogWarning ($"\tConnector '{connectorName}' in type '{connectorDeclaringType.FullName}' has invalid return type, expected 'System.Delegate', found '{connectorMethod.ReturnType.FullName}'"); + return false; + } + + 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"); + return false; + } + + // In the standard handler "pattern", the native callback backing field is private, static and thus in the same type + // as the native callback. + FieldDefinition delegateField = FindField (nativeCallbackMethod.DeclaringType, delegateFieldName); + if (delegateField != null) { + if (String.Compare ("System.Delegate", delegateField.FieldType.FullName, StringComparison.Ordinal) != 0) { + log.LogWarning ($"\tdelegate field '{delegateFieldName}' in type '{nativeCallbackMethod.DeclaringType.FullName}' has invalid type, expected 'System.Delegate', found '{delegateField.FieldType.FullName}'"); + return false; + } + } + + Console.WriteLine ($"##G1: {implementedMethod.DeclaringType.FullName} -> {JavaNativeTypeManager.ToJniName (implementedMethod.DeclaringType, tdCache)}"); + Console.WriteLine ($"##G1: top type: {topType.FullName} -> {JavaNativeTypeManager.ToJniName (topType, tdCache)}"); + + StoreMethod ( + connectorName, + registeredMethod, + new MarshalMethodEntry ( + topType, + nativeCallbackMethod, + connectorMethod, + registeredMethod, + implementedMethod, + delegateField, + JavaNativeTypeManager.ToJniName (topType, tdCache), + jniName, + jniSignature) + ); + + StoreAssembly (connectorMethod.Module.Assembly); + StoreAssembly (nativeCallbackMethod.Module.Assembly); + if (delegateField != null) { + StoreAssembly (delegateField.Module.Assembly); + } + + return true; + } + + TypeDefinition FindType (AssemblyDefinition asm, string typeName) + { + foreach (ModuleDefinition md in asm.Modules) { + foreach (TypeDefinition td in md.Types) { + TypeDefinition match = GetMatchingType (td); + if (match != null) { + return match; + } + } + } + + return null; + + TypeDefinition GetMatchingType (TypeDefinition def) + { + if (String.Compare (def.FullName, typeName, StringComparison.Ordinal) == 0) { + return def; + } + + if (!def.HasNestedTypes) { + return null; + } + + TypeDefinition ret; + foreach (TypeDefinition nested in def.NestedTypes) { + ret = GetMatchingType (nested); + if (ret != null) { + return ret; + } + } + + return null; + } + } + + MethodDefinition FindMethod (TypeDefinition type, string methodName, IMethodSignatureMatcher signatureMatcher = null) + { + foreach (MethodDefinition method in type.Methods) { + if (!method.IsManaged || method.IsConstructor) { + continue; + } + + if (String.Compare (methodName, method.Name, StringComparison.Ordinal) != 0) { + continue; + } + + if (signatureMatcher == null || signatureMatcher.Matches (method)) { + return method; + } + } + + if (type.BaseType == null) { + return null; + } + + return FindMethod (tdCache.Resolve (type.BaseType), methodName, signatureMatcher); + } + + FieldDefinition FindField (TypeDefinition type, string fieldName, bool lookForInherited = false) + { + foreach (FieldDefinition field in type.Fields) { + if (String.Compare (field.Name, fieldName, StringComparison.Ordinal) == 0) { + return field; + } + } + + if (!lookForInherited || type.BaseType == null) { + return null; + } + + return FindField (tdCache.Resolve (type.BaseType), fieldName, lookForInherited); + } + + 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; + } + + if (!marshalMethods.TryGetValue (key, out IList list) || list == null) { + list = new List (); + marshalMethods.Add (key, list); + } + list.Add (entry); + } + + void StoreAssembly (AssemblyDefinition asm) + { + if (assemblies.Contains (asm)) { + return; + } + + assemblies.Add (asm); + } +#endif + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs index cb2a8d56df5..e18ab55f20b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; using System.Text; using Java.Interop.Tools.TypeNameMappings; @@ -17,6 +18,12 @@ namespace Xamarin.Android.Tasks { class MarshalMethodsNativeAssemblyGenerator : LlvmIrComposer { + sealed class MarshalMethodInfo + { + public MarshalMethodEntry Method { get; } + public string NativeSymbolName { get; } + } + // This is here only to generate strongly-typed IR internal sealed class MonoClass {} @@ -29,15 +36,17 @@ struct MarshalMethodsManagedClass MonoClass klass; }; - public ICollection UniqueAssemblyNames { get; set; } - public int NumberOfAssembliesInApk { get; set; } - public List OverriddenMethodDescriptors { get; set; } + public ICollection UniqueAssemblyNames { get; set; } + public int NumberOfAssembliesInApk { get; set; } + public IDictionary> MarshalMethods { get; set; } StructureInfo monoImage; StructureInfo monoClass; public override void Init () - {} + { + Console.WriteLine ($"Marshal methods count: {MarshalMethods?.Count ?? 0}"); + } protected override void MapStructures (LlvmIrGenerator generator) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsState.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsState.cs new file mode 100644 index 00000000000..85063dc7dbe --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsState.cs @@ -0,0 +1,17 @@ +#if ENABLE_MARSHAL_METHODS +using System; +using System.Collections.Generic; + +namespace Xamarin.Android.Tasks +{ + sealed class MarshalMethodsState + { + public IDictionary> MarshalMethods { get; } + + public MarshalMethodsState (IDictionary> marshalMethods) + { + MarshalMethods = marshalMethods ?? throw new ArgumentNullException (nameof (marshalMethods)); + } + } +} +#endif diff --git a/src/monodroid/CMakeLists.txt b/src/monodroid/CMakeLists.txt index 7fca9eaf631..41577f4f55a 100644 --- a/src/monodroid/CMakeLists.txt +++ b/src/monodroid/CMakeLists.txt @@ -744,9 +744,9 @@ target_link_libraries( ${LINK_LIBS} xamarin-app ) -if((NOT DEBUG_BUILD) AND ANDROID AND ENABLE_NET AND ENABLE_MARSHAL_METHODS) - target_link_libraries( - ${XAMARIN_MONO_ANDROID_LIB} - ${XAMARIN_APP_MARSHALING_LIB} - ) -endif() +# if((NOT DEBUG_BUILD) AND ANDROID AND ENABLE_NET AND ENABLE_MARSHAL_METHODS) +# target_link_libraries( +# ${XAMARIN_MONO_ANDROID_LIB} +# ${XAMARIN_APP_MARSHALING_LIB} +# ) +# endif() diff --git a/src/monodroid/jni/embedded-assemblies.cc b/src/monodroid/jni/embedded-assemblies.cc index 31182173f89..0c4e0bf6986 100644 --- a/src/monodroid/jni/embedded-assemblies.cc +++ b/src/monodroid/jni/embedded-assemblies.cc @@ -357,6 +357,10 @@ EmbeddedAssemblies::assembly_store_open_from_bundles (dynamic_local_string