diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RewriteMarshalMethodsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RewriteMarshalMethodsStep.cs new file mode 100644 index 00000000000..e85ef7af3e7 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/RewriteMarshalMethodsStep.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Java.Interop.Tools.JavaCallableWrappers; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Mono.Linker; +using Mono.Linker.Steps; +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; + +namespace MonoDroid.Tuner; + +/// +/// Scans an assembly for marshal methods and converts them to LLVM marshal methods. +/// +public class RewriteMarshalMethodsStep : BaseStep, IAssemblyModifierPipelineStep +{ + public TaskLoggingHelper Log { get; set; } + + bool? brokenExceptionTransitionsEnabled; + + public RewriteMarshalMethodsStep (TaskLoggingHelper log) + { + Log = log; + } + + public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) + { + if (!context.IsAndroidAssembly) + return; + + var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip; + + if (action == AssemblyAction.Delete) + return; + + // We need to parse the environment files supplied by the user to see if they want to use broken exception transitions. This information is needed + // in order to properly generate wrapper methods in the marshal methods assembly rewriter. + // We don't care about those generated by us, since they won't contain the `XA_BROKEN_EXCEPTION_TRANSITIONS` variable we look for. + if (!brokenExceptionTransitionsEnabled.HasValue) { + var environmentParser = new EnvironmentFilesParser (); + brokenExceptionTransitionsEnabled = environmentParser.AreBrokenExceptionTransitionsEnabled (context.Environments); + } + + var collection = MarshalMethodsCollection.FromAssembly (context.Architecture, assembly, Context.Resolver, Log); + + var state = new NativeCodeGenState (context.Architecture, Context, Context.Resolver, [], [], collection); + Run (state, context); + + var stateObject = MarshalMethodCecilAdapter.CreateNativeCodeGenState (context.Architecture, state); + var destinationMarshalMethodsXml = MarshalMethodsXmlFile.GetMarshalMethodsXmlFilePath (context.Destination.ItemSpec); + + MarshalMethodsXmlFile.Export (destinationMarshalMethodsXml, context.Architecture, stateObject, Log); + + // TODO: Only return true if we actually modified the assembly + context.IsAssemblyModified = true; + } + + void Run (NativeCodeGenState state, StepContext context) + { + if (state.Classifier is null) { + Log.LogError ("state.Classifier cannot be null if marshal methods are enabled"); + return; + } + + if (!context.EnableManagedMarshalMethodsLookup) { + RewriteMethods (state, brokenExceptionTransitionsEnabled.GetValueOrDefault ()); + 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); + RewriteMethods (state, brokenExceptionTransitionsEnabled.GetValueOrDefault ()); + } + + Log.LogDebugMessage ($"[{state.TargetArch}] Number of generated marshal methods: {state.Classifier.MarshalMethods.Count}"); + if (state.Classifier.DynamicallyRegisteredMarshalMethods.Count > 0) { + Log.LogWarning ($"[{state.TargetArch}] Number of methods in the project that will be registered dynamically: {state.Classifier.DynamicallyRegisteredMarshalMethods.Count}"); + } + + var wrappedCount = state.Classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround)); + + if (wrappedCount > 0) { + // TODO: change to LogWarning once the generator can output code which requires no non-blittable wrappers + Log.LogDebugMessage ($"[{state.TargetArch}] Number of methods in the project that need marshal method wrappers: {wrappedCount}"); + } + } + + void RewriteMethods (NativeCodeGenState state, bool brokenExceptionTransitionsEnabled) + { + if (state.Classifier == null) { + return; + } + + 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/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index 4086bde3e99..6c19f9f623c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -24,6 +24,8 @@ public class AssemblyModifierPipeline : AndroidTask { public override string TaskPrefix => "AMP"; + public string AndroidSdkPlatform { get; set; } = ""; + public string ApplicationJavaClass { get; set; } = ""; public string CodeGenerationTarget { get; set; } = ""; @@ -37,8 +39,15 @@ public class AssemblyModifierPipeline : AndroidTask public bool EnableMarshalMethods { get; set; } + public bool EnableManagedMarshalMethodsLookup { get; set; } + + public ITaskItem [] Environments { get; set; } = []; + public bool ErrorOnCustomJavaObject { get; set; } + // If we're using ILLink, this process modifies the linked assemblies in place + protected virtual bool ModifiesAssembliesInPlace => true; + public string? PackageNamingPolicy { get; set; } /// @@ -64,7 +73,7 @@ public class AssemblyModifierPipeline : AndroidTask [Required] public string TargetName { get; set; } = ""; - protected JavaPeerStyle codeGenerationTarget; + JavaPeerStyle codeGenerationTarget; public override bool RunTask () { @@ -76,6 +85,8 @@ public override bool RunTask () var readerParameters = new ReaderParameters { ReadSymbols = ReadSymbols, + ReadWrite = ModifiesAssembliesInPlace || EnableMarshalMethods, + InMemory = ModifiesAssembliesInPlace || EnableMarshalMethods, }; Dictionary> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty (), validate: false); @@ -118,7 +129,7 @@ public override bool RunTask () Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); - RunPipeline (pipeline!, source, destination); + RunPipeline (pipeline!, source, destination, perArchAssemblies [sourceArch].Values.ToArray ()); } pipeline?.Dispose (); @@ -138,6 +149,13 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont findJavaObjectsStep.Initialize (context); pipeline.Steps.Add (findJavaObjectsStep); + // RewriteMarshalMethodsStep + if (EnableMarshalMethods && !Debug) { + var rewriteMarshalMethodsStep = new RewriteMarshalMethodsStep (Log); + rewriteMarshalMethodsStep.Initialize (context); + pipeline.Steps.Add (rewriteMarshalMethodsStep); + } + // SaveChangedAssemblyStep var writerParameters = new WriterParameters { DeterministicMvid = Deterministic, @@ -156,13 +174,15 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont pipeline.Steps.Add (findTypeMapObjectsStep); } - void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination) + void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination, ITaskItem [] archAssemblies) { var assembly = pipeline.Resolver.GetAssembly (source.ItemSpec); - var context = new StepContext (source, destination) { + var context = new StepContext (source, destination, AndroidSdkPlatform, Environments, archAssemblies) { + Architecture = MonoAndroidHelper.GetRequiredValidArchitecture (source), CodeGenerationTarget = codeGenerationTarget, EnableMarshalMethods = EnableMarshalMethods, + EnableManagedMarshalMethodsLookup = EnableManagedMarshalMethodsLookup, IsAndroidAssembly = MonoAndroidHelper.IsAndroidAssembly (source), IsDebug = Debug, IsFrameworkAssembly = MonoAndroidHelper.IsFrameworkAssembly (source), diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 31818e8cafc..36cd5c529bd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -155,6 +155,7 @@ void Run (bool useMarshalMethods) // Now that "never" never happened, we can proceed knowing that at least the assembly sets are the same for each architecture var nativeCodeGenStates = new ConcurrentDictionary (); + var nativeCodeGenStateObjects = new ConcurrentDictionary (); NativeCodeGenState? templateCodeGenState = null; var firstArch = allAssembliesPerArch.First ().Key; @@ -169,7 +170,7 @@ void Run (bool useMarshalMethods) // Pick the "first" one as the one to generate Java code for var generateJavaCode = arch == firstArch; - (bool success, NativeCodeGenState? state) = GenerateJavaSourcesAndMaybeClassifyMarshalMethods (arch, archAssemblies, MaybeGetArchAssemblies (userAssembliesPerArch, arch), useMarshalMethods, generateJavaCode); + (bool success, NativeCodeGenState? state, NativeCodeGenStateObject? stateObject) = GenerateJavaSourcesAndMaybeClassifyMarshalMethods (arch, archAssemblies, MaybeGetArchAssemblies (userAssembliesPerArch, arch), useMarshalMethods, generateJavaCode); if (!success) { generateSucceeded = false; @@ -181,6 +182,7 @@ void Run (bool useMarshalMethods) } nativeCodeGenStates.TryAdd (arch, state); + nativeCodeGenStateObjects.TryAdd (arch, stateObject); }); // If we hit an error generating the Java code, we should bail out now @@ -198,6 +200,21 @@ void Run (bool useMarshalMethods) // Save NativeCodeGenState for later tasks Log.LogDebugMessage ($"Saving {nameof (NativeCodeGenState)} to {nameof (NativeCodeGenStateRegisterTaskKey)}"); BuildEngine4.RegisterTaskObjectAssemblyLocal (MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (NativeCodeGenStateRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), nativeCodeGenStates, RegisteredTaskObjectLifetime.Build); + + // If we still need the NativeCodeGenState in the task because we're using marshal methods, + // we're going to transfer it to a new object that doesn't require holding open Cecil AssemblyDefinitions. + if (useMarshalMethods) { + //var nativeCodeGenStateObject = MarshalMethodCecilAdapter.GetNativeCodeGenStateCollection (Log, nativeCodeGenStates); + var nativeCodeGenStateCollection = new NativeCodeGenStateCollection (); + + foreach (var kvp in nativeCodeGenStateObjects) { + nativeCodeGenStateCollection.States.Add (kvp.Key, kvp.Value); + Log.LogDebugMessage ($"Added NativeCodeGenStateObject for arch: {kvp.Key}, containing {kvp.Value.MarshalMethods.Count} marshal methods"); + } + + Log.LogDebugMessage ($"Saving {nameof (NativeCodeGenStateObject)} to {nameof (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey)}"); + BuildEngine4.RegisterTaskObjectAssemblyLocal (MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), nativeCodeGenStateCollection, RegisteredTaskObjectLifetime.Build); + } } internal static Dictionary MaybeGetArchAssemblies (Dictionary> dict, AndroidTargetArch arch) @@ -209,7 +226,7 @@ internal static Dictionary MaybeGetArchAssemblies (Dictionary return archDict; } - (bool success, NativeCodeGenState? stubsState) GenerateJavaSourcesAndMaybeClassifyMarshalMethods (AndroidTargetArch arch, Dictionary assemblies, Dictionary userAssemblies, bool useMarshalMethods, bool generateJavaCode) + (bool success, NativeCodeGenState? stubsState, NativeCodeGenStateObject? stateObject) GenerateJavaSourcesAndMaybeClassifyMarshalMethods (AndroidTargetArch arch, Dictionary assemblies, Dictionary userAssemblies, bool useMarshalMethods, bool generateJavaCode) { XAAssemblyResolver resolver = MakeResolver (useMarshalMethods, arch, assemblies); var tdCache = new TypeDefinitionCache (); @@ -227,15 +244,62 @@ internal static Dictionary MaybeGetArchAssemblies (Dictionary } if (!success) { - return (false, null); + return (false, null, null); } MarshalMethodsCollection? marshalMethodsCollection = null; + NativeCodeGenStateObject? stateObject = null; + + if (useMarshalMethods) { + stateObject = new NativeCodeGenStateObject (); + + foreach (var assembly in assemblies.Values) { + var marshalMethodXmlFile = MarshalMethodsXmlFile.GetMarshalMethodsXmlFilePath (assembly.ItemSpec); + + if (!File.Exists (marshalMethodXmlFile)) + continue; + + var xml = MarshalMethodsXmlFile.Import (marshalMethodXmlFile); + + if (xml is null || xml?.Value.MarshalMethods.Count == 0) { + Log.LogDebugMessage ($"'{marshalMethodXmlFile}' is empty, skipping."); + continue; + } + + foreach (var kvp in xml.Value.Value.MarshalMethods) { + if (!stateObject.MarshalMethods.TryGetValue (kvp.Key, out var methods)) { + methods = new List (); + stateObject.MarshalMethods.Add (kvp.Key, methods); + } + + foreach (var method in kvp.Value) { + // We don't need to add the special case method multiple times + if (methods.Count > 0 && method.IsSpecial) + continue; + methods.Add (method); + } + } + } + + //marshalMethodsCollection = MarshalMethodsCollection.FromAssemblies (arch, assemblies.Values.ToList (), resolver, Log); + //marshalMethodsCollection.AddSpecialCaseMethods (); + + marshalMethodsCollection = new EmptyMarshalMethodsCollection (); + } + + var state = new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, marshalMethodsCollection); - if (useMarshalMethods) - marshalMethodsCollection = MarshalMethodsCollection.FromAssemblies (arch, assemblies.Values.ToList (), resolver, Log); + //if (useMarshalMethods) { + // var info = new ManagedMarshalMethodsLookupInfo (Log); - return (true, new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, marshalMethodsCollection)); + // foreach (var kvp in marshalMethodsCollection.MarshalMethods) + // foreach (var method in kvp.Value) + // info.AddNativeCallbackWrapper (method.NativeCallback); + + // state.ManagedMarshalMethodsLookupInfo = info; + //} + + return (true, state, stateObject); } (List allJavaTypes, List javaTypesForJCW) ScanForJavaTypes (XAAssemblyResolver res, TypeDefinitionCache cache, Dictionary assemblies, Dictionary userAssemblies, bool useMarshalMethods) @@ -343,4 +407,43 @@ void CompareLists (string name, List list1, List list2) Log.LogDebugMessage ($"No differences"); } } + + class EmptyMarshalMethodsCollection : MarshalMethodsCollection + { + public override HashSet AssembliesWithMarshalMethods => throw new NotSupportedException (); + + /// + /// Marshal methods that have already been rewritten as LLVM marshal methods. + /// + public override IDictionary> ConvertedMarshalMethods => throw new NotSupportedException (); + + /// + /// Marshal methods that cannot be rewritten and must be registered dynamically. + /// + public override List DynamicallyRegisteredMarshalMethods => throw new NotSupportedException (); + + /// + /// Marshal methods that can be rewritten as LLVM marshal methods. + /// + public override IDictionary> MarshalMethods => throw new NotSupportedException (); + + public EmptyMarshalMethodsCollection () + { + } + + public override void AddSpecialCaseMethods () + { + throw new NotImplementedException (); + } + + public override bool ShouldBeDynamicallyRegistered (TypeDefinition topType, MethodDefinition registeredMethod, MethodDefinition implementedMethod, CustomAttribute registerAttribute) + { + throw new NotImplementedException (); + } + + public override bool TypeHasDynamicallyRegisteredMethods (TypeDefinition type) + { + throw new NotImplementedException (); + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs index 1b9195d85c7..c04c0ea72b7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs @@ -28,7 +28,6 @@ public class GenerateMainAndroidManifest : AndroidTask public string CodeGenerationTarget { get; set; } = ""; public bool Debug { get; set; } public bool EmbedAssemblies { get; set; } - public bool EnableMarshalMethods { get; set; } [Required] public string IntermediateOutputDirectory { get; set; } = ""; public string []? ManifestPlaceholders { get; set; } @@ -52,8 +51,6 @@ public class GenerateMainAndroidManifest : AndroidTask AndroidRuntime androidRuntime; JavaPeerStyle codeGenerationTarget; - bool UseMarshalMethods => !Debug && EnableMarshalMethods; - public override bool RunTask () { // Retrieve the stored NativeCodeGenState (and remove it from the cache) @@ -74,16 +71,6 @@ public override bool RunTask () var additionalProviders = MergeManifest (templateCodeGenState, GenerateJavaStubs.MaybeGetArchAssemblies (userAssembliesPerArch, templateCodeGenState.TargetArch)); GenerateAdditionalProviderSources (templateCodeGenState, additionalProviders); - - // If we still need the NativeCodeGenState in the task because we're using marshal methods, - // we're going to transfer it to a new object that doesn't require holding open Cecil AssemblyDefinitions. - if (UseMarshalMethods) { - var nativeCodeGenStateObject = MarshalMethodCecilAdapter.GetNativeCodeGenStateCollection (Log, nativeCodeGenStates); - - Log.LogDebugMessage ($"Saving {nameof (NativeCodeGenStateObject)} to {nameof (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey)}"); - BuildEngine4.RegisterTaskObjectAssemblyLocal (MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), nativeCodeGenStateObject, RegisteredTaskObjectLifetime.Build); - } - // Dispose the Cecil resolvers so the assemblies are closed. Log.LogDebugMessage ($"Disposing all {nameof (NativeCodeGenState)}.{nameof (NativeCodeGenState.Resolver)}"); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs index a811405e481..dc8faf555a2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs @@ -63,12 +63,12 @@ public override bool RunTask () // If using marshal methods, we cannot use the .typemap.xml files currently because // the type token ids were changed by the marshal method rewriter after we wrote the .xml files. - if (!useMarshalMethods) + //if (!useMarshalMethods) GenerateAllTypeMappings (); // Generate typemaps from the native code generator state (produced by the marshal method rewriter) - if (RunCheckedBuild || useMarshalMethods) - GenerateAllTypeMappingsFromNativeState (useMarshalMethods); + //if (RunCheckedBuild || useMarshalMethods) + // GenerateAllTypeMappingsFromNativeState (useMarshalMethods); return !Log.HasLoggedErrors; } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs index d3e061b1f65..4339c0cd53e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs @@ -14,6 +14,9 @@ public class LinkAssembliesNoShrink : AssemblyModifierPipeline public bool AddKeepAlives { get; set; } + // If we're running instead of ILLink, this process writes copies of the assemblies to a new location + protected override bool ModifiesAssembliesInPlace => false; + public bool UseDesignerAssembly { get; set; } protected override void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs index 703b08d19d1..67b58dc872b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs @@ -4,6 +4,7 @@ using Java.Interop.Tools.JavaCallableWrappers; using Microsoft.Build.Framework; using Mono.Cecil; +using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; @@ -51,22 +52,31 @@ public interface IAssemblyModifierPipelineStep public class StepContext { + public AndroidTargetArch Architecture { get; set; } + public string AndroidSdkPlatform { get; set; } public JavaPeerStyle CodeGenerationTarget { get; set; } public ITaskItem Destination { get; } public bool EnableMarshalMethods { get; set; } + public bool EnableManagedMarshalMethodsLookup { get; set; } + public ITaskItem [] Environments { get; set; } public bool IsAndroidAssembly { get; set; } public bool IsAssemblyModified { get; set; } public bool IsDebug { get; set; } public bool IsFrameworkAssembly { get; set; } public bool IsMainAssembly { get; set; } public bool IsUserAssembly { get; set; } + // This only contains the resolved assemblies for *this* architecture + public ITaskItem [] ResolvedAssemblies { get; set; } public ITaskItem Source { get; } public bool IsAndroidUserAssembly => IsAndroidAssembly && IsUserAssembly; - public StepContext (ITaskItem source, ITaskItem destination) + public StepContext (ITaskItem source, ITaskItem destination, string androidSdkPlatform, ITaskItem [] environments, ITaskItem [] resolvedAssemblies) { - Source = source; + AndroidSdkPlatform = androidSdkPlatform; Destination = destination; + Environments = environments; + ResolvedAssemblies = resolvedAssemblies; + Source = source; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs index cb446607eea..d44e1e31c90 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs @@ -25,11 +25,11 @@ class JCWGeneratorContext { public AndroidTargetArch Arch { get; } public TypeDefinitionCache TypeDefinitionCache { get; } - public XAAssemblyResolver Resolver { get; } + public IAssemblyResolver Resolver { get; } public IList JavaTypes { get; } public ICollection ResolvedAssemblies { get; } - public JCWGeneratorContext (AndroidTargetArch arch, XAAssemblyResolver res, ICollection resolvedAssemblies, List javaTypesForJCW, TypeDefinitionCache tdCache) + public JCWGeneratorContext (AndroidTargetArch arch, IAssemblyResolver res, ICollection resolvedAssemblies, List javaTypesForJCW, TypeDefinitionCache tdCache) { Arch = arch; Resolver = res; @@ -174,7 +174,7 @@ public static void EnsureAllArchitecturesAreIdentical (TaskLoggingHelper logger, } EnsureIdenticalCollections (logger, templateState, state); - EnsureClassifiersMatch (logger, templateState, state); + //EnsureClassifiersMatch (logger, templateState, state); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs index 02f3d1927b8..6b2fabf1360 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs @@ -11,6 +11,7 @@ namespace Xamarin.Android.Tasks; class MarshalMethodCecilAdapter { + [return: NotNullIfNotNull (nameof (nativeCodeGenStates))] public static NativeCodeGenStateCollection? GetNativeCodeGenStateCollection (TaskLoggingHelper log, ConcurrentDictionary? nativeCodeGenStates) { if (nativeCodeGenStates is null) { @@ -31,7 +32,7 @@ class MarshalMethodCecilAdapter return collection; } - static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch, NativeCodeGenState state) + public static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch, NativeCodeGenState state) { var obj = new NativeCodeGenStateObject (); @@ -54,6 +55,9 @@ static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMarshalMethodsLookupInfo? info) { + var nativeCallback = entry.NativeCallback; + //var nativeCallback = entry is ConvertedMarshalMethodEntry cmm ? cmm.ConvertedNativeCallback : entry.NativeCallback; + var obj = new MarshalMethodEntryObject ( declaringType: CreateDeclaringType (entry.DeclaringType), implementedMethod: CreateMethod (entry.ImplementedMethod), @@ -61,12 +65,12 @@ static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMa jniTypeName: entry.JniTypeName, jniMethodName: entry.JniMethodName, jniMethodSignature: entry.JniMethodSignature, - nativeCallback: CreateMethod (entry.NativeCallback), + nativeCallback: CreateMethod (nativeCallback), registeredMethod: CreateMethodBase (entry.RegisteredMethod) ); if (info is not null) { - (uint assemblyIndex, uint classIndex, uint methodIndex) = info.GetIndex (entry.NativeCallback); + (uint assemblyIndex, uint classIndex, uint methodIndex) = info.GetIndex (nativeCallback); obj.NativeCallback.AssemblyIndex = assemblyIndex; obj.NativeCallback.ClassIndex = classIndex; @@ -142,7 +146,7 @@ class NativeCodeGenStateObject public Dictionary> MarshalMethods { get; } = []; } -class MarshalMethodEntryObject +public class MarshalMethodEntryObject { public MarshalMethodEntryTypeObject DeclaringType { get; } public MarshalMethodEntryMethodObject? ImplementedMethod { get; } @@ -166,7 +170,7 @@ public MarshalMethodEntryObject (MarshalMethodEntryTypeObject declaringType, Mar } } -class MarshalMethodEntryAssemblyObject +public class MarshalMethodEntryAssemblyObject { public string FullName { get; } public string NameFullName { get; } // Cecil's Assembly.Name.FullName @@ -182,7 +186,7 @@ public MarshalMethodEntryAssemblyObject (string fullName, string nameFullName, s } } -class MarshalMethodEntryModuleObject +public class MarshalMethodEntryModuleObject { public MarshalMethodEntryAssemblyObject Assembly { get; } @@ -192,7 +196,7 @@ public MarshalMethodEntryModuleObject (MarshalMethodEntryAssemblyObject assembly } } -class MarshalMethodEntryTypeObject +public class MarshalMethodEntryTypeObject { public string FullName { get; } public uint MetadataToken { get; } @@ -206,7 +210,7 @@ public MarshalMethodEntryTypeObject (string fullName, uint metadataToken, Marsha } } -class MarshalMethodEntryMethodBaseObject +public class MarshalMethodEntryMethodBaseObject { public string FullName { get; } @@ -216,7 +220,7 @@ public MarshalMethodEntryMethodBaseObject (string fullName) } } -class MarshalMethodEntryMethodObject : MarshalMethodEntryMethodBaseObject +public class MarshalMethodEntryMethodObject : MarshalMethodEntryMethodBaseObject { public string Name { get; } public MarshalMethodEntryTypeObject DeclaringType { get; } @@ -239,7 +243,7 @@ public MarshalMethodEntryMethodObject (string name, string fullName, MarshalMeth } } -class MarshalMethodEntryMethodParameterObject +public class MarshalMethodEntryMethodParameterObject { public string Name { get; } public string ParameterTypeName { get; } // Cecil's ParameterDefinition.ParameterType.Name diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs index 47bb70fd95b..9bbee5c309a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -26,11 +26,11 @@ sealed class AssemblyImports readonly TaskLoggingHelper log; readonly MarshalMethodsCollection classifier; - readonly XAAssemblyResolver resolver; + readonly IAssemblyResolver resolver; readonly AndroidTargetArch targetArch; readonly ManagedMarshalMethodsLookupInfo? managedMarshalMethodsLookupInfo; - public MarshalMethodsAssemblyRewriter (TaskLoggingHelper log, AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, ManagedMarshalMethodsLookupInfo? managedMarshalMethodsLookupInfo) + public MarshalMethodsAssemblyRewriter (TaskLoggingHelper log, AndroidTargetArch targetArch, MarshalMethodsCollection classifier, IAssemblyResolver resolver, ManagedMarshalMethodsLookupInfo? managedMarshalMethodsLookupInfo) { this.log = log ?? throw new ArgumentNullException (nameof (log)); this.targetArch = targetArch; @@ -123,78 +123,78 @@ public void Rewrite (bool brokenExceptionTransitions) managedMarshalMethodLookupGenerator.Generate (classifier.MarshalMethods.Values); } - foreach (AssemblyDefinition asm in classifier.AssembliesWithMarshalMethods) { - string? path = asm.MainModule.FileName; - if (String.IsNullOrEmpty (path)) { - throw new InvalidOperationException ($"[{targetArch}] Internal error: assembly '{asm}' does not specify path to its file"); - } - - string pathPdb = Path.ChangeExtension (path, ".pdb"); - bool havePdb = File.Exists (pathPdb); - - var writerParams = new WriterParameters { - WriteSymbols = havePdb, - }; - - string directory = Path.Combine (Path.GetDirectoryName (path), "new"); - Directory.CreateDirectory (directory); - string output = Path.Combine (directory, Path.GetFileName (path)); - log.LogDebugMessage ($"[{targetArch}] Writing new version of '{path}' assembly: {output}"); - - // TODO: this should be used eventually, but it requires that all the types are reloaded from the assemblies before typemaps are generated - // since Cecil doesn't update the MVID in the already loaded types - //asm.MainModule.Mvid = Guid.NewGuid (); - asm.Write (output, writerParams); - - CopyFile (output, path); - RemoveFile (output); - - if (havePdb) { - string outputPdb = Path.ChangeExtension (output, ".pdb"); - if (File.Exists (outputPdb)) { - CopyFile (outputPdb, pathPdb); - } - RemoveFile (outputPdb); - } - } - - void CopyFile (string source, string target) - { - log.LogDebugMessage ($"[{targetArch}] Copying rewritten assembly: {source} -> {target}"); - - string targetBackup = $"{target}.bak"; - if (File.Exists (target)) { - // Try to avoid sharing violations by first renaming the target - File.Move (target, targetBackup); - } - - File.Copy (source, target, true); - - if (File.Exists (targetBackup)) { - try { - File.Delete (targetBackup); - } catch (Exception ex) { - // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. - log.LogDebugMessage ($"[{targetArch}] While trying to delete '{targetBackup}', exception was thrown: {ex}"); - log.LogDebugMessage ($"[{targetArch}] Failed to delete backup file '{targetBackup}', ignoring."); - } - } - } - - void RemoveFile (string? path) - { - if (String.IsNullOrEmpty (path) || !File.Exists (path)) { - return; - } - - try { - log.LogDebugMessage ($"[{targetArch}] Deleting: {path}"); - File.Delete (path); - } catch (Exception ex) { - log.LogWarning ($"[{targetArch}] Unable to delete source file '{path}'"); - log.LogDebugMessage ($"[{targetArch}] {ex.ToString ()}"); - } - } + //foreach (AssemblyDefinition asm in classifier.Assemblies) { + // string? path = asm.MainModule.FileName; + // if (String.IsNullOrEmpty (path)) { + // throw new InvalidOperationException ($"[{targetArch}] Internal error: assembly '{asm}' does not specify path to its file"); + // } + + // string pathPdb = Path.ChangeExtension (path, ".pdb"); + // bool havePdb = File.Exists (pathPdb); + + // var writerParams = new WriterParameters { + // WriteSymbols = havePdb, + // }; + + // string directory = Path.Combine (Path.GetDirectoryName (path), "new"); + // Directory.CreateDirectory (directory); + // string output = Path.Combine (directory, Path.GetFileName (path)); + // log.LogDebugMessage ($"[{targetArch}] Writing new version of '{path}' assembly: {output}"); + + // // TODO: this should be used eventually, but it requires that all the types are reloaded from the assemblies before typemaps are generated + // // since Cecil doesn't update the MVID in the already loaded types + // //asm.MainModule.Mvid = Guid.NewGuid (); + // asm.Write (output, writerParams); + + // CopyFile (output, path); + // RemoveFile (output); + + // if (havePdb) { + // string outputPdb = Path.ChangeExtension (output, ".pdb"); + // if (File.Exists (outputPdb)) { + // CopyFile (outputPdb, pathPdb); + // } + // RemoveFile (outputPdb); + // } + //} + + //void CopyFile (string source, string target) + //{ + // log.LogDebugMessage ($"[{targetArch}] Copying rewritten assembly: {source} -> {target}"); + + // string targetBackup = $"{target}.bak"; + // if (File.Exists (target)) { + // // Try to avoid sharing violations by first renaming the target + // File.Move (target, targetBackup); + // } + + // File.Copy (source, target, true); + + // if (File.Exists (targetBackup)) { + // try { + // File.Delete (targetBackup); + // } catch (Exception ex) { + // // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. + // log.LogDebugMessage ($"[{targetArch}] While trying to delete '{targetBackup}', exception was thrown: {ex}"); + // log.LogDebugMessage ($"[{targetArch}] Failed to delete backup file '{targetBackup}', ignoring."); + // } + // } + //} + + //void RemoveFile (string? path) + //{ + // if (String.IsNullOrEmpty (path) || !File.Exists (path)) { + // return; + // } + + // try { + // log.LogDebugMessage ($"[{targetArch}] Deleting: {path}"); + // File.Delete (path); + // } catch (Exception ex) { + // log.LogWarning ($"[{targetArch}] Unable to delete source file '{path}'"); + // log.LogDebugMessage ($"[{targetArch}] {ex.ToString ()}"); + // } + //} static bool HasUnmanagedCallersOnlyAttribute (MethodDefinition method) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs index a801d0c5355..0f5e8d66fe3 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs @@ -111,6 +111,12 @@ public ConvertedMarshalMethodEntry (TypeDefinition declaringType, MethodDefiniti { ConvertedNativeCallback = convertedNativeCallback ?? throw new ArgumentNullException (nameof (convertedNativeCallback)); } + + public ConvertedMarshalMethodEntry (TypeDefinition declaringType, MethodDefinition nativeCallback, string jniTypeName, string jniName, string jniSignature) + : base (declaringType, nativeCallback, jniTypeName, jniName, jniSignature) + { + ConvertedNativeCallback = nativeCallback; + } } sealed class DynamicallyRegisteredMarshalMethodEntry : MethodEntry @@ -274,6 +280,7 @@ public bool Matches (MethodDefinition method) // we need to accept them as equivalent static readonly (string Source, string Replacement)[] equivalent_types = [ (Source: "System.Boolean", Replacement: "System.SByte"), + (Source: "System.Boolean", Replacement: "System.Byte"), (Source: "System.Char", Replacement: "System.UInt16"), ]; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs index 44e21810574..198056f6631 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -21,22 +20,22 @@ class MarshalMethodsCollection : JavaCallableMethodClassifier /// Assemblies that contain convertible marshal methods. These assemblies need to /// have the proper assembly references added to them. /// - public HashSet AssembliesWithMarshalMethods { get; } = []; + public virtual HashSet AssembliesWithMarshalMethods { get; } = []; /// /// Marshal methods that have already been rewritten as LLVM marshal methods. /// - public IDictionary> ConvertedMarshalMethods { get; } = new Dictionary> (StringComparer.Ordinal); + public virtual IDictionary> ConvertedMarshalMethods { get; } = new Dictionary> (StringComparer.Ordinal); /// /// Marshal methods that cannot be rewritten and must be registered dynamically. /// - public List DynamicallyRegisteredMarshalMethods { get; } = []; + public virtual List DynamicallyRegisteredMarshalMethods { get; } = []; /// /// Marshal methods that can be rewritten as LLVM marshal methods. /// - public IDictionary> MarshalMethods { get; } = new Dictionary> (StringComparer.Ordinal); + public virtual IDictionary> MarshalMethods { get; } = new Dictionary> (StringComparer.Ordinal); readonly MarshalMethodsClassifier classifier; readonly TaskLoggingHelper log; @@ -50,11 +49,18 @@ public MarshalMethodsCollection (MarshalMethodsClassifier classifier) resolver = classifier.Resolver; } + public MarshalMethodsCollection () + { + classifier = null!; + log = null!; + resolver = null!; + } + /// /// Adds MarshalMethodEntry for each method that won't be returned by the JavaInterop type scanner, mostly /// used for hand-written methods (e.g. Java.Interop.TypeManager+JavaTypeManager::n_Activate) /// - public void AddSpecialCaseMethods () + public virtual void AddSpecialCaseMethods () { AddTypeManagerSpecialCaseMethods (); } @@ -89,7 +95,26 @@ public static MarshalMethodsCollection FromAssemblies (AndroidTargetArch arch, L return collection; } - static void ScanTypeForMarshalMethods (TypeDefinition type, MarshalMethodsCollection collection, XAAssemblyResolver resolver, TypeDefinitionCache cache, TaskLoggingHelper log, MarshalMethodsClassifier classifier) + public static MarshalMethodsCollection FromAssembly (AndroidTargetArch arch, AssemblyDefinition assembly, IAssemblyResolver resolver, TaskLoggingHelper log) + { + var cache = new TypeDefinitionCache (); + var classifier = new MarshalMethodsClassifier (cache, resolver, log); + var collection = new MarshalMethodsCollection (classifier); + var scanner = new XAJavaTypeScanner (arch, log, cache); + + var javaTypes = scanner.GetJavaTypes (assembly); + + foreach (var type in javaTypes) { + if (type.IsInterface || JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (type, cache)) + continue; + + ScanTypeForMarshalMethods (type, collection, resolver, cache, log, classifier); + } + + return collection; + } + + static void ScanTypeForMarshalMethods (TypeDefinition type, MarshalMethodsCollection collection, IAssemblyResolver resolver, TypeDefinitionCache cache, TaskLoggingHelper log, MarshalMethodsClassifier classifier) { // Methods foreach (var minfo in type.Methods.Where (m => !m.IsConstructor)) { @@ -130,7 +155,7 @@ public override bool ShouldBeDynamicallyRegistered (TypeDefinition topType, Meth return method is DynamicallyRegisteredMarshalMethodEntry; } - public bool TypeHasDynamicallyRegisteredMethods (TypeDefinition type) + public virtual bool TypeHasDynamicallyRegisteredMethods (TypeDefinition type) { return typesWithDynamicallyRegisteredMarshalMethods.Contains (type); } @@ -146,18 +171,20 @@ MethodEntry AddMethod (TypeDefinition topType, MethodDefinition registeredMethod return dynamicMethod; } - if (marshalMethod is ConvertedMarshalMethodEntry convertedMethod) { - var key = convertedMethod.GetStoreMethodKey (classifier.TypeDefinitionCache); + //if (marshalMethod is ConvertedMarshalMethodEntry convertedMethod) { + // var key = convertedMethod.GetStoreMethodKey (classifier.TypeDefinitionCache); - if (!ConvertedMarshalMethods.TryGetValue (key, out var list)) { - list = new List (); - ConvertedMarshalMethods.Add (key, list); - } + // if (!ConvertedMarshalMethods.TryGetValue (key, out var list)) { + // list = new List (); + // ConvertedMarshalMethods.Add (key, list); + // } - list.Add (convertedMethod); + // list.Add (convertedMethod); - return convertedMethod; - } + // AssembliesWithMarshalMethods.Add (convertedMethod.NativeCallback.Module.Assembly); + + // return convertedMethod; + //} if (marshalMethod is MarshalMethodEntry marshalMethodEntry) { var key = marshalMethodEntry.GetStoreMethodKey (classifier.TypeDefinitionCache); @@ -273,7 +300,7 @@ void AddTypeManagerSpecialCaseMethods () } var entry = new MarshalMethodEntry (javaTypeManager, nActivate_mm, jniTypeName!, jniMethodName!, jniSignature!); // NRT- Guarded above - MarshalMethods.Add (".:!SpEcIaL:Java.Interop.TypeManager+JavaTypeManager::n_Activate_mm", new List { entry }); + MarshalMethods.Add (".:!SpEcIaL:Java.Interop.TypeManager+JavaTypeManager::n_Activate_mm", [entry]); [DoesNotReturn] void ThrowMissingMethod (string name) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs index 1420a25494d..3721071fc1c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs @@ -342,7 +342,7 @@ void Init () foreach (MarshalMethodInfo method in allMethods) { if (seenNativeSymbols.Contains (method.NativeSymbolName)) { - Log.LogDebugMessage ($"Removed MM duplicate '{method.NativeSymbolName}' (implemented: {method.Method.ImplementedMethod.FullName}; registered: {method.Method.RegisteredMethod.FullName}"); + Log.LogDebugMessage ($"Removed MM duplicate '{method.NativeSymbolName}' (implemented: {method.Method.ImplementedMethod?.FullName}; registered: {method.Method.RegisteredMethod?.FullName}"); continue; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsXmlFile.cs new file mode 100644 index 00000000000..b05b0836688 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsXmlFile.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Reflection; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +class MarshalMethodsXmlFile +{ + static readonly XmlWriterSettings settings = new XmlWriterSettings { + Indent = true, + NewLineOnAttributes = false, + OmitXmlDeclaration = true, + }; + + public static void Export (string filename, AndroidTargetArch arch, NativeCodeGenStateObject state, TaskLoggingHelper log) + { + if (state.MarshalMethods.Count == 0) { + WriteEmptyFile (filename, log); + return; + } + + using var sw = MemoryStreamPool.Shared.CreateStreamWriter (); + using (var xml = XmlWriter.Create (sw, settings)) + Export (xml, arch, state); + + sw.Flush (); + + Files.CopyIfStreamChanged (sw.BaseStream, filename); + + log.LogDebugMessage ($"Wrote '{filename}'"); + } + + public static KeyValuePair? Import (string filename) + { + // If the file has zero length, then the assembly doesn't have marshal methods. + // This check is much faster than loading and parsing an empty XML file. + var fi = new FileInfo (filename); + + if (fi.Length == 0) + return null; + + var xml = XDocument.Load (filename); + var root = xml.Root ?? throw new InvalidOperationException ($"Invalid XML file '{filename}'"); + + var arch = (AndroidTargetArch) Enum.Parse (typeof (AndroidTargetArch), root.GetRequiredAttribute ("arch")); + var state = new KeyValuePair (arch, new NativeCodeGenStateObject ()); + + var marshalMethods = root.Element ("marshal-methods"); + + if (marshalMethods is not null) + ImportMethods (marshalMethods, state); + + return state; + } + + static void ImportMethods (XElement element, KeyValuePair state) + { + foreach (var mm in element.Elements ("key")) { + var name = mm.GetRequiredAttribute ("name"); + var methods = new List (); + + foreach (var method in mm.Elements ("method")) { + + var isSpecial = method.GetAttributeOrDefault ("is-special", false); + var jniTypeName = method.GetRequiredAttribute ("jni-type-name"); + var jniMethodName = method.GetRequiredAttribute ("jni-method-name"); + var jniMethodSignature = method.GetRequiredAttribute ("jni-method-signature"); + var declaringType = ImportDeclaringType (method.Element ("declaring-type")); + var implementedMethod = ImportMethod (method.Element ("implemented-method")); + var nativeCallback = ImportMethod (method.Element ("native-callback")); + var registeredMethod = ImportMethodBase (method.Element ("registered-method")); + + var obj = new MarshalMethodEntryObject ( + declaringType: declaringType, + implementedMethod: implementedMethod, + isSpecial: isSpecial, + jniTypeName: jniTypeName, + jniMethodName: jniMethodName, + jniMethodSignature: jniMethodSignature, + nativeCallback: nativeCallback!, + registeredMethod: registeredMethod + ); + + methods.Add (obj); + } + state.Value.MarshalMethods.Add (name, methods); + } + } + + static MarshalMethodEntryMethodObject? ImportMethod (XElement element) + { + if (element is null) + return null; + + var name = element.GetRequiredAttribute ("name"); + var fullName = element.GetRequiredAttribute ("full-name"); + var metadataToken = uint.Parse (element.GetRequiredAttribute ("metadata-token")); + var assemblyIndex = element.GetUIntAttributeOrDefault ("assembly-index", null); + var classIndex = element.GetUIntAttributeOrDefault ("class-index", null); + var methodIndex = element.GetUIntAttributeOrDefault ("method-index", null); + var declaringType = ImportDeclaringType (element.Element ("declaring-type")); + var parameters = ImportParameters (element.Element ("parameters")); + + var method = new MarshalMethodEntryMethodObject ( + name: name, + fullName: fullName, + declaringType: declaringType, + metadataToken: metadataToken, + parameters: parameters + ); + + method.AssemblyIndex = assemblyIndex; + method.ClassIndex = classIndex; + method.MethodIndex = methodIndex; + + return method; + } + + static List ImportParameters (XElement element) + { + var parameters = new List (); + + if (element is null) + return parameters; + + foreach (var parameter in element.Elements ("parameter")) { + var name = parameter.GetAttributeOrDefault ("name", ""); + var parameterTypeName = parameter.GetRequiredAttribute ("parameter-type-name"); + var parameterObject = new MarshalMethodEntryMethodParameterObject (name, parameterTypeName); + parameters.Add (parameterObject); + } + + return parameters; + } + + static MarshalMethodEntryMethodBaseObject? ImportMethodBase (XElement element) + { + if (element is null) + return null; + + var fullName = element.GetRequiredAttribute ("full-name"); + + return new MarshalMethodEntryMethodBaseObject ( + fullName: fullName + ); + } + + static MarshalMethodEntryTypeObject ImportDeclaringType (XElement element) + { + var fullName = element.GetRequiredAttribute ("full-name"); + var metadataToken = uint.Parse (element.GetRequiredAttribute ("metadata-token")); + var module = element.Element ("module"); + + return new MarshalMethodEntryTypeObject ( + fullName: fullName, + metadataToken: metadataToken, + module: ImportModule (module) + ); + } + + static MarshalMethodEntryModuleObject ImportModule (XElement element) + { + var assembly = element.Element ("assembly"); + + return new MarshalMethodEntryModuleObject ( + assembly: ImportAssembly (assembly) + ); + } + + static MarshalMethodEntryAssemblyObject ImportAssembly (XElement element) + { + var fullName = element.GetRequiredAttribute ("full-name"); + var nameFullName = element.GetRequiredAttribute ("name-full-name"); + var mainModuleFileName = element.GetAttributeOrDefault ("main-module-file-name", ""); + var nameName = element.GetRequiredAttribute ("name-name"); + + return new MarshalMethodEntryAssemblyObject ( + fullName: fullName, + nameFullName: nameFullName, + mainModuleFileName: mainModuleFileName, + nameName: nameName + ); + } + + static void Export (XmlWriter xml, AndroidTargetArch arch, NativeCodeGenStateObject state) + { + xml.WriteStartElement ("api"); + xml.WriteAttributeString ("arch", arch.ToString ()); + + xml.WriteStartElement ("marshal-methods"); + + foreach (var kvp in state.MarshalMethods) + ExportMethods (xml, kvp); + + xml.WriteEndElement (); + xml.WriteEndElement (); + } + + static void ExportMethods (XmlWriter xml, KeyValuePair> kvp) + { + xml.WriteStartElement ("key"); + xml.WriteAttributeString ("name", kvp.Key); + + foreach (var method in kvp.Value) { + xml.WriteStartElement ("method"); + xml.WriteAttributeString ("is-special", method.IsSpecial.ToString ()); + xml.WriteAttributeString ("jni-type-name", method.JniTypeName); + xml.WriteAttributeString ("jni-method-name", method.JniMethodName); + xml.WriteAttributeString ("jni-method-signature", method.JniMethodSignature); + ExportDeclaringType (xml, method.DeclaringType); + ExportMethod (xml, "implemented-method", method.ImplementedMethod); + ExportMethod (xml, "native-callback", method.NativeCallback); + ExportRegisteredMethod (xml, method.RegisteredMethod); + xml.WriteEndElement (); + } + + xml.WriteEndElement (); + } + + static void ExportDeclaringType (XmlWriter xml, MarshalMethodEntryTypeObject type) + { + xml.WriteStartElement ("declaring-type"); + xml.WriteAttributeString ("full-name", type.FullName); + xml.WriteAttributeString ("metadata-token", type.MetadataToken.ToString ()); + ExportModule (xml, type.Module); + xml.WriteEndElement (); + } + + static void ExportModule (XmlWriter xml, MarshalMethodEntryModuleObject module) + { + xml.WriteStartElement ("module"); + ExportAssembly (xml, module.Assembly); + xml.WriteEndElement (); + } + + static void ExportAssembly (XmlWriter xml, MarshalMethodEntryAssemblyObject assembly) + { + xml.WriteStartElement ("assembly"); + xml.WriteAttributeString ("full-name", assembly.FullName); + xml.WriteAttributeString ("name-full-name", assembly.NameFullName); + xml.WriteAttributeString ("main-module-file-name", assembly.MainModuleFileName); + xml.WriteAttributeString ("name-name", assembly.NameName); + xml.WriteEndElement (); + } + + static void ExportRegisteredMethod (XmlWriter xml, MarshalMethodEntryMethodBaseObject? method) + { + if (method is null) + return; + + xml.WriteStartElement ("registered-method"); + xml.WriteAttributeString ("full-name", method.FullName); + xml.WriteEndElement (); + } + + static void ExportMethod (XmlWriter xml, string elementName, MarshalMethodEntryMethodObject? method) + { + if (method is null) + return; + + xml.WriteStartElement (elementName); + xml.WriteAttributeString ("name", method.Name); + xml.WriteAttributeString ("full-name", method.FullName); + xml.WriteAttributeString ("metadata-token", method.MetadataToken.ToString ()); + xml.WriteAttributeStringIfNotDefault ("assembly-index", method.AssemblyIndex.ToString ()); + xml.WriteAttributeStringIfNotDefault ("class-index", method.ClassIndex.ToString ()); + xml.WriteAttributeStringIfNotDefault ("method-index", method.MethodIndex.ToString ()); + + ExportDeclaringType (xml, method.DeclaringType); + + if (method.HasParameters) { + xml.WriteStartElement ("parameters"); + foreach (var parameter in method.Parameters) + ExportParameter (xml, parameter); + xml.WriteEndElement (); + } + + xml.WriteEndElement (); + } + + static void ExportParameter (XmlWriter xml, MarshalMethodEntryMethodParameterObject parameter) + { + xml.WriteStartElement ("parameter"); + xml.WriteAttributeString ("name", parameter.Name); + xml.WriteAttributeString ("parameter-type-name", parameter.ParameterTypeName); + xml.WriteEndElement (); + } + + /// + /// Given an assembly path, return the path to the ".mm.xml" file that should be next to it. + /// + public static string GetMarshalMethodsXmlFilePath (string assemblyPath) + => Path.ChangeExtension (assemblyPath, ".mm.xml"); + + public static void WriteEmptyFile (string destination, TaskLoggingHelper log) + { + log.LogDebugMessage ($"Writing empty file '{destination}'"); + + // We write a zero byte file to indicate the file couldn't have JLO types and wasn't scanned + File.Create (destination).Dispose (); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs index 6a0785dfd94..bf77c68e802 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs @@ -31,13 +31,13 @@ class NativeCodeGenState public List AllJavaTypes { get; } public List JavaTypesForJCW { get; } - public XAAssemblyResolver Resolver { get; } + public IAssemblyResolver Resolver { get; } 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, MarshalMethodsCollection? classifier) + public NativeCodeGenState (AndroidTargetArch arch, TypeDefinitionCache tdCache, IAssemblyResolver resolver, List allJavaTypes, List javaTypesForJCW, MarshalMethodsCollection? classifier) { TargetArch = arch; TypeCache = tdCache; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs index 5ed1a1da988..02d9c8a46b3 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/UtilityExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Xml; @@ -56,6 +57,20 @@ public static T GetAttributeOrDefault (this XElement xml, string name, T defa return (T) Convert.ChangeType (value, typeof (T)); } + [return: NotNullIfNotNull (nameof (defaultValue))] + public static uint? GetUIntAttributeOrDefault (this XElement xml, string name, uint? defaultValue) + { + var value = xml.Attribute (name)?.Value; + + if (string.IsNullOrWhiteSpace (value)) + return defaultValue; + + if (uint.TryParse (value, out var result)) + return result; + + return defaultValue; + } + public static string GetRequiredAttribute (this XElement xml, string name) { var value = xml.Attribute (name)?.Value; diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index cddbe0d63f2..9bae33d59a7 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -48,6 +48,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index b48988a65e7..7657044b73d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1471,10 +1471,13 @@ because xbuild doesn't support framework reference assemblies. Inputs="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)" Outputs="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')"> - - + -->