diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index 718f185b4ba..1212ee73b7e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -188,23 +188,23 @@ void Run (bool useMarshalMethods) var nativeCodeGenStates = new Dictionary (); bool generateJavaCode = true; NativeCodeGenState? templateCodeGenState = null; - var scanner = new PinvokeScanner (Log); + PinvokeScanner? pinvokeScanner = EnableNativeRuntimeLinking ? new PinvokeScanner (Log) : null; foreach (var kvp in allAssembliesPerArch) { AndroidTargetArch arch = kvp.Key; Dictionary archAssemblies = kvp.Value; (bool success, NativeCodeGenState? state) = GenerateJavaSourcesAndMaybeClassifyMarshalMethods (arch, archAssemblies, MaybeGetArchAssemblies (userAssembliesPerArch, arch), useMarshalMethods, generateJavaCode); - if (!success) { + if (!success || state == null) { return; } - if (EnableNativeRuntimeLinking) { - (success, List pinfos) = ScanForUsedPinvokes (scanner, arch, state.Resolver); + if (pinvokeScanner != null) { + (success, List pinfos) = ScanForUsedPinvokes (pinvokeScanner, arch, state.Resolver); if (!success) { return; } - BuildEngine4.RegisterTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (PinvokeScanner.PinvokesInfoRegisterTaskKey), pinfos, RegisteredTaskObjectLifetime.Build); + state.PinvokeInfos = pinfos; } if (generateJavaCode) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs index 0387f23ea38..78fe5f5f127 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs @@ -327,7 +327,7 @@ void AddEnvironment () } Dictionary? nativeCodeGenStates = null; - if (enableMarshalMethods) { + if (enableMarshalMethods || EnableNativeRuntimeLinking) { nativeCodeGenStates = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal> ( ProjectSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateRegisterTaskKey), RegisteredTaskObjectLifetime.Build @@ -370,8 +370,10 @@ void AddEnvironment () string targetAbi = abi.ToLowerInvariant (); string environmentBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"environment.{targetAbi}"); string marshalMethodsBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"marshal_methods.{targetAbi}"); + string? pinvokePreserveBaseAsmFilePath = EnableNativeRuntimeLinking ? Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}") : null; string environmentLlFilePath = $"{environmentBaseAsmFilePath}.ll"; string marshalMethodsLlFilePath = $"{marshalMethodsBaseAsmFilePath}.ll"; + string? pinvokePreserveLlFilePath = pinvokePreserveBaseAsmFilePath != null ? $"{pinvokePreserveBaseAsmFilePath}.ll" : null; AndroidTargetArch targetArch = GetAndroidTargetArchForAbi (abi); using var appConfigWriter = MemoryStreamPool.Shared.CreateStreamWriter (); @@ -402,10 +404,17 @@ void AddEnvironment () } if (EnableNativeRuntimeLinking) { - // var pinfoGen = new PreservePinvokesNativeAssemblyGenerator ( - // Log, - // targetArch, - + var pinvokePreserveGen = new PreservePinvokesNativeAssemblyGenerator (Log, EnsureCodeGenState (targetArch), MonoComponents); + LLVMIR.LlvmIrModule pinvokePreserveModule = pinvokePreserveGen.Construct (); + using var pinvokePreserveWriter = MemoryStreamPool.Shared.CreateStreamWriter (); + try { + pinvokePreserveGen.Generate (pinvokePreserveModule, targetArch, pinvokePreserveWriter, pinvokePreserveLlFilePath); + } catch { + throw; + } finally { + pinvokePreserveWriter.Flush (); + Files.CopyIfStreamChanged (pinvokePreserveWriter.BaseStream, pinvokePreserveLlFilePath); + } } LLVMIR.LlvmIrModule marshalMethodsModule = marshalMethodsAsmGen.Construct (); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs index d826e94110c..355d5c1e6c0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs @@ -16,6 +16,7 @@ public class PrepareAbiItems : AndroidTask const string CompressedAssembliesBase = "compressed_assemblies"; const string JniRemappingBase = "jni_remap"; const string MarshalMethodsBase = "marshal_methods"; + const string PinvokePreserveBase = "pinvoke_preserve"; public override string TaskPrefix => "PAI"; @@ -56,6 +57,8 @@ public override bool RunTask () baseName = JniRemappingBase; } else if (String.Compare ("marshal_methods", Mode, StringComparison.OrdinalIgnoreCase) == 0) { baseName = MarshalMethodsBase; + } else if (String.Compare ("runtime_linking", Mode, StringComparison.OrdinalIgnoreCase) == 0) { + baseName = PinvokePreserveBase; } else { Log.LogError ($"Unknown mode: {Mode}"); return false; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs index 96d95391a49..4113a604a69 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs @@ -30,6 +30,13 @@ class NativeCodeGenState /// public List AllJavaTypes { get; } + /// + /// Contains information about p/invokes used by the managed assemblies included in the + /// application. Will be **null** unless native runtime linking at application build time + /// is enabled. + /// + public List? PinvokeInfos { get; set; } + public List JavaTypesForJCW { get; } public XAAssemblyResolver Resolver { get; } public TypeDefinitionCache TypeCache { get; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/PinvokeScanner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/PinvokeScanner.cs index 8c82be7b2c2..ae510e5c38e 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/PinvokeScanner.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/PinvokeScanner.cs @@ -14,8 +14,6 @@ namespace Xamarin.Android.Tasks; class PinvokeScanner { - public const string PinvokesInfoRegisterTaskKey = ".:!PreservePinvokesTaskKey!:."; - public sealed class PinvokeEntryInfo { public readonly string LibraryName; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs index dfaa5af0116..1fb05402e42 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs @@ -5,6 +5,7 @@ using System.Text; using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Xamarin.Android.Tasks.LLVMIR; @@ -17,19 +18,107 @@ namespace Xamarin.Android.Tasks; class PreservePinvokesNativeAssemblyGenerator : LlvmIrComposer { - readonly TaskLoggingHelper log; - readonly AndroidTargetArch targetArch; - readonly ICollection pinfos; + // Maps a component name after ridding it of the `lib` prefix and the extension to a "canonical" + // name of a library, as used in `[DllImport]` attributes. + readonly Dictionary libraryNameMap = new (StringComparer.Ordinal) { + { "xa-java-interop", "java-interop" }, + { "mono-android.release-static", String.Empty }, + { "mono-android.release", String.Empty }, + }; - public PreservePinvokesNativeAssemblyGenerator (TaskLoggingHelper log, AndroidTargetArch targetArch, ICollection pinfos) + readonly NativeCodeGenState state; + readonly ITaskItem[] monoComponents; + + public PreservePinvokesNativeAssemblyGenerator (TaskLoggingHelper log, NativeCodeGenState codeGenState, ITaskItem[] monoComponents) : base (log) { - this.log = log; - this.targetArch = targetArch; - this.pinfos = pinfos; + if (codeGenState.PinvokeInfos == null) { + throw new InvalidOperationException ($"Internal error: {nameof (codeGenState)} `{nameof (codeGenState.PinvokeInfos)}` property is `null`"); + } + + this.state = codeGenState; + this.monoComponents = monoComponents; } protected override void Construct (LlvmIrModule module) { + Log.LogDebugMessage ("Constructing p/invoke preserve code"); + List pinvokeInfos = state.PinvokeInfos!; + if (pinvokeInfos.Count == 0) { + // This is a very unlikely scenario, but we will work just fine. The module that this generator produces will merely result + // in an empty (but valid) .ll file and an "empty" object file to link into the shared library. + return; + } + + Log.LogDebugMessage (" Looking for enabled native components"); + var componentNames = new List (); + var nativeComponents = new NativeRuntimeComponents (monoComponents); + foreach (NativeRuntimeComponents.Archive archiveItem in nativeComponents.KnownArchives) { + if (!archiveItem.Include) { + continue; + } + + Log.LogDebugMessage ($" {archiveItem.Name}"); + componentNames.Add (archiveItem.Name); + } + + if (componentNames.Count == 0) { + Log.LogDebugMessage ("No native framework components are included in the build, not scanning for p/invoke usage"); + return; + } + + Log.LogDebugMessage (" Checking discovered p/invokes against the list of components"); + foreach (PinvokeScanner.PinvokeEntryInfo pinfo in pinvokeInfos) { + Log.LogDebugMessage ($" p/invoke: {pinfo.EntryName} in {pinfo.LibraryName}"); + if (MustPreserve (pinfo, componentNames)) { + Log.LogDebugMessage (" must be preserved"); + } else { + Log.LogDebugMessage (" no need to preserve"); + } + } + } + + // Returns `true` for all p/invokes that we know are part of our set of components, otherwise returns `false`. + // Returning `false` merely means that the p/invoke isn't in any of BCL or our code and therefore we shouldn't + // care. It doesn't mean the p/invoke will be removed in any way. + bool MustPreserve (PinvokeScanner.PinvokeEntryInfo pinfo, List components) + { + if (String.Compare ("xa-internal-api", pinfo.LibraryName, StringComparison.Ordinal) == 0) { + return true; + } + + foreach (string component in components) { + // The most common pattern for the BCL - file name without extension + string componentName = Path.GetFileNameWithoutExtension (component); + if (Matches (pinfo.LibraryName, componentName)) { + return true; + } + + // If it starts with `lib`, drop the prefix + if (componentName.StartsWith ("lib", StringComparison.Ordinal)) { + if (Matches (pinfo.LibraryName, componentName.Substring (3))) { + return true; + } + } + + // Might require mapping of component name to a canonical one + if (libraryNameMap.TryGetValue (componentName, out string? mappedComponentName) && !String.IsNullOrEmpty (mappedComponentName)) { + if (Matches (pinfo.LibraryName, mappedComponentName)) { + return true; + } + } + + // Try full file name, as the last resort + if (Matches (pinfo.LibraryName, Path.GetFileName (component))) { + return true; + } + } + + return false; + + bool Matches (string libraryName, string componentName) + { + return String.Compare (libraryName, componentName, StringComparison.Ordinal) == 0; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 9e42cd07b95..d0a180fd670 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1598,6 +1598,15 @@ because xbuild doesn't support framework reference assemblies. Mode="marshal_methods"> + + + @@ -1740,6 +1749,8 @@ because xbuild doesn't support framework reference assemblies. + + @@ -1964,6 +1975,9 @@ because xbuild doesn't support framework reference assemblies. <_NativeAssemblyTarget Include="@(_AndroidRemapAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')"> %(_AndroidRemapAssemblySource.abi) + <_NativeAssemblyTarget Include="@(_RuntimeLinkingAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')"> + %(_RuntimeLinkingAssemblySource.abi) + @@ -1983,10 +1997,10 @@ because xbuild doesn't support framework reference assemblies.