From 656beee71675a093433ed56c8f0069ee92f7c5ec Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 21 Jan 2026 11:43:43 +0100 Subject: [PATCH 01/13] [tools] Move RegistrarMode to its own file. --- tools/common/Application.cs | 9 --------- tools/common/RegistrarMode.cs | 13 +++++++++++++ tools/dotnet-linker/dotnet-linker.csproj | 3 +++ tools/mtouch/mtouch.csproj | 3 +++ 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 tools/common/RegistrarMode.cs diff --git a/tools/common/Application.cs b/tools/common/Application.cs index 5405b8eb0244..9d2115d4ea3e 100644 --- a/tools/common/Application.cs +++ b/tools/common/Application.cs @@ -59,15 +59,6 @@ public enum RegistrarOptions { Trace = 1, } - public enum RegistrarMode { - Default, - Dynamic, - PartialStatic, - Static, - ManagedStatic, - TrimmableStatic, - } - public partial class Application : IToolLog { public Cache? Cache; public string AppDirectory = "."; diff --git a/tools/common/RegistrarMode.cs b/tools/common/RegistrarMode.cs new file mode 100644 index 000000000000..9876623b0c9b --- /dev/null +++ b/tools/common/RegistrarMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Xamarin.Bundler; + +public enum RegistrarMode { + Default, + Dynamic, + PartialStatic, + Static, + ManagedStatic, + TrimmableStatic, +} diff --git a/tools/dotnet-linker/dotnet-linker.csproj b/tools/dotnet-linker/dotnet-linker.csproj index c3ff1f126681..456e1905a013 100644 --- a/tools/dotnet-linker/dotnet-linker.csproj +++ b/tools/dotnet-linker/dotnet-linker.csproj @@ -110,6 +110,9 @@ external/tools/common/CSToObjCMap.cs + + tools\common\RegistrarMode.cs + external/tools/common/Rewriter.cs diff --git a/tools/mtouch/mtouch.csproj b/tools/mtouch/mtouch.csproj index d9ba0a1ea76e..88f535184fca 100644 --- a/tools/mtouch/mtouch.csproj +++ b/tools/mtouch/mtouch.csproj @@ -39,6 +39,9 @@ external/tools/common/cache.cs + + tools\common\RegistrarMode.cs + external/tools/linker/MonoTouch.Tuner/Extensions.cs From 08b67346ab4329a6986dd37eeb87343d75f957ed Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 21 Jan 2026 18:12:34 +0100 Subject: [PATCH 02/13] [tools] Move NormalizedStringComparer into its own file. --- tools/common/Assembly.cs | 27 ------------------- tools/common/NormalizedStringComparer.cs | 33 ++++++++++++++++++++++++ tools/dotnet-linker/dotnet-linker.csproj | 3 +++ tools/mtouch/mtouch.csproj | 3 +++ 4 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 tools/common/NormalizedStringComparer.cs diff --git a/tools/common/Assembly.cs b/tools/common/Assembly.cs index 6ad8d9141958..8e100d6d9609 100644 --- a/tools/common/Assembly.cs +++ b/tools/common/Assembly.cs @@ -600,33 +600,6 @@ public bool IsAOTCompiled { } } - public sealed class NormalizedStringComparer : IEqualityComparer { - public static readonly NormalizedStringComparer OrdinalIgnoreCase = new NormalizedStringComparer (StringComparer.OrdinalIgnoreCase); - - StringComparer comparer; - - public NormalizedStringComparer (StringComparer comparer) - { - this.comparer = comparer; - } - - public bool Equals (string? x, string? y) - { - // From what I gather it doesn't matter which normalization form - // is used, but I chose Form D because HFS normalizes to Form D. - if (x is not null) - x = x.Normalize (System.Text.NormalizationForm.FormD); - if (y is not null) - y = y.Normalize (System.Text.NormalizationForm.FormD); - return comparer.Equals (x, y); - } - - public int GetHashCode (string? obj) - { - return comparer.GetHashCode (obj?.Normalize (System.Text.NormalizationForm.FormD) ?? ""); - } - } - public class AssemblyCollection : IEnumerable { Dictionary HashedAssemblies = new Dictionary (NormalizedStringComparer.OrdinalIgnoreCase); diff --git a/tools/common/NormalizedStringComparer.cs b/tools/common/NormalizedStringComparer.cs new file mode 100644 index 000000000000..c6e1d884ffa5 --- /dev/null +++ b/tools/common/NormalizedStringComparer.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Xamarin.Bundler; + +#nullable enable + +public sealed class NormalizedStringComparer : IEqualityComparer { + public static readonly NormalizedStringComparer OrdinalIgnoreCase = new NormalizedStringComparer (StringComparer.OrdinalIgnoreCase); + + StringComparer comparer; + + public NormalizedStringComparer (StringComparer comparer) + { + this.comparer = comparer; + } + + public bool Equals (string? x, string? y) + { + // From what I gather it doesn't matter which normalization form + // is used, but I chose Form D because HFS normalizes to Form D. + if (x is not null) + x = x.Normalize (System.Text.NormalizationForm.FormD); + if (y is not null) + y = y.Normalize (System.Text.NormalizationForm.FormD); + return comparer.Equals (x, y); + } + + public int GetHashCode (string obj) + { + return comparer.GetHashCode (obj.Normalize (System.Text.NormalizationForm.FormD)); + } +} diff --git a/tools/dotnet-linker/dotnet-linker.csproj b/tools/dotnet-linker/dotnet-linker.csproj index 456e1905a013..8c9fec13a15f 100644 --- a/tools/dotnet-linker/dotnet-linker.csproj +++ b/tools/dotnet-linker/dotnet-linker.csproj @@ -77,6 +77,9 @@ external/tools/common/LinkMode.cs + + tools\common\NormalizedStringComparer.cs + external/tools/common/SdkVersions.cs diff --git a/tools/mtouch/mtouch.csproj b/tools/mtouch/mtouch.csproj index 88f535184fca..e0ebeca5ccf1 100644 --- a/tools/mtouch/mtouch.csproj +++ b/tools/mtouch/mtouch.csproj @@ -33,6 +33,9 @@ external/tools/common/AssemblyBuildTarget.cs + + tools\common\NormalizedStringComparer.cs + external/tools/common/PListExtensions.cs From a29a1ac57bf629de803745bba06b0e2a2ee45f2e Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 21 Jan 2026 20:06:54 +0100 Subject: [PATCH 03/13] [tools] Simplify a little bit of code. --- tools/common/Application.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/common/Application.cs b/tools/common/Application.cs index 9d2115d4ea3e..03f0d58bc48c 100644 --- a/tools/common/Application.cs +++ b/tools/common/Application.cs @@ -948,7 +948,6 @@ public void GetAotArguments (string filename, Abi abi, string outputDir, string bool enable_debug_symbols = app.PackageManagedDebugSymbols; bool interp = app.IsInterpreted (Assembly.GetIdentity (filename)) && !(isDedupAssembly.HasValue && isDedupAssembly.Value); bool interp_full = !interp && app.UseInterpreter; - bool is32bit = (abi & Abi.Arch32Mask) > 0; string arch = abi.AsArchString (); processArguments.Add ("--debug"); From 542e99d976f7c8cf417b6e6fbe89c46046bf9911 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 12:49:18 +0200 Subject: [PATCH 04/13] [tests] Clean up Configure.cs a bit. --- tests/Makefile | 8 +-- tests/common/Configuration.cs | 55 +++++++++---------- tests/dotnet/UnitTests/AssetsTest.cs | 4 +- .../AssemblySetup.cs | 2 +- .../TaskTests/ACToolTaskTest.cs | 2 +- .../TaskTests/GetAvailableDevicesTest.cs | 2 +- .../TaskTests/IBToolTaskTests.cs | 2 +- .../TaskTests/MergeAppBundleTaskTest.cs | 2 +- .../TestHelpers/TestBase.cs | 2 +- tests/mtouch/MLaunchTool.cs | 2 +- 10 files changed, 36 insertions(+), 45 deletions(-) diff --git a/tests/Makefile b/tests/Makefile index 6a0802b2f561..34087555249a 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -52,9 +52,7 @@ test.config: Makefile $(TOP)/Make.config $(TOP)/eng/Version.Details.xml @echo "JENKINS_RESULTS_DIRECTORY=$(abspath $(JENKINS_RESULTS_DIRECTORY))" >> $@ @echo "XCODE_DEVELOPER_ROOT=$(XCODE_DEVELOPER_ROOT)" >> $@ @echo "DOTNET=$(DOTNET)" >> $@ - @echo "IOS_SDK_VERSION=$(IOS_SDK_VERSION)" >> $@ - @echo "TVOS_SDK_VERSION=$(TVOS_SDK_VERSION)" >> $@ - @echo "MACOS_SDK_VERSION=$(MACOS_SDK_VERSION)" >> $@ + @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),$(platform)_SDK_VERSION=$($(platform)_SDK_VERSION)\\n)" | sed 's/^ //' >> $@ @echo "DOTNET_BCL_DIR=$(DOTNET_BCL_DIR)" >> $@ @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS_NO_ARCH='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS_NO_ARCH)'\\n)" | sed 's/^ //' >> $@ @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS)'\\n)" | sed 's/^ //' >> $@ @@ -83,9 +81,7 @@ test-system.config: Makefile $(TOP)/Make.config $(TOP)/eng/Version.Details.xml @rm -f $@ @echo "JENKINS_RESULTS_DIRECTORY=$(abspath $(JENKINS_RESULTS_DIRECTORY))" >> $@ @echo "DOTNET=$(DOTNET)" >> $@ - @echo "IOS_SDK_VERSION=$(IOS_SDK_VERSION)" >> $@ - @echo "TVOS_SDK_VERSION=$(TVOS_SDK_VERSION)" >> $@ - @echo "MACOS_SDK_VERSION=$(MACOS_SDK_VERSION)" >> $@ + @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),$(platform)_SDK_VERSION=$($(platform)_SDK_VERSION)\\n)" | sed 's/^ //' >> $@ @echo "DOTNET_TFM=$(DOTNET_TFM)" >> $@ @echo "DOTNET_BCL_DIR=$(DOTNET_BCL_DIR)" >> $@ @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS)'\\n)" | sed 's/^ //' >> $@ diff --git a/tests/common/Configuration.cs b/tests/common/Configuration.cs index 8154d4e9b100..1735ac8df815 100644 --- a/tests/common/Configuration.cs +++ b/tests/common/Configuration.cs @@ -10,25 +10,17 @@ namespace Xamarin.Tests { static partial class Configuration { - public const string XI_ProductName = "MonoTouch"; - public const string XM_ProductName = "Xamarin.Mac"; - public static string DotNetBclDir = ""; public static string DotNetCscCommand = ""; public static string DotNetExecutable; public static string DotNetTfm; - public static string? mt_src_root; - public static string sdk_version; + public static string ios_sdk_version; public static string tvos_sdk_version; public static string macos_sdk_version; - public static string xcode_root; + public static string maccatalyst_sdk_version; + static string xcode_root; public static string? XcodeVersionString; - public static string xcode83_root; - public static string xcode94_root; -#if MONOMAC - public static string mac_xcode_root; -#endif - public static Dictionary make_config = new Dictionary (); + static Dictionary make_config = new Dictionary (); public static bool include_ios; public static bool include_mac; @@ -286,12 +278,11 @@ static Configuration () { ParseConfigFiles (); - sdk_version = GetVariable ("IOS_SDK_VERSION", "8.0"); + ios_sdk_version = GetVariable ("IOS_SDK_VERSION", "8.0"); tvos_sdk_version = GetVariable ("TVOS_SDK_VERSION", "9.0"); macos_sdk_version = GetVariable ("MACOS_SDK_VERSION", "10.12"); + maccatalyst_sdk_version = GetVariable ("MACCATALYST_SDK_VERSION", "14.0"); xcode_root = GetVariable ("XCODE_DEVELOPER_ROOT", "/Applications/Xcode.app/Contents/Developer"); - xcode83_root = GetVariable ("XCODE83_DEVELOPER_ROOT", "/Applications/Xcode83.app/Contents/Developer"); - xcode94_root = GetVariable ("XCODE94_DEVELOPER_ROOT", "/Applications/Xcode94.app/Contents/Developer"); include_ios = !string.IsNullOrEmpty (GetVariable ("INCLUDE_IOS", "")); include_mac = !string.IsNullOrEmpty (GetVariable ("INCLUDE_MAC", "")); include_tvos = !string.IsNullOrEmpty (GetVariable ("INCLUDE_TVOS", "")); @@ -305,16 +296,10 @@ static Configuration () DOTNET_DIR = GetVariable ("DOTNET_DIR", ""); XcodeVersionString = GetVariable ("XCODE_VERSION", GetXcodeVersion (xcode_root)); -#if MONOMAC - mac_xcode_root = xcode_root; -#endif Console.WriteLine ("Test configuration:"); - Console.WriteLine (" SDK_VERSION={0}", sdk_version); Console.WriteLine (" XCODE_ROOT={0}", xcode_root); -#if MONOMAC - Console.WriteLine (" MAC_XCODE_ROOT={0}", mac_xcode_root); -#endif + Console.WriteLine (" XCODE_VERSION={0}", XcodeVersionString); Console.WriteLine (" INCLUDE_IOS={0}", include_ios); Console.WriteLine (" INCLUDE_MAC={0}", include_mac); Console.WriteLine (" INCLUDE_TVOS={0}", include_tvos); @@ -371,13 +356,7 @@ public static bool TryGetRootPath ([NotNullWhen (true)] out string? rootPath) } } - public static string SourceRoot { - get { - if (mt_src_root is null) - mt_src_root = RootPath; - return mt_src_root; - } - } + public static string SourceRoot => RootPath; public static string TestProjectsDirectory { get { @@ -694,7 +673,7 @@ public static void SetBuildVariables (ApplePlatform platform, [NotNullIfNotNull if (environment is null) environment = new Dictionary (); - environment ["DEVELOPER_DIR"] = Path.GetDirectoryName (Path.GetDirectoryName (xcode_root)!)!; + environment ["DEVELOPER_DIR"] = Path.GetDirectoryName (Path.GetDirectoryName (XcodeLocation)!)!; // This is set by `dotnet test` and can cause building legacy projects to fail to build with: // Microsoft.NET.Build.Extensions.ConflictResolution.targets(30,5): @@ -732,6 +711,22 @@ public static string GetTestLibraryDirectory (ApplePlatform platform, bool? simu return Path.Combine (SourceRoot, "tests", "test-libraries", ".libs", dir); } + public static string GetSdkVersion (ApplePlatform platform) + { + switch (platform) { + case ApplePlatform.iOS: + return ios_sdk_version; + case ApplePlatform.MacOSX: + return macos_sdk_version; + case ApplePlatform.TVOS: + return tvos_sdk_version; + case ApplePlatform.MacCatalyst: + return maccatalyst_sdk_version; + default: + throw new NotImplementedException ($"Unknown platform: {platform}"); + } + } + // This implementation of Touch is to update a timestamp (not to make sure a certain file exists). public static void Touch (string file) { diff --git a/tests/dotnet/UnitTests/AssetsTest.cs b/tests/dotnet/UnitTests/AssetsTest.cs index 3466457299e9..c980bbf977a3 100644 --- a/tests/dotnet/UnitTests/AssetsTest.cs +++ b/tests/dotnet/UnitTests/AssetsTest.cs @@ -136,9 +136,9 @@ public static string GetFullSdkVersion (ApplePlatform platform, string runtimeId switch (platform) { case ApplePlatform.iOS: if (runtimeIdentifiers.Contains ("simulator")) { - return $"iphonesimulator{Configuration.sdk_version}"; + return $"iphonesimulator{Configuration.ios_sdk_version}"; } else { - return $"iphoneos{Configuration.sdk_version}"; + return $"iphoneos{Configuration.ios_sdk_version}"; } case ApplePlatform.TVOS: if (runtimeIdentifiers.Contains ("simulator")) { diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs index 5059aab86236..28575a0412a9 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs @@ -17,7 +17,7 @@ public void AssemblyInitialization () const string msbuild_exe_path = "/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/msbuild/15.0/bin/MSBuild.dll"; if (is_in_vsmac) { var env = new Dictionary { - { "DEVELOPER_DIR", Path.GetDirectoryName (Path.GetDirectoryName (Configuration.xcode_root)) ?? string.Empty }, + { "DEVELOPER_DIR", Path.GetDirectoryName (Path.GetDirectoryName (Configuration.XcodeLocation)) ?? string.Empty }, { "MSBUILD_EXE_PATH", msbuild_exe_path }, }; diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs index 218b3cf4fa9e..958fcdb63f8b 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs @@ -60,7 +60,7 @@ ACTool CreateACToolTask (ApplePlatform platform, string projectDir, out string i task.MinimumOSVersion = Xamarin.SdkVersions.GetMinVersion (platform).ToString (); task.OutputPath = Path.Combine (intermediateOutputPath, "OutputPath"); task.ProjectDir = projectDir; - task.SdkDevPath = Configuration.xcode_root; + task.SdkDevPath = Configuration.XcodeLocation; task.SdkPlatform = sdkPlatform; task.SdkVersion = version.ToString (); task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs index c202f5e615aa..dc6ab813dde8 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs @@ -31,7 +31,7 @@ GetAvailableDevicesTaskWrapper CreateTask (ApplePlatform platform, string simctl SimCtlJson = simctlJson, DeviceCtlJson = devicectlJson, }; - task.SdkDevPath = Configuration.xcode_root; + task.SdkDevPath = Configuration.XcodeLocation; task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString (); if (!string.IsNullOrEmpty (appManifest)) { diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs index 2b66ae4a5f93..bc6fdbf5f6a0 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs @@ -44,7 +44,7 @@ IBTool CreateIBToolTask (ApplePlatform framework, string projectDir, string inte task.MinimumOSVersion = PDictionary.OpenFile (Path.Combine (projectDir, "Info.plist")).GetMinimumOSVersion (); task.ResourcePrefix = "Resources"; task.ProjectDir = projectDir; - task.SdkDevPath = Configuration.xcode_root; + task.SdkDevPath = Configuration.XcodeLocation; task.SdkPlatform = platform; task.SdkVersion = version.ToString (); task.TargetFrameworkMoniker = TargetFramework.DotNet_iOS_String; diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs index 309d9aa597f9..afe1a7eaa7b6 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs @@ -60,7 +60,7 @@ MergeAppBundles CreateTask (string outputBundle, params string [] inputBundles) var task = CreateTask (); task.InputAppBundles = inputItems.ToArray (); task.OutputAppBundle = outputBundle; - task.SdkDevPath = Configuration.xcode_root; + task.SdkDevPath = Configuration.XcodeLocation; return task; } diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs index 05b391b10fbf..8f0e1ba36e6d 100644 --- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs @@ -54,7 +54,7 @@ public virtual void Setup () var t = new T (); t.BuildEngine = Engine; if (t is XamarinTask xt) - xt.SdkDevPath = Configuration.xcode_root; + xt.SdkDevPath = Configuration.XcodeLocation; return t; } diff --git a/tests/mtouch/MLaunchTool.cs b/tests/mtouch/MLaunchTool.cs index 5185e8f6cfd2..789d29a1dd4c 100644 --- a/tests/mtouch/MLaunchTool.cs +++ b/tests/mtouch/MLaunchTool.cs @@ -97,7 +97,7 @@ IList BuildArguments () } if (!string.IsNullOrEmpty (platformName) && !string.IsNullOrEmpty (simType)) { - var device = string.Format (":v2:runtime=com.apple.CoreSimulator.SimRuntime.{0}-{1},devicetype=com.apple.CoreSimulator.SimDeviceType.{2}", platformName, Configuration.sdk_version.Replace ('.', '-'), simType); + var device = string.Format (":v2:runtime=com.apple.CoreSimulator.SimRuntime.{0}-{1},devicetype=com.apple.CoreSimulator.SimDeviceType.{2}", platformName, Configuration.ios_sdk_version.Replace ('.', '-'), simType); sb.Add ($"--device:{device}"); } From 2d9925c267622b01dec379d793e749a43bedef9d Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 11:26:27 +0200 Subject: [PATCH 05/13] [xharness] Run the new assembly processing tests. --- tests/xharness/Harness.cs | 7 +++++++ tests/xharness/TestLabel.cs | 3 ++- tools/devops/automation/templates/common/configure.yml | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/xharness/Harness.cs b/tests/xharness/Harness.cs index 33cf6b5ffb93..a8490b532f08 100644 --- a/tests/xharness/Harness.cs +++ b/tests/xharness/Harness.cs @@ -355,6 +355,13 @@ void PopulateUnitTestProjects () Timeout = (TimeSpan?) TimeSpan.FromMinutes (10), Filter = "", }, + new { + Label = TestLabel.AssemblyProcessing, + ProjectPath = Path.GetFullPath (Path.Combine (HarnessConfiguration.RootDirectory, "assembly-preparer", "assembly-preparer-tests.csproj")), + Name = "Assembly processing tests", + Timeout = (TimeSpan?) TimeSpan.FromMinutes (10), + Filter = "", + }, new { Label = TestLabel.Generator, ProjectPath = Path.GetFullPath (Path.Combine (HarnessConfiguration.RootDirectory, "bgen", "bgen-tests.csproj")), diff --git a/tests/xharness/TestLabel.cs b/tests/xharness/TestLabel.cs index 9085a1eb815f..0bc5e9d78910 100644 --- a/tests/xharness/TestLabel.cs +++ b/tests/xharness/TestLabel.cs @@ -44,7 +44,8 @@ public enum TestLabel : Int64 { Generator = 1 << 11, [Label ("interdependent-binding-projects")] InterdependentBindingProjects = 1 << 12, - // 1 << 13 is unused + [Label ("assembly-processing")] + AssemblyProcessing = 1 << 13, [Label ("introspection")] Introspection = 1 << 14, [Label ("linker")] diff --git a/tools/devops/automation/templates/common/configure.yml b/tools/devops/automation/templates/common/configure.yml index a7dd354368e7..377d7d4cde15 100644 --- a/tools/devops/automation/templates/common/configure.yml +++ b/tools/devops/automation/templates/common/configure.yml @@ -31,6 +31,11 @@ parameters: testPrefix: 'windows_integration', testStage: 'windows_integration' }, + { + label: assembly-processing, + splitByPlatforms: false, + testPrefix: 'simulator_tests', + }, { label: cecil, splitByPlatforms: false, From 9cac08cdb2b3ac9582182d67a4fca686138dd3b0 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 19:17:18 +0200 Subject: [PATCH 06/13] [tools] Remove some dead code --- tools/dotnet-linker/LinkerConfiguration.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tools/dotnet-linker/LinkerConfiguration.cs b/tools/dotnet-linker/LinkerConfiguration.cs index e9375514f12c..c0152c375d5b 100644 --- a/tools/dotnet-linker/LinkerConfiguration.cs +++ b/tools/dotnet-linker/LinkerConfiguration.cs @@ -126,7 +126,6 @@ public static LinkerConfiguration GetInstance (LinkContext context) Application = new Application (this); CompilerFlags = new CompilerFlags (Application); - var use_llvm = false; var lines = File.ReadAllLines (linker_file); var significantLines = new List (); // This is the input the cache uses to verify if the cache is still valid for (var i = 0; i < lines.Length; i++) { @@ -439,10 +438,6 @@ public static LinkerConfiguration GetInstance (LinkContext context) ErrorHelper.Show (Application, messages); } - if (use_llvm) { - Abi |= Abi.LLVM; - } - Application.CreateCache (significantLines.ToArray ()); if (Application.Cache is not null) Application.Cache.SetLocation (Application, CacheDirectory); From 00274d7d2baab947ccfbc7be58ca956cc6b66df5 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 19:08:12 +0200 Subject: [PATCH 07/13] [tools] Extract OptimizeGeneratedCode into its own class. Extract the static optimization methods from OptimizeGeneratedCodeHandler into a new standalone OptimizeGeneratedCode class. The handler class now only contains the linker-specific scaffolding and delegates to the new class. This makes the optimization logic reusable without depending on the linker's ExceptionalMarkHandler base class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dotnet-linker/BackingFieldDelayHandler.cs | 6 +- .../OptimizeGeneratedCodeStep.cs | 4 +- tools/dotnet-linker/dotnet-linker.csproj | 3 + tools/linker/CoreOptimizeGeneratedCode.cs | 1207 +--------------- tools/linker/OptimizeGeneratedCode.cs | 1214 +++++++++++++++++ tools/linker/RegistrarRemovalTrackingStep.cs | 2 +- 6 files changed, 1225 insertions(+), 1211 deletions(-) create mode 100644 tools/linker/OptimizeGeneratedCode.cs diff --git a/tools/dotnet-linker/BackingFieldDelayHandler.cs b/tools/dotnet-linker/BackingFieldDelayHandler.cs index 9a61786f7c0d..970f5d4893d6 100644 --- a/tools/dotnet-linker/BackingFieldDelayHandler.cs +++ b/tools/dotnet-linker/BackingFieldDelayHandler.cs @@ -84,9 +84,9 @@ public static void ReapplyDisposedFields (DerivedLinkContext context, string ope var store_field = ins; var load_null = ins.Previous; var load_this = ins.Previous.Previous; - if (OptimizeGeneratedCodeHandler.ValidateInstruction (app, method, store_field, operation, Code.Stfld) && - OptimizeGeneratedCodeHandler.ValidateInstruction (app, method, load_null, operation, Code.Ldnull) && - OptimizeGeneratedCodeHandler.ValidateInstruction (app, method, load_this, operation, Code.Ldarg_0)) { + if (OptimizeGeneratedCode.ValidateInstruction (app, method, store_field, operation, Code.Stfld) && + OptimizeGeneratedCode.ValidateInstruction (app, method, load_null, operation, Code.Ldnull) && + OptimizeGeneratedCode.ValidateInstruction (app, method, load_this, operation, Code.Ldarg_0)) { store_field.OpCode = OpCodes.Nop; load_null.OpCode = OpCodes.Nop; load_this.OpCode = OpCodes.Nop; diff --git a/tools/dotnet-linker/OptimizeGeneratedCodeStep.cs b/tools/dotnet-linker/OptimizeGeneratedCodeStep.cs index d785f16c94af..a7b57042609f 100644 --- a/tools/dotnet-linker/OptimizeGeneratedCodeStep.cs +++ b/tools/dotnet-linker/OptimizeGeneratedCodeStep.cs @@ -14,7 +14,7 @@ public class OptimizeGeneratedCodeStep : AssemblyModifierStep { protected override bool IsActiveFor (AssemblyDefinition assembly) { - return OptimizeGeneratedCodeHandler.IsActiveFor (assembly, Configuration.Profile, DerivedLinkContext.Annotations); + return OptimizeGeneratedCode.IsActiveFor (assembly, Configuration.Profile, DerivedLinkContext.Annotations); } protected override bool ProcessType (TypeDefinition type) @@ -32,7 +32,7 @@ protected override bool ProcessMethod (MethodDefinition method) Device = App.IsDeviceBuild, }; } - return OptimizeGeneratedCodeHandler.OptimizeMethod (data, method); + return OptimizeGeneratedCode.OptimizeMethod (data, method); } } } diff --git a/tools/dotnet-linker/dotnet-linker.csproj b/tools/dotnet-linker/dotnet-linker.csproj index 8c9fec13a15f..9a5d2966bc7b 100644 --- a/tools/dotnet-linker/dotnet-linker.csproj +++ b/tools/dotnet-linker/dotnet-linker.csproj @@ -140,6 +140,9 @@ external/tools/linker/CoreTypeMapStep.cs + + external/tools/linker/OptimizeGeneratedCode.cs + external/src/ObjCRuntime/Registrar.cs diff --git a/tools/linker/CoreOptimizeGeneratedCode.cs b/tools/linker/CoreOptimizeGeneratedCode.cs index c33d231aeb66..de6fabbec099 100644 --- a/tools/linker/CoreOptimizeGeneratedCode.cs +++ b/tools/linker/CoreOptimizeGeneratedCode.cs @@ -1,18 +1,8 @@ // Copyright 2012-2013, 2016 Xamarin Inc. All rights reserved. -using System; -using System.Collections.Generic; -using System.Linq; - -using ObjCRuntime; using Mono.Cecil; -using Mono.Cecil.Cil; using Mono.Linker; using Mono.Linker.Steps; -using Mono.Tuner; -using MonoTouch.Tuner; - -using Xamarin.Bundler; #nullable enable @@ -31,504 +21,7 @@ public override void Initialize (LinkContext context, MarkContext markContext) bool IsActiveFor (AssemblyDefinition assembly) { - return IsActiveFor (assembly, Profile, Annotations); - } - - public static bool IsActiveFor (AssemblyDefinition assembly, Profile profile, AnnotationStore annotations) - { - // Unless an assembly is or references our platform assembly, then it won't have anything we need to register - if (!profile.IsOrReferencesProductAssembly (assembly)) - return false; - - // We only care about assemblies that are being linked. - if (annotations.GetAction (assembly) != AssemblyAction.Link) - return false; - - return true; - } - - // [GeneratedCode] is not enough - e.g. it's used for anonymous delegates even if the - // code itself is not tool/compiler generated - static protected bool IsExport (ICustomAttributeProvider provider) - { - return provider.HasCustomAttribute (Namespaces.Foundation, "ExportAttribute"); - } - - // less risky to nop-ify if branches are pointing to this instruction - static protected void Nop (Instruction ins) - { - // Leave 'leave' instructions in place, they might be required for the resulting IL to be correct/verifiable. - switch (ins.OpCode.Code) { - case Code.Leave: - case Code.Leave_S: - return; - } - ins.OpCode = OpCodes.Nop; - ins.Operand = null; - } - - internal static bool ValidateInstruction (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, string operation, Code expected) - { - return ValidateInstruction (data.App, caller, ins, operation, expected); - } - - internal static bool ValidateInstruction (IToolLog log, MethodDefinition caller, Instruction ins, string operation, Code expected) - { - if (ins.OpCode.Code != expected) { - log.Log (1, "Could not {0} in {1} at offset {2}, expected {3} got {4}", operation, caller, ins.Offset, expected, ins); - return false; - } - - return true; - } - - internal static bool ValidateInstruction (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, string operation, params Code [] expected) - { - foreach (var code in expected) { - if (ins.OpCode.Code == code) - return true; - } - - data.App.Log (1, "Could not {0} in {1} at offset {2}, expected any of [{3}] got {4}", operation, caller, ins.Offset, string.Join (", ", expected), ins); - return false; - } - - static int? GetConstantValue (OptimizeGeneratedCodeData data, Instruction? ins) - { - if (ins is null) - return null; - - switch (ins.OpCode.Code) { - case Code.Ldc_I4_0: - return 0; - case Code.Ldc_I4_1: - return 1; - case Code.Ldc_I4_2: - return 2; - case Code.Ldc_I4_3: - return 3; - case Code.Ldc_I4_4: - return 4; - case Code.Ldc_I4_5: - return 5; - case Code.Ldc_I4_6: - return 6; - case Code.Ldc_I4_7: - return 7; - case Code.Ldc_I4_8: - return 8; - case Code.Ldc_I4: - case Code.Ldc_I4_S: - if (ins.Operand is int) - return (int) ins.Operand; - if (ins.Operand is sbyte) - return (sbyte) ins.Operand; - return null; -#if DEBUG - case Code.Isinst: // We might be able to calculate a constant value here in the future - case Code.Ldloc: - case Code.Ldloca: - case Code.Ldloc_0: - case Code.Ldloc_1: - case Code.Ldloc_2: - case Code.Ldloc_3: - case Code.Ldloc_S: - case Code.Ldloca_S: - case Code.Ldarg: - case Code.Ldarg_0: - case Code.Ldarg_1: - case Code.Ldarg_2: - case Code.Ldarg_3: - case Code.Ldarg_S: - case Code.Ldarga: - case Code.Call: - case Code.Calli: - case Code.Callvirt: - case Code.Box: - case Code.Ldsfld: - case Code.Dup: // You might think we could get the constant of the previous instruction, but this instruction might be the target of a branch, in which case the question becomes: which instruction was the previous instruction? And that's not a question easily answered without a much more thorough analysis of the code. - case Code.Ldlen: - case Code.Ldind_U1: - case Code.Ldind_U2: - case Code.Ldind_U4: - case Code.Ldind_Ref: - case Code.Conv_I: - case Code.Conv_I1: - case Code.Conv_I2: - case Code.Conv_I4: - case Code.Conv_U: - case Code.Sizeof: - case Code.Ldfld: - case Code.Ldflda: - return null; // just to not hit the CWL below -#endif - default: -#if DEBUG - data.App.Log (9, "Unknown conditional instruction: {0}", ins); -#endif - return null; - } - } - - static bool MarkInstructions (OptimizeGeneratedCodeData data, MethodDefinition method, Mono.Collections.Generic.Collection instructions, bool [] reachable, int start, int end) - { - if (reachable [start]) - return true; // We've already marked this section of code - - for (int i = start; i < end; i++) { - reachable [i] = true; - - var ins = instructions [i]; - switch (ins.OpCode.FlowControl) { - case FlowControl.Branch: - // Unconditional branch, we continue marking from the instruction that we branch to. - var br_target = (Instruction) ins.Operand; - return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (br_target), instructions.Count); - case FlowControl.Cond_Branch: - // Conditional instruction, we need to check if we can calculate a constant value for the condition - var cond_target = ins.Operand as Instruction; - bool? branch = null; // did we get a constant value for the condition, and if so, did we branch or not? - var cond_instruction_count = 0; // The number of instructions that compose the condition - - if (ins.OpCode.Code == Code.Switch) { - // Treat all branches of the switch statement as reachable. - // FIXME: calculate the potential constant branch (currently there are no optimizable methods where the switch condition is constant, so this is not needed for now) - var targets = ins.Operand as Instruction []; - if (targets is null) { - data.App.Log (4, "Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand); - return false; - } - foreach (var target in targets) { - // not constant, continue marking both this code sequence and the branched sequence - if (!MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (target), end)) - return false; - } - return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (ins.Next), end); - } - - if (cond_target is null) { - data.App.Log (4, "Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand); - return false; - } - - switch (ins.OpCode.Code) { - case Code.Brtrue: - case Code.Brtrue_S: { - var v = GetConstantValue (data, ins.Previous); - if (v.HasValue) - branch = v.Value != 0; - cond_instruction_count = 2; - break; - } - case Code.Brfalse: - case Code.Brfalse_S: { - var v = GetConstantValue (data, ins.Previous); - if (v.HasValue) - branch = v.Value == 0; - cond_instruction_count = 2; - break; - } - case Code.Beq: - case Code.Beq_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value == x2.Value; - cond_instruction_count = 3; - break; - } - case Code.Bne_Un: - case Code.Bne_Un_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value != x2.Value; - cond_instruction_count = 3; - break; - } - case Code.Ble: - case Code.Ble_S: - case Code.Ble_Un: - case Code.Ble_Un_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value <= x2.Value; - cond_instruction_count = 3; - break; - } - case Code.Blt: - case Code.Blt_S: - case Code.Blt_Un: - case Code.Blt_Un_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value < x2.Value; - cond_instruction_count = 3; - break; - } - case Code.Bge: - case Code.Bge_S: - case Code.Bge_Un: - case Code.Bge_Un_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value >= x2.Value; - cond_instruction_count = 3; - break; - } - case Code.Bgt: - case Code.Bgt_S: - case Code.Bgt_Un: - case Code.Bgt_Un_S: { - var x1 = GetConstantValue (data, ins.Previous?.Previous); - var x2 = GetConstantValue (data, ins.Previous); - if (x1.HasValue && x2.HasValue) - branch = x1.Value > x2.Value; - cond_instruction_count = 3; - break; - } - default: - data.App.Log ("Can't optimize {0} because of unknown branch instruction: {1}", method, ins); - break; - } - - if (branch.HasValue) { - // Make sure nothing else in the method branches into the middle of our supposedly constant condition, - // bypassing our constant calculation. Note that it's not a bad to branch to the _first_ instruction in - // the sequence (thus the +2 here), just into the middle of it. - if (AnyBranchTo (data, instructions, instructions [i - cond_instruction_count + 2], ins)) - branch = null; - } - - if (!branch.HasValue) { - // not constant, continue marking both this code sequence and the branched sequence - if (!MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (cond_target), end)) - return false; - } else { - // we can remove the branch (and the code that loads the condition), so we mark those instructions as dead. - for (int a = 0; a < cond_instruction_count; a++) - reachable [i - a] = false; - - // Now continue marking according to whether we branched or not - if (branch.Value) { - // branch always taken - return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (cond_target), end); - } else { - // branch never taken - // continue looping - } - } - break; - case FlowControl.Call: - case FlowControl.Next: - // Nothing special, continue marking - break; - case FlowControl.Return: - case FlowControl.Throw: - // Control flow returns here, so stop marking - return true; - case FlowControl.Break: - case FlowControl.Meta: - case FlowControl.Phi: - default: - data.App.Log (4, "Can't optimize {0} because of unknown flow control for: {1}", method, ins); - return false; - } - } - - return true; - } - - // Check if there are any branches in the instructions that branch to anywhere between 'first' and 'last' instructions (both inclusive). - static bool AnyBranchTo (OptimizeGeneratedCodeData data, Mono.Collections.Generic.Collection instructions, Instruction first, Instruction last) - { - if (first.Offset > last.Offset) { - data.App.Log ($"Broken assumption: {first} is after {last}"); - return true; // This is the safe thing to do, since it will prevent inlining - } - - for (int i = 0; i < instructions.Count; i++) { - var ins = instructions [i]; - switch (ins.OpCode.FlowControl) { - case FlowControl.Branch: - case FlowControl.Cond_Branch: - var target = ins.Operand as Instruction; - if (target is not null && target.Offset >= first.Offset && target.Offset <= last.Offset) - return true; - break; - } - } - - return false; - } - - static bool EliminateDeadCode (OptimizeGeneratedCodeData data, MethodDefinition caller) - { - var modified = false; - if (data.Optimizations.DeadCodeElimination != true) - return modified; - - var instructions = caller.Body.Instructions; - var reachable = new bool [instructions.Count]; - - // We walk the instructions in the method, starting with the first instruction, - // marking all reachable instructions. Any non-reachable instructions at the end - // can be removed. - - if (!MarkInstructions (data, caller, instructions, reachable, 0, instructions.Count)) - return modified; - - // Handle exception handlers specially, they do not follow normal code flow. - bool []? reachableExceptionHandlers = null; - if (caller.Body.HasExceptionHandlers) { - reachableExceptionHandlers = new bool [caller.Body.ExceptionHandlers.Count]; - for (var e = 0; e < reachableExceptionHandlers.Length; e++) { - var eh = caller.Body.ExceptionHandlers [e]; - - // First check if the protected region is reachable - var startI = instructions.IndexOf (eh.TryStart); - var endI = instructions.IndexOf (eh.TryEnd); - for (int i = startI; i < endI; i++) { - if (reachable [i]) { - reachableExceptionHandlers [e] = true; - break; - } - } - // The protected code isn't reachable, none of the handlers will be executed - if (!reachableExceptionHandlers [e]) - continue; - - switch (eh.HandlerType) { - case ExceptionHandlerType.Catch: - // We don't need catch handlers the reachable instructions are all nops - var allNops = true; - for (int i = startI; i < endI; i++) { - if (instructions [i].OpCode.Code != Code.Nop) { - allNops = false; - break; - } - } - if (!allNops) { - if (!MarkInstructions (data, caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd))) - return modified; - } - break; - case ExceptionHandlerType.Finally: - // finally clauses are always executed, even if the protected region is empty - if (!MarkInstructions (data, caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd))) - return modified; - break; - case ExceptionHandlerType.Fault: - case ExceptionHandlerType.Filter: - // FIXME: and until fixed, exit gracefully without doing anything - data.App.Log (4, "Unhandled exception handler: {0}, skipping dead code elimination for {1}", eh.HandlerType, caller); - return modified; - } - } - } - - if (Array.IndexOf (reachable, false) == -1) - return modified; // entire method is reachable - - // Kill branch instructions when there are only dead instructions between the branch instruction and the target of the branch - for (int i = 0; i < instructions.Count; i++) { - if (!reachable [i]) - continue; - - var ins = instructions [i]; - if (ins.OpCode.Code != Code.Br && ins.OpCode.Code != Code.Br_S) - continue; - var target = ins.Operand as Instruction; - if (target is null) - continue; - if (target.Offset < ins.Offset) - continue; // backwards branch, keep those - - var start = i + 1; - var end = instructions.IndexOf (target); - var any_reachable = false; - for (int k = start; k < end; k++) { - if (reachable [k]) { - any_reachable = true; - break; - } - } - if (any_reachable) - continue; - - // The branch instruction just branches over unreachable instructions, so it can be considered unreachable too. - reachable [i] = false; - } - - // Check if there are unreachable instructions at the end. - var last_reachable = Array.LastIndexOf (reachable, true); - if (last_reachable < reachable.Length - 1) { - // There are unreachable instructions at the end. - // We must verify that there are no branches into these instructions. - // In theory there shouldn't be any (if there are branches into these instructions, - // they're reachable), but let's still verify just in case. - var last_reachable_offset = instructions [last_reachable].Offset; - for (int i = 0; i < last_reachable; i++) { - if (!reachable [i]) - continue; // Unreachable instructions don't branch anywhere, because they'll be removed. - var ins = instructions [i]; - switch (ins.OpCode.FlowControl) { - case FlowControl.Break: - case FlowControl.Cond_Branch: - var target = (Instruction) ins.Operand; - if (target.Offset > last_reachable_offset) { - data.App.Log (4, "Can't optimize {0} because of branching beyond last instruction alive: {1}", caller, ins); - return modified; - } - break; - } - } - } -#if false - data.App.Log ($"{caller.FullName}:"); - for (int i = 0; i < reachable.Length; i++) { - data.App.Log ($"{(reachable [i] ? " " : "- ")} {instructions [i]}"); - if (!reachable [i]) - Nop (instructions [i]); - } - data.App.Log (""); -#endif - - // Exterminate, exterminate, exterminate - for (int i = 0; i < reachable.Length; i++) { - if (!reachable [i]) { - Nop (instructions [i]); - modified = true; - } - } - - // Remove exception handlers - if (reachableExceptionHandlers is not null) { - for (int i = reachableExceptionHandlers.Length - 1; i >= 0; i--) { - if (reachableExceptionHandlers [i]) - continue; - caller.Body.ExceptionHandlers.RemoveAt (i); - modified = true; - } - } - - // Remove unreachable instructions (nops) at the end, because the last instruction can only be ret/throw/backwards branch. - for (int i = last_reachable + 1; i < reachable.Length; i++) { - instructions.RemoveAt (last_reachable + 1); - modified = true; - } - - return modified; - } - - static bool GetIsExtensionType (TypeDefinition type) - { - // if 'type' inherits from NSObject inside an assembly that has [GeneratedCode] - // or for static types used for optional members (using extensions methods), they can be optimized too - return type.IsSealed && type.IsAbstract && type.Name.EndsWith ("_Extensions", StringComparison.Ordinal); + return OptimizeGeneratedCode.IsActiveFor (assembly, Profile, Annotations); } protected override void Process (MethodDefinition method) @@ -544,703 +37,7 @@ protected override void Process (MethodDefinition method) Device = LinkContext.App.IsDeviceBuild, }; } - OptimizeMethod (data, method); - } - - public static bool OptimizeMethod (OptimizeGeneratedCodeData data, MethodDefinition method) - { - var modified = false; - - if (!method.HasBody) - return modified; - - if (method.IsBindingImplOptimizableCode (data.LinkContext)) { - // We optimize all methods that have the [BindingImpl (BindingImplAttributes.Optimizable)] attribute. - } else if (method.IsGeneratedCode (data.LinkContext) && ( - GetIsExtensionType (method.DeclaringType) - || IsExport (method))) { - // We optimize methods that have the [GeneratedCodeAttribute] and is either an extension type or an exported method - } else { - // but it would be too risky to apply on user-generated code - return modified; - } - - if (data.Optimizations.InlineIsARM64CallingConvention == true && data.InlineIsArm64CallingConvention.HasValue && method.Name == "GetIsARM64CallingConvention" && method.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) { - // Rewrite to return the constant value - var instr = method.Body.Instructions; - instr.Clear (); - instr.Add (Instruction.Create (data.InlineIsArm64CallingConvention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); - instr.Add (Instruction.Create (OpCodes.Ret)); - return true; // nothing else to do here. - } - - if (ProcessProtocolInterfaceStaticConstructor (data, method)) - return true; - - var instructions = method.Body.Instructions; - for (int i = 0; i < instructions.Count; i++) { - var ins = instructions [i]; - switch (ins.OpCode.Code) { - case Code.Newobj: - case Code.Call: - modified |= ProcessCalls (data, method, ins, out var instructionsAddedOrRemoved); - i += instructionsAddedOrRemoved; - break; - case Code.Ldsfld: - modified |= ProcessLoadStaticField (data, method, ins); - break; - } - } - - modified |= EliminateDeadCode (data, method); - return modified; - } - - // Returns the number of instructions added (or removed) in the 'instructionsAddedOrRemoved' parameter. - // Returns true if any modifications were done. - static bool ProcessCalls (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) - { - var modified = false; - instructionsAddedOrRemoved = 0; - var mr = ins.Operand as MethodReference; - switch (mr?.Name) { - case "EnsureUIThread": - modified |= ProcessEnsureUIThread (data, caller, ins); - break; - case "get_IsDirectBinding": - modified |= ProcessIsDirectBinding (data, caller, ins); - break; - case "SetupBlock": - case "SetupBlockUnsafe": - modified |= ProcessSetupBlock (data, caller, ins, out instructionsAddedOrRemoved); - break; - case ".ctor": - if (!mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) - break; - return ProcessBlockLiteralConstructor (data, caller, ins, out instructionsAddedOrRemoved); - } - - return modified; - } - - static bool ProcessLoadStaticField (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - var modified = false; - var fr = ins.Operand as FieldReference; - switch (fr?.Name) { - case "IsARM64CallingConvention": - modified |= ProcessIsARM64CallingConvention (data, caller, ins); - break; - case "Arch": - // https://app.asana.com/0/77259014252/77812690163 - modified |= ProcessRuntimeArch (data, caller, ins); - break; - } - return modified; - } - - static bool ProcessEnsureUIThread (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - if (data.Optimizations.RemoveUIThreadChecks != true) - return false; - - // Verify we're checking the right get_EnsureUIThread call - var declaringTypeNamespace = data.LinkContext.App.Platform == Utils.ApplePlatform.MacOSX ? Namespaces.AppKit : Namespaces.UIKit; - var declaringTypeName = data.LinkContext.App.Platform == Utils.ApplePlatform.MacOSX ? "NSApplication" : "UIApplication"; - var mr = ins.Operand as MethodReference; - if (mr is null || !mr.DeclaringType.Is (declaringTypeNamespace, declaringTypeName)) - return false; - - // Verify a few assumptions before doing anything - const string operation = "remove calls to [NS|UI]Application::EnsureUIThread"; - if (!ValidateInstruction (data, caller, ins, operation, Code.Call)) - return false; - - // This is simple: just remove the call - Nop (ins); // call void UIKit.UIApplication::EnsureUIThread() - return true; - } - - static bool? IsDirectBindingConstant (OptimizeGeneratedCodeData data, TypeDefinition type) - { - return type.IsNSObject (data.LinkContext) ? type.GetIsDirectBindingConstant (data.LinkContext) : null; - } - - static bool ProcessIsDirectBinding (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - const string operation = "inline IsDirectBinding"; - - if (data.Optimizations.InlineIsDirectBinding != true) - return false; - - bool? isdirectbinding_constant = IsDirectBindingConstant (data, caller.DeclaringType); - - // If we don't know the constant isdirectbinding value, then we can't inline anything - if (!isdirectbinding_constant.HasValue) - return false; - - // Verify we're checking the right get_IsDirectBinding call - var mr = ins.Operand as MethodReference; - if (mr is null || !mr.DeclaringType.Is (Namespaces.Foundation, "NSObject")) - return false; - - // Verify a few assumptions before doing anything - if (!ValidateInstruction (data, caller, ins.Previous, operation, Code.Ldarg_0)) - return false; - - if (!ValidateInstruction (data, caller, ins, operation, Code.Call)) - return false; - - // Clearing the branch succeeded, so clear the condition too - // ldarg.0 - Nop (ins.Previous); - // call System.Boolean Foundation.NSObject::get_IsDirectBinding() - ins.OpCode = isdirectbinding_constant.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0; - ins.Operand = null; - return true; - } - - static bool ProcessSetupBlock (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) - { - instructionsAddedOrRemoved = 0; - if (data.Optimizations.OptimizeBlockLiteralSetupBlock != true) - return false; - - // This will optimize calls to SetupBlock and SetupBlockUnsafe by calculating the signature for the block - // (which both SetupBlock and SetupBlockUnsafe do), and then rewrite the code to call SetupBlockImpl instead - // (which takes the block signature as an argument instead of calculating it). This is required to - // remove the dynamic registrar, because calculating the block signature is done in the dynamic registrar. - // - // This code is a mirror of the code in BlockLiteral.SetupBlock (to calculate the block signature). - var mr = ins.Operand as MethodReference; - if (mr is null || !mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) - return false; - - if (caller.DeclaringType.Is ("ObjCRuntime", "BlockLiteral")) { - switch (caller.Name) { - case "GetBlockForDelegate": - case "CreateBlockForDelegate": - // These methods contain a non-optimizable call to SetupBlock, and this way we don't show any warnings to users about things they can't do anything about. - return false; - } - } - - string? signature = null; - try { - // We need to figure out the type of the first argument to the call to SetupBlock[Impl]. - // - // Example sequence: - // - // ldsfld ObjCRuntime.Trampolines/DJSContextExceptionHandler ObjCRuntime.Trampolines/SDJSContextExceptionHandler::Handler - // ldarg.1 - // call System.Void ObjCRuntime.BlockLiteral::SetupBlockUnsafe(System.Delegate, System.Delegate) - // - - // Locating the instruction that loads the first argument can be complicated, so we simplify by making a few assumptions: - // 1. The instruction immediately before the call instruction (which would load the last argument) is a Push1/Pop0 instruction. - // This avoids running into trouble when the instruction does something else (it could be a any other instruction, which would throw off the next calculations) - // 2. We have a approved list of instructions we know how to calculate the type for, and which we use on the second to last instruction before the call instruction - - // First verify the Push1/Pop0 behavior in point 1. - var prev = ins.Previous; - while (prev.OpCode.Code == Code.Nop) - prev = prev.Previous; // Skip any nops. - if (prev.OpCode.StackBehaviourPush != StackBehaviour.Push1) { - //todo: localize mmp error 2106 - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev)); - return false; - } else if (prev.OpCode.StackBehaviourPop != StackBehaviour.Pop0) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev)); - return false; - } - - var loadTrampolineInstruction = prev.Previous; - while (loadTrampolineInstruction.OpCode.Code == Code.Nop) - loadTrampolineInstruction = loadTrampolineInstruction.Previous; // Skip any nops. - - // Then find the type of the previous instruction (the first argument to SetupBlock[Unsafe]) - var trampolineDelegateType = GetPushedType (caller, loadTrampolineInstruction); - if (trampolineDelegateType is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_A, caller, ins.Offset, mr.Name, loadTrampolineInstruction)); - return false; - } - - if (trampolineDelegateType.Is ("System", "Delegate") || trampolineDelegateType.Is ("System", "MulticastDelegate")) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_B, caller, trampolineDelegateType.FullName, mr.Name)); - return false; - } - - if (!data.LinkContext.App.StaticRegistrar.TryComputeBlockSignature (caller, trampolineDelegateType, out var exception, out signature)) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, exception, caller, ins, Errors.MM2106_D, caller, ins.Offset, exception.Message)); - return false; - - } - } catch (Exception e) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, e, caller, ins, Errors.MM2106_D, caller, ins.Offset, e.Message)); - return false; - } - - // We got the information we need: rewrite the IL. - var instructions = caller.Body.Instructions; - var index = instructions.IndexOf (ins); - // Inject the extra arguments - instructions.Insert (index, Instruction.Create (OpCodes.Ldstr, signature)); - instructions.Insert (index, Instruction.Create (mr.Name == "SetupBlockUnsafe" ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1)); - // Change the call to call SetupBlockImpl instead - ins.Operand = GetBlockSetupImpl (data, caller, ins); - - //Driver.Log (4, "Optimized call to BlockLiteral.SetupBlock in {0} at offset {1} with delegate type {2} and signature {3}", caller, ins.Offset, delegateType.FullName, signature); - instructionsAddedOrRemoved = 2; - return true; - } - - internal static bool IsBlockLiteralCtor_Type_String (MethodDefinition md) - { - if (!md.HasParameters) - return false; - - if (md.Parameters.Count != 4) - return false; - - if (!(md.Parameters [0].ParameterType is PointerType pt) || !pt.ElementType.Is ("System", "Void")) - return false; - - if (!md.Parameters [1].ParameterType.Is ("System", "Object")) - return false; - - if (!md.Parameters [2].ParameterType.Is ("System", "Type")) - return false; - - if (!md.Parameters [3].ParameterType.Is ("System", "String")) - return false; - - return true; - } - - static bool ProcessBlockLiteralConstructor (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) - { - instructionsAddedOrRemoved = 0; - - if (data.Optimizations.OptimizeBlockLiteralSetupBlock != true) - return false; - - // This will optimize calls to this BlockLiteral constructor: - // (void* ptr, object context, Type trampolineType, string trampolineMethod) - // by calculating the signature for the block using the last two arguments, - // and then rewrite the code to call this constructor overload instead: - // (void* ptr, object context, string signature) - // This is required to remove the dynamic registrar, because calculating the block signature - // is done in the dynamic registrar. - // - // This code is a mirror of the code in BlockLiteral.SetupBlock (to calculate the block signature). - var mr = ins.Operand as MethodReference; - if (mr is null || !mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) - return false; - - var md = mr.Resolve (); - if (md is null || !IsBlockLiteralCtor_Type_String (md)) - return false; - - string? signature = null; - Instruction? sequenceStart; - try { - // We need to figure out the last argument to the call to the ctor - // - // Example sequence: - // - // ldarg.0 - // ldarg.1 - // ldtoken ... - // call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle) - // ldstr ... - // newobj BlockLiteral (void*, System.Object, System.Type, System.String) - // - - // Verify 'ldstr ...' - var loadString = GetPreviousSkippingNops (ins); - if (loadString.OpCode != OpCodes.Ldstr) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadString)); - return false; - } - - // Verify 'call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle)' - var callGetTypeFromHandle = GetPreviousSkippingNops (loadString); - if (callGetTypeFromHandle.OpCode != OpCodes.Call || !(callGetTypeFromHandle.Operand is MethodReference methodOperand) || methodOperand.Name != "GetTypeFromHandle" || !methodOperand.DeclaringType.Is ("System", "Type")) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, callGetTypeFromHandle)); - return false; - } - - // Verify 'ldtoken ...' - var loadType = GetPreviousSkippingNops (callGetTypeFromHandle); - if (loadType.OpCode != OpCodes.Ldtoken) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadType)); - return false; - } - - // Then find the type of the previous instruction - var trampolineContainerTypeReference = loadType.Operand as TypeReference; - if (trampolineContainerTypeReference is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadType.Operand)); - return false; - } - - var trampolineContainerType = trampolineContainerTypeReference.Resolve (); - if (trampolineContainerType is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, trampolineContainerTypeReference)); - return false; - } - - // Find the trampoline method - var trampolineMethodName = (string) loadString.Operand; - var trampolineMethods = trampolineContainerType.Methods.Where (v => v.Name == trampolineMethodName).ToArray (); - if (!trampolineMethods.Any ()) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_E1 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because no method named '{3}' was found in the type '{4}'. */, caller, ins.Offset, mr.Name, trampolineMethodName, trampolineContainerType.FullName)); - return false; - } else if (trampolineMethods.Count () > 1) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_E2 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because more than one method named '{3}' was found in the type '{4}'. */, caller, ins.Offset, mr.Name, trampolineMethodName, trampolineContainerType.FullName)); - return false; - } - var trampolineMethod = trampolineMethods [0]; - if (!trampolineMethod.HasParameters || trampolineMethod.Parameters.Count < 1) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_F /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the method '{3}' must have at least one parameter. */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName)); - return false; - } - - // Check that the method's first parameter is either IntPtr, void* or BlockLiteral* - var firstParameterType = trampolineMethod.Parameters [0].ParameterType; - if (firstParameterType.Is ("System", "IntPtr")) { - // ok - } else if (firstParameterType is PointerType ptrType) { - var ptrTargetType = ptrType.ElementType; - if (!(ptrTargetType.Is ("System", "Void") || ptrTargetType.Is ("ObjCRuntime", "BlockLiteral"))) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_G /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the first parameter in the method '{3}' isn't 'System.IntPtr', 'void*' or 'ObjCRuntime.BlockLiteral*' (it's '{4}') */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName, firstParameterType.FullName)); - return false; - } - // ok - } else { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_G /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the first parameter in the method '{3}' isn't 'System.IntPtr', 'void*' or 'ObjCRuntime.BlockLiteral*' (it's '{4}') */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName, firstParameterType.FullName)); - return false; - } - - // Check that the method has [UnmanagedCallersOnly] - if (!trampolineMethod.HasCustomAttributes || !trampolineMethod.CustomAttributes.Any (v => v.AttributeType.Is ("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute"))) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_H /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the method '{3}' does not have an [UnmanagedCallersOnly] attribute. */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName)); - return false; - } - - var userDelegateType = data.LinkContext.App.StaticRegistrar.GetUserDelegateType (trampolineMethod); - MethodReference? userMethod = null; - var blockSignature = true; - if (userDelegateType is not null) { - userMethod = data.LinkContext.App.StaticRegistrar.GetDelegateInvoke (userDelegateType); - } else { - userMethod = trampolineMethod; - } - - if (userMethod is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_D, caller, ins.Offset, "Could not find delegate invoke method")); - return false; - } - - // Calculate the block signature. - var parameters = new TypeReference [userMethod.Parameters.Count]; - for (int p = 0; p < parameters.Length; p++) - parameters [p] = userMethod.Parameters [p].ParameterType; - signature = data.LinkContext.App.StaticRegistrar.ComputeSignature (userMethod.DeclaringType, false, userMethod.ReturnType, parameters, userMethod.Resolve (), isBlockSignature: blockSignature); - - sequenceStart = loadType; - } catch (Exception e) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, e, caller, ins, Errors.MM2106_D, caller, ins.Offset, e.Message)); - return false; - } - - // We got the information we need: rewrite the IL. - var instructions = caller.Body.Instructions; - var index = instructions.IndexOf (sequenceStart); - int instructionDiff = 0; - while (instructions [index] != ins) { - instructions.RemoveAt (index); - instructionDiff--; - } - // Inject the extra arguments - instructions.Insert (index, Instruction.Create (OpCodes.Ldstr, signature)); - instructionDiff++; - // Change the call to call the ctor with the string signature parameter instead - ins.Operand = GetBlockLiteralConstructor (data, caller, ins); - - data.App.Log (4, "Optimized call to BlockLiteral..ctor in {0} at offset {1} with signature {2}", caller, ins.Offset, signature); - instructionsAddedOrRemoved = instructionDiff; - return true; - } - - static Instruction GetPreviousSkippingNops (Instruction ins) - { - do { - ins = ins.Previous; - } while (ins.OpCode == OpCodes.Nop); - return ins; - } - - static Instruction? SkipNops (Instruction? ins) - { - if (ins is null) - return null; - - while (ins.OpCode == OpCodes.Nop) { - if (ins.Next is null) - return null; - ins = ins.Next; - } - return ins; - } - - static bool ProcessIsARM64CallingConvention (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - const string operation = "inline Runtime.IsARM64CallingConvention"; - - if (data.Optimizations.InlineIsARM64CallingConvention != true) - return false; - - if (!data.InlineIsArm64CallingConvention.HasValue) - return false; - - // Verify we're checking the right IsARM64CallingConvention field - var fr = ins.Operand as FieldReference; - if (fr is null || !fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) - return false; - - if (!ValidateInstruction (data, caller, ins, operation, Code.Ldsfld)) - return false; - - // We're fine, inline the Runtime.IsARM64CallingConvention value - ins.OpCode = data.InlineIsArm64CallingConvention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0; - ins.Operand = null; - - return true; - } - - static bool ProcessRuntimeArch (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - const string operation = "inline Runtime.Arch"; - - if (data.Optimizations.InlineRuntimeArch != true) - return false; - - // Verify we're checking the right Arch field - var fr = ins.Operand as FieldReference; - if (fr is null || !fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) - return false; - - // Verify a few assumptions before doing anything - if (!ValidateInstruction (data, caller, ins, operation, Code.Ldsfld)) - return false; - - // We're fine, inline the Runtime.Arch condition - // The enum values are Runtime.DEVICE = 0 and Runtime.SIMULATOR = 1, - ins.OpCode = data.Device ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1; - ins.Operand = null; - return true; - } - - // Returns the type of the value pushed on the stack by the given instruction. - // Returns null for unknown instructions, or for instructions that don't push anything on the stack. - static TypeReference? GetPushedType (MethodDefinition method, Instruction ins) - { - var index = 0; - switch (ins.OpCode.Code) { - case Code.Ldloc_0: - case Code.Ldarg_0: - index = 0; - break; - case Code.Ldloc_1: - case Code.Ldarg_1: - index = 1; - break; - case Code.Ldloc_2: - case Code.Ldarg_2: - index = 2; - break; - case Code.Ldloc_3: - case Code.Ldarg_3: - index = 3; - break; - case Code.Ldloc: - case Code.Ldloc_S: - return ((VariableDefinition) ins.Operand).VariableType; - case Code.Ldarg: - case Code.Ldarg_S: - return ((ParameterDefinition) ins.Operand).ParameterType; - case Code.Ldfld: - case Code.Ldsfld: - return ((FieldReference) ins.Operand).FieldType; - case Code.Call: - case Code.Calli: - case Code.Callvirt: - return ((MethodReference) ins.Operand).ReturnType; - default: - return null; - } - - switch (ins.OpCode.Code) { - case Code.Ldloc: - case Code.Ldloc_0: - case Code.Ldloc_1: - case Code.Ldloc_2: - case Code.Ldloc_3: - return method.Body.Variables [index].VariableType; - case Code.Ldarg: - case Code.Ldarg_0: - case Code.Ldarg_1: - case Code.Ldarg_2: - case Code.Ldarg_3: - if (method.IsStatic) { - return method.Parameters [index].ParameterType; - } else if (index == 0) { - return method.DeclaringType; - } else { - return method.Parameters [index - 1].ParameterType; - } - default: - return null; - } - } - - static MethodReference GetBlockSetupImpl (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - if (data.SetupBlockImplDefinition is null) { - var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); - foreach (var method in type.Methods) { - if (method.Name != "SetupBlockImpl") - continue; - data.SetupBlockImplDefinition = method; - data.SetupBlockImplDefinition.IsPublic = true; // Make sure the method is callable from the optimized code. - break; - } - if (data.SetupBlockImplDefinition is null) - throw ErrorHelper.CreateError (data.LinkContext.App, 99, caller, ins, Errors.MX0099, $"could not find the method {Namespaces.ObjCRuntime}.BlockLiteral.SetupBlockImpl"); - } - return caller.Module.ImportReference (data.SetupBlockImplDefinition); - } - - static MethodReference GetBlockLiteralConstructor (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) - { - if (data.BlockCtorDefinition is null) { - var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); - foreach (var method in type.Methods) { - if (!method.IsConstructor) - continue; - if (method.IsStatic) - continue; - if (!method.HasParameters || method.Parameters.Count != 3) - continue; - if (!(method.Parameters [0].ParameterType is PointerType pt) || !pt.ElementType.Is ("System", "Void")) - continue; - if (!method.Parameters [1].ParameterType.Is ("System", "Object")) - continue; - if (!method.Parameters [2].ParameterType.Is ("System", "String")) - continue; - data.BlockCtorDefinition = method; - break; - } - if (data.BlockCtorDefinition is null) - throw ErrorHelper.CreateError (data.LinkContext.App, 99, caller, ins, Errors.MX0099, $"could not find the constructor ObjCRuntime.BlockLiteral (void*, object, string)"); - } - return caller.Module.ImportReference (data.BlockCtorDefinition); - } - - static bool ProcessProtocolInterfaceStaticConstructor (OptimizeGeneratedCodeData data, MethodDefinition method) - { - // The static cctor in protocol interfaces exists only to preserve the protocol's members, for inspection by the registrar(s). - // If we're registering protocols, then we don't need to preserve protocol members, because the registrar - // already knows everything about it => we can remove the static cctor. - - if (!(method.DeclaringType.IsInterface && method.IsStatic && method.IsConstructor && method.HasBody)) - return false; - - if (data.Optimizations.RegisterProtocols != true) { - data.App.Log (4, "Did not optimize static constructor in the protocol interface {0}: the 'register-protocols' optimization is disabled.", method.DeclaringType.FullName); - return false; - } - - if (!method.DeclaringType.HasCustomAttributes || !method.DeclaringType.CustomAttributes.Any (v => v.AttributeType.Is ("Foundation", "ProtocolAttribute"))) { - data.App.Log (4, "Did not optimize static constructor in the protocol interface {0}: no Protocol attribute found.", method.DeclaringType.FullName); - return false; - } - - var ins = SkipNops (method.Body.Instructions.First ()); - if (ins is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); - return false; - } else if (ins.OpCode != OpCodes.Ldnull) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); - return false; - } - - ins = SkipNops (ins.Next); - if (ins is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); - return false; - } - var callGCKeepAlive = ins; - if (callGCKeepAlive.OpCode != OpCodes.Call || !(callGCKeepAlive.Operand is MethodReference methodOperand) || methodOperand.Name != "KeepAlive" || !methodOperand.DeclaringType.Is ("System", "GC")) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); - return false; - } - - ins = SkipNops (ins.Next); - if (ins is null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); - return false; - } else if (ins.OpCode != OpCodes.Ret) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); - return false; - } - - ins = SkipNops (ins.Next); - if (ins is not null) { - ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); - return false; - } - - // We can just remove the entire method, however that confuses the linker later on, so just empty it out and remove all the attributes. - data.App.Log (4, "Optimized static constructor in the protocol interface {0} (static constructor was cleared and custom attributes removed)", method.DeclaringType.FullName); - method.Body.Instructions.Clear (); - method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); - - // Only remove DynamicDependency attributes that takes a single string argument. - // The generator generates other DynamicDependency attributes, and we don't want to remove those. - for (var i = method.CustomAttributes.Count - 1; i >= 0; i--) { - var ca = method.CustomAttributes [i]; - - if (!ca.AttributeType.Is ("System.Diagnostics.CodeAnalysis", "DynamicDependencyAttribute")) - continue; - - if (!ca.HasConstructorArguments) - continue; - - if (ca.ConstructorArguments.Count != 1) - continue; - - if (!ca.ConstructorArguments [0].Type.Is ("System", "String")) - continue; - - method.CustomAttributes.RemoveAt (i); - } - - return true; + OptimizeGeneratedCode.OptimizeMethod (data, method); } } - - public class OptimizeGeneratedCodeData { - public required Xamarin.Tuner.DerivedLinkContext LinkContext; - public required Optimizations Optimizations; - public required bool Device; - - public MethodDefinition? SetupBlockImplDefinition; - public MethodDefinition? BlockCtorDefinition; - public bool? InlineIsArm64CallingConvention; - - public Application App => LinkContext.App; - } - } diff --git a/tools/linker/OptimizeGeneratedCode.cs b/tools/linker/OptimizeGeneratedCode.cs new file mode 100644 index 000000000000..46af059779e8 --- /dev/null +++ b/tools/linker/OptimizeGeneratedCode.cs @@ -0,0 +1,1214 @@ +// Copyright 2012-2013, 2016 Xamarin Inc. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; + +using ObjCRuntime; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Linker; +using Mono.Linker.Steps; +using Mono.Tuner; +using MonoTouch.Tuner; + +using Xamarin.Bundler; + +#nullable enable + +namespace Xamarin.Linker { + public class OptimizeGeneratedCode { + public static bool IsActiveFor (AssemblyDefinition assembly, Profile profile, AnnotationStore annotations) + { + // Unless an assembly is or references our platform assembly, then it won't have anything we need to register + if (!profile.IsOrReferencesProductAssembly (assembly)) + return false; + + // We only care about assemblies that are being linked. + if (annotations.GetAction (assembly) != AssemblyAction.Link) + return false; + + return true; + } + + // [GeneratedCode] is not enough - e.g. it's used for anonymous delegates even if the + // code itself is not tool/compiler generated + static protected bool IsExport (ICustomAttributeProvider provider) + { + return provider.HasCustomAttribute (Namespaces.Foundation, "ExportAttribute"); + } + + // less risky to nop-ify if branches are pointing to this instruction + static protected void Nop (Instruction ins) + { + // Leave 'leave' instructions in place, they might be required for the resulting IL to be correct/verifiable. + switch (ins.OpCode.Code) { + case Code.Leave: + case Code.Leave_S: + return; + } + ins.OpCode = OpCodes.Nop; + ins.Operand = null; + } + + internal static bool ValidateInstruction (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, string operation, Code expected) + { + return ValidateInstruction (data.App, caller, ins, operation, expected); + } + + internal static bool ValidateInstruction (IToolLog log, MethodDefinition caller, Instruction ins, string operation, Code expected) + { + if (ins.OpCode.Code != expected) { + log.Log (1, "Could not {0} in {1} at offset {2}, expected {3} got {4}", operation, caller, ins.Offset, expected, ins); + return false; + } + + return true; + } + + internal static bool ValidateInstruction (IToolLog log, MethodDefinition caller, Instruction ins, string operation, params Code [] expected) + { + foreach (var code in expected) { + if (ins.OpCode.Code == code) + return true; + } + + log.Log (1, "Could not {0} in {1} at offset {2}, expected any of [{3}] got {4}", operation, caller, ins.Offset, string.Join (", ", expected), ins); + return false; + } + + static int? GetConstantValue (OptimizeGeneratedCodeData data, Instruction? ins) + { + if (ins is null) + return null; + + switch (ins.OpCode.Code) { + case Code.Ldc_I4_0: + return 0; + case Code.Ldc_I4_1: + return 1; + case Code.Ldc_I4_2: + return 2; + case Code.Ldc_I4_3: + return 3; + case Code.Ldc_I4_4: + return 4; + case Code.Ldc_I4_5: + return 5; + case Code.Ldc_I4_6: + return 6; + case Code.Ldc_I4_7: + return 7; + case Code.Ldc_I4_8: + return 8; + case Code.Ldc_I4: + case Code.Ldc_I4_S: + if (ins.Operand is int) + return (int) ins.Operand; + if (ins.Operand is sbyte) + return (sbyte) ins.Operand; + return null; +#if DEBUG + case Code.Isinst: // We might be able to calculate a constant value here in the future + case Code.Ldloc: + case Code.Ldloca: + case Code.Ldloc_0: + case Code.Ldloc_1: + case Code.Ldloc_2: + case Code.Ldloc_3: + case Code.Ldloc_S: + case Code.Ldloca_S: + case Code.Ldarg: + case Code.Ldarg_0: + case Code.Ldarg_1: + case Code.Ldarg_2: + case Code.Ldarg_3: + case Code.Ldarg_S: + case Code.Ldarga: + case Code.Call: + case Code.Calli: + case Code.Callvirt: + case Code.Box: + case Code.Ldsfld: + case Code.Dup: // You might think we could get the constant of the previous instruction, but this instruction might be the target of a branch, in which case the question becomes: which instruction was the previous instruction? And that's not a question easily answered without a much more thorough analysis of the code. + case Code.Ldlen: + case Code.Ldind_U1: + case Code.Ldind_U2: + case Code.Ldind_U4: + case Code.Ldind_Ref: + case Code.Conv_I: + case Code.Conv_I1: + case Code.Conv_I2: + case Code.Conv_I4: + case Code.Conv_U: + case Code.Sizeof: + case Code.Ldfld: + case Code.Ldflda: + return null; // just to not hit the CWL below +#endif + default: +#if DEBUG + data.App.Log (9, "Unknown conditional instruction: {0}", ins); +#endif + return null; + } + } + + static bool MarkInstructions (OptimizeGeneratedCodeData data, MethodDefinition method, Mono.Collections.Generic.Collection instructions, bool [] reachable, int start, int end) + { + if (reachable [start]) + return true; // We've already marked this section of code + + for (int i = start; i < end; i++) { + reachable [i] = true; + + var ins = instructions [i]; + switch (ins.OpCode.FlowControl) { + case FlowControl.Branch: + // Unconditional branch, we continue marking from the instruction that we branch to. + var br_target = (Instruction) ins.Operand; + return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (br_target), instructions.Count); + case FlowControl.Cond_Branch: + // Conditional instruction, we need to check if we can calculate a constant value for the condition + var cond_target = ins.Operand as Instruction; + bool? branch = null; // did we get a constant value for the condition, and if so, did we branch or not? + var cond_instruction_count = 0; // The number of instructions that compose the condition + + if (ins.OpCode.Code == Code.Switch) { + // Treat all branches of the switch statement as reachable. + // FIXME: calculate the potential constant branch (currently there are no optimizable methods where the switch condition is constant, so this is not needed for now) + var targets = ins.Operand as Instruction []; + if (targets is null) { + data.App.Log (4, "Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand); + return false; + } + foreach (var target in targets) { + // not constant, continue marking both this code sequence and the branched sequence + if (!MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (target), end)) + return false; + } + return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (ins.Next), end); + } + + if (cond_target is null) { + data.App.Log (4, "Can't optimize {0} because of unknown target of branch instruction {1} {2}", method, ins, ins.Operand); + return false; + } + + switch (ins.OpCode.Code) { + case Code.Brtrue: + case Code.Brtrue_S: { + var v = GetConstantValue (data, ins.Previous); + if (v.HasValue) + branch = v.Value != 0; + cond_instruction_count = 2; + break; + } + case Code.Brfalse: + case Code.Brfalse_S: { + var v = GetConstantValue (data, ins.Previous); + if (v.HasValue) + branch = v.Value == 0; + cond_instruction_count = 2; + break; + } + case Code.Beq: + case Code.Beq_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value == x2.Value; + cond_instruction_count = 3; + break; + } + case Code.Bne_Un: + case Code.Bne_Un_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value != x2.Value; + cond_instruction_count = 3; + break; + } + case Code.Ble: + case Code.Ble_S: + case Code.Ble_Un: + case Code.Ble_Un_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value <= x2.Value; + cond_instruction_count = 3; + break; + } + case Code.Blt: + case Code.Blt_S: + case Code.Blt_Un: + case Code.Blt_Un_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value < x2.Value; + cond_instruction_count = 3; + break; + } + case Code.Bge: + case Code.Bge_S: + case Code.Bge_Un: + case Code.Bge_Un_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value >= x2.Value; + cond_instruction_count = 3; + break; + } + case Code.Bgt: + case Code.Bgt_S: + case Code.Bgt_Un: + case Code.Bgt_Un_S: { + var x1 = GetConstantValue (data, ins.Previous?.Previous); + var x2 = GetConstantValue (data, ins.Previous); + if (x1.HasValue && x2.HasValue) + branch = x1.Value > x2.Value; + cond_instruction_count = 3; + break; + } + default: + data.App.Log ("Can't optimize {0} because of unknown branch instruction: {1}", method, ins); + break; + } + + if (branch.HasValue) { + // Make sure nothing else in the method branches into the middle of our supposedly constant condition, + // bypassing our constant calculation. Note that it's not a bad to branch to the _first_ instruction in + // the sequence (thus the +2 here), just into the middle of it. + if (AnyBranchTo (data, instructions, instructions [i - cond_instruction_count + 2], ins)) + branch = null; + } + + if (!branch.HasValue) { + // not constant, continue marking both this code sequence and the branched sequence + if (!MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (cond_target), end)) + return false; + } else { + // we can remove the branch (and the code that loads the condition), so we mark those instructions as dead. + for (int a = 0; a < cond_instruction_count; a++) + reachable [i - a] = false; + + // Now continue marking according to whether we branched or not + if (branch.Value) { + // branch always taken + return MarkInstructions (data, method, instructions, reachable, instructions.IndexOf (cond_target), end); + } else { + // branch never taken + // continue looping + } + } + break; + case FlowControl.Call: + case FlowControl.Next: + // Nothing special, continue marking + break; + case FlowControl.Return: + case FlowControl.Throw: + // Control flow returns here, so stop marking + return true; + case FlowControl.Break: + case FlowControl.Meta: + case FlowControl.Phi: + default: + data.App.Log (4, "Can't optimize {0} because of unknown flow control for: {1}", method, ins); + return false; + } + } + + return true; + } + + // Check if there are any branches in the instructions that branch to anywhere between 'first' and 'last' instructions (both inclusive). + static bool AnyBranchTo (OptimizeGeneratedCodeData data, Mono.Collections.Generic.Collection instructions, Instruction first, Instruction last) + { + if (first.Offset > last.Offset) { + data.App.Log ($"Broken assumption: {first} is after {last}"); + return true; // This is the safe thing to do, since it will prevent inlining + } + + for (int i = 0; i < instructions.Count; i++) { + var ins = instructions [i]; + switch (ins.OpCode.FlowControl) { + case FlowControl.Branch: + case FlowControl.Cond_Branch: + var target = ins.Operand as Instruction; + if (target is not null && target.Offset >= first.Offset && target.Offset <= last.Offset) + return true; + break; + } + } + + return false; + } + + static bool EliminateDeadCode (OptimizeGeneratedCodeData data, MethodDefinition caller) + { + var modified = false; + if (data.Optimizations.DeadCodeElimination != true) + return modified; + + var instructions = caller.Body.Instructions; + var reachable = new bool [instructions.Count]; + + // We walk the instructions in the method, starting with the first instruction, + // marking all reachable instructions. Any non-reachable instructions at the end + // can be removed. + + if (!MarkInstructions (data, caller, instructions, reachable, 0, instructions.Count)) + return modified; + + // Handle exception handlers specially, they do not follow normal code flow. + bool []? reachableExceptionHandlers = null; + if (caller.Body.HasExceptionHandlers) { + reachableExceptionHandlers = new bool [caller.Body.ExceptionHandlers.Count]; + for (var e = 0; e < reachableExceptionHandlers.Length; e++) { + var eh = caller.Body.ExceptionHandlers [e]; + + // First check if the protected region is reachable + var startI = instructions.IndexOf (eh.TryStart); + var endI = instructions.IndexOf (eh.TryEnd); + for (int i = startI; i < endI; i++) { + if (reachable [i]) { + reachableExceptionHandlers [e] = true; + break; + } + } + // The protected code isn't reachable, none of the handlers will be executed + if (!reachableExceptionHandlers [e]) + continue; + + switch (eh.HandlerType) { + case ExceptionHandlerType.Catch: + // We don't need catch handlers the reachable instructions are all nops + var allNops = true; + for (int i = startI; i < endI; i++) { + if (instructions [i].OpCode.Code != Code.Nop) { + allNops = false; + break; + } + } + if (!allNops) { + if (!MarkInstructions (data, caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd))) + return modified; + } + break; + case ExceptionHandlerType.Finally: + // finally clauses are always executed, even if the protected region is empty + if (!MarkInstructions (data, caller, instructions, reachable, instructions.IndexOf (eh.HandlerStart), instructions.IndexOf (eh.HandlerEnd))) + return modified; + break; + case ExceptionHandlerType.Fault: + case ExceptionHandlerType.Filter: + // FIXME: and until fixed, exit gracefully without doing anything + data.App.Log (4, "Unhandled exception handler: {0}, skipping dead code elimination for {1}", eh.HandlerType, caller); + return modified; + } + } + } + + if (Array.IndexOf (reachable, false) == -1) + return modified; // entire method is reachable + + // Kill branch instructions when there are only dead instructions between the branch instruction and the target of the branch + for (int i = 0; i < instructions.Count; i++) { + if (!reachable [i]) + continue; + + var ins = instructions [i]; + if (ins.OpCode.Code != Code.Br && ins.OpCode.Code != Code.Br_S) + continue; + var target = ins.Operand as Instruction; + if (target is null) + continue; + if (target.Offset < ins.Offset) + continue; // backwards branch, keep those + + var start = i + 1; + var end = instructions.IndexOf (target); + var any_reachable = false; + for (int k = start; k < end; k++) { + if (reachable [k]) { + any_reachable = true; + break; + } + } + if (any_reachable) + continue; + + // The branch instruction just branches over unreachable instructions, so it can be considered unreachable too. + reachable [i] = false; + } + + // Check if there are unreachable instructions at the end. + var last_reachable = Array.LastIndexOf (reachable, true); + if (last_reachable < reachable.Length - 1) { + // There are unreachable instructions at the end. + // We must verify that there are no branches into these instructions. + // In theory there shouldn't be any (if there are branches into these instructions, + // they're reachable), but let's still verify just in case. + var last_reachable_offset = instructions [last_reachable].Offset; + for (int i = 0; i < last_reachable; i++) { + if (!reachable [i]) + continue; // Unreachable instructions don't branch anywhere, because they'll be removed. + var ins = instructions [i]; + switch (ins.OpCode.FlowControl) { + case FlowControl.Break: + case FlowControl.Cond_Branch: + var target = (Instruction) ins.Operand; + if (target.Offset > last_reachable_offset) { + data.App.Log (4, "Can't optimize {0} because of branching beyond last instruction alive: {1}", caller, ins); + return modified; + } + break; + } + } + } +#if false + Console.WriteLine ($"{caller.FullName}:"); + for (int i = 0; i < reachable.Length; i++) { + Console.WriteLine ($"{(reachable [i] ? " " : "- ")} {instructions [i]}"); + if (!reachable [i]) + Nop (instructions [i]); + } + Console.WriteLine (); +#endif + + // Exterminate, exterminate, exterminate + for (int i = 0; i < reachable.Length; i++) { + if (!reachable [i]) { + Nop (instructions [i]); + modified = true; + } + } + + // Remove exception handlers + if (reachableExceptionHandlers is not null) { + for (int i = reachableExceptionHandlers.Length - 1; i >= 0; i--) { + if (reachableExceptionHandlers [i]) + continue; + caller.Body.ExceptionHandlers.RemoveAt (i); + modified = true; + } + } + + // Remove unreachable instructions (nops) at the end, because the last instruction can only be ret/throw/backwards branch. + for (int i = last_reachable + 1; i < reachable.Length; i++) { + instructions.RemoveAt (last_reachable + 1); + modified = true; + } + + return modified; + } + + static bool GetIsExtensionType (TypeDefinition type) + { + // if 'type' inherits from NSObject inside an assembly that has [GeneratedCode] + // or for static types used for optional members (using extensions methods), they can be optimized too + return type.IsSealed && type.IsAbstract && type.Name.EndsWith ("_Extensions", StringComparison.Ordinal); + } + + public static bool OptimizeMethod (OptimizeGeneratedCodeData data, MethodDefinition method) + { + var modified = false; + + if (!method.HasBody) + return modified; + + if (method.IsBindingImplOptimizableCode (data.LinkContext)) { + // We optimize all methods that have the [BindingImpl (BindingImplAttributes.Optimizable)] attribute. + } else if (method.IsGeneratedCode (data.LinkContext) && ( + GetIsExtensionType (method.DeclaringType) + || IsExport (method))) { + // We optimize methods that have the [GeneratedCodeAttribute] and is either an extension type or an exported method + } else { + // but it would be too risky to apply on user-generated code + return modified; + } + + if (data.Optimizations.InlineIsARM64CallingConvention == true && data.InlineIsArm64CallingConvention.HasValue && method.Name == "GetIsARM64CallingConvention" && method.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) { + // Rewrite to return the constant value + var instr = method.Body.Instructions; + instr.Clear (); + instr.Add (Instruction.Create (data.InlineIsArm64CallingConvention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0)); + instr.Add (Instruction.Create (OpCodes.Ret)); + return true; // nothing else to do here. + } + + if (ProcessProtocolInterfaceStaticConstructor (data, method)) + return true; + + var instructions = method.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) { + var ins = instructions [i]; + switch (ins.OpCode.Code) { + case Code.Newobj: + case Code.Call: + modified |= ProcessCalls (data, method, ins, out var instructionsAddedOrRemoved); + i += instructionsAddedOrRemoved; + break; + case Code.Ldsfld: + modified |= ProcessLoadStaticField (data, method, ins); + break; + } + } + + modified |= EliminateDeadCode (data, method); + return modified; + } + + // Returns the number of instructions added (or removed) in the 'instructionsAddedOrRemoved' parameter. + // Returns true if any modifications were done. + static bool ProcessCalls (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) + { + var modified = false; + instructionsAddedOrRemoved = 0; + var mr = ins.Operand as MethodReference; + switch (mr?.Name) { + case "EnsureUIThread": + modified |= ProcessEnsureUIThread (data, caller, ins); + break; + case "get_IsDirectBinding": + modified |= ProcessIsDirectBinding (data, caller, ins); + break; + case "SetupBlock": + case "SetupBlockUnsafe": + modified |= ProcessSetupBlock (data, caller, ins, out instructionsAddedOrRemoved); + break; + case ".ctor": + if (!mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) + break; + return ProcessBlockLiteralConstructor (data, caller, ins, out instructionsAddedOrRemoved); + } + + return modified; + } + + static bool ProcessLoadStaticField (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + var modified = false; + var fr = ins.Operand as FieldReference; + switch (fr?.Name) { + case "IsARM64CallingConvention": + modified |= ProcessIsARM64CallingConvention (data, caller, ins); + break; + case "Arch": + // https://app.asana.com/0/77259014252/77812690163 + modified |= ProcessRuntimeArch (data, caller, ins); + break; + } + return modified; + } + + static bool ProcessEnsureUIThread (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + if (data.Optimizations.RemoveUIThreadChecks != true) + return false; + + // Verify we're checking the right get_EnsureUIThread call + var declaringTypeNamespace = data.LinkContext.App.Platform == Utils.ApplePlatform.MacOSX ? Namespaces.AppKit : Namespaces.UIKit; + var declaringTypeName = data.LinkContext.App.Platform == Utils.ApplePlatform.MacOSX ? "NSApplication" : "UIApplication"; + var mr = ins.Operand as MethodReference; + if (mr is null || !mr.DeclaringType.Is (declaringTypeNamespace, declaringTypeName)) + return false; + + // Verify a few assumptions before doing anything + const string operation = "remove calls to [NS|UI]Application::EnsureUIThread"; + if (!ValidateInstruction (data.App, caller, ins, operation, Code.Call)) + return false; + + // This is simple: just remove the call + Nop (ins); // call void UIKit.UIApplication::EnsureUIThread() + return true; + } + + static bool? IsDirectBindingConstant (OptimizeGeneratedCodeData data, TypeDefinition type) + { + return type.IsNSObject (data.LinkContext) ? type.GetIsDirectBindingConstant (data.LinkContext) : null; + } + + static bool ProcessIsDirectBinding (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + const string operation = "inline IsDirectBinding"; + + if (data.Optimizations.InlineIsDirectBinding != true) + return false; + + bool? isdirectbinding_constant = IsDirectBindingConstant (data, caller.DeclaringType); + + // If we don't know the constant isdirectbinding value, then we can't inline anything + if (!isdirectbinding_constant.HasValue) + return false; + + // Verify we're checking the right get_IsDirectBinding call + var mr = ins.Operand as MethodReference; + if (mr is null || !mr.DeclaringType.Is (Namespaces.Foundation, "NSObject")) + return false; + + // Verify a few assumptions before doing anything + if (!ValidateInstruction (data.App, caller, ins.Previous, operation, Code.Ldarg_0)) + return false; + + if (!ValidateInstruction (data.App, caller, ins, operation, Code.Call)) + return false; + + // Clearing the branch succeeded, so clear the condition too + // ldarg.0 + Nop (ins.Previous); + // call System.Boolean Foundation.NSObject::get_IsDirectBinding() + ins.OpCode = isdirectbinding_constant.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0; + ins.Operand = null; + return true; + } + + static bool ProcessSetupBlock (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) + { + instructionsAddedOrRemoved = 0; + if (data.Optimizations.OptimizeBlockLiteralSetupBlock != true) + return false; + + // This will optimize calls to SetupBlock and SetupBlockUnsafe by calculating the signature for the block + // (which both SetupBlock and SetupBlockUnsafe do), and then rewrite the code to call SetupBlockImpl instead + // (which takes the block signature as an argument instead of calculating it). This is required to + // remove the dynamic registrar, because calculating the block signature is done in the dynamic registrar. + // + // This code is a mirror of the code in BlockLiteral.SetupBlock (to calculate the block signature). + var mr = ins.Operand as MethodReference; + if (mr is null || !mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) + return false; + + if (caller.DeclaringType.Is ("ObjCRuntime", "BlockLiteral")) { + switch (caller.Name) { + case "GetBlockForDelegate": + case "CreateBlockForDelegate": + // These methods contain a non-optimizable call to SetupBlock, and this way we don't show any warnings to users about things they can't do anything about. + return false; + } + } + + string? signature = null; + try { + // We need to figure out the type of the first argument to the call to SetupBlock[Impl]. + // + // Example sequence: + // + // ldsfld ObjCRuntime.Trampolines/DJSContextExceptionHandler ObjCRuntime.Trampolines/SDJSContextExceptionHandler::Handler + // ldarg.1 + // call System.Void ObjCRuntime.BlockLiteral::SetupBlockUnsafe(System.Delegate, System.Delegate) + // + + // Locating the instruction that loads the first argument can be complicated, so we simplify by making a few assumptions: + // 1. The instruction immediately before the call instruction (which would load the last argument) is a Push1/Pop0 instruction. + // This avoids running into trouble when the instruction does something else (it could be a any other instruction, which would throw off the next calculations) + // 2. We have a approved list of instructions we know how to calculate the type for, and which we use on the second to last instruction before the call instruction + + // First verify the Push1/Pop0 behavior in point 1. + var prev = ins.Previous; + while (prev.OpCode.Code == Code.Nop) + prev = prev.Previous; // Skip any nops. + if (prev.OpCode.StackBehaviourPush != StackBehaviour.Push1) { + //todo: localize mmp error 2106 + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev)); + return false; + } else if (prev.OpCode.StackBehaviourPop != StackBehaviour.Pop0) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106, caller, ins.Offset, mr.Name, prev)); + return false; + } + + var loadTrampolineInstruction = prev.Previous; + while (loadTrampolineInstruction.OpCode.Code == Code.Nop) + loadTrampolineInstruction = loadTrampolineInstruction.Previous; // Skip any nops. + + // Then find the type of the previous instruction (the first argument to SetupBlock[Unsafe]) + var trampolineDelegateType = GetPushedType (caller, loadTrampolineInstruction); + if (trampolineDelegateType is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_A, caller, ins.Offset, mr.Name, loadTrampolineInstruction)); + return false; + } + + if (trampolineDelegateType.Is ("System", "Delegate") || trampolineDelegateType.Is ("System", "MulticastDelegate")) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_B, caller, trampolineDelegateType.FullName, mr.Name)); + return false; + } + + if (!data.LinkContext.App.StaticRegistrar.TryComputeBlockSignature (caller, trampolineDelegateType, out var exception, out signature)) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, exception, caller, ins, Errors.MM2106_D, caller, ins.Offset, exception.Message)); + return false; + + } + } catch (Exception e) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, e, caller, ins, Errors.MM2106_D, caller, ins.Offset, e.Message)); + return false; + } + + // We got the information we need: rewrite the IL. + var instructions = caller.Body.Instructions; + var index = instructions.IndexOf (ins); + // Inject the extra arguments + instructions.Insert (index, Instruction.Create (OpCodes.Ldstr, signature)); + instructions.Insert (index, Instruction.Create (mr.Name == "SetupBlockUnsafe" ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1)); + // Change the call to call SetupBlockImpl instead + ins.Operand = GetBlockSetupImpl (data, caller, ins); + + //data.App.Log (4, "Optimized call to BlockLiteral.SetupBlock in {0} at offset {1} with delegate type {2} and signature {3}", caller, ins.Offset, delegateType.FullName, signature); + instructionsAddedOrRemoved = 2; + return true; + } + + internal static bool IsBlockLiteralCtor_Type_String (MethodDefinition md) + { + if (!md.HasParameters) + return false; + + if (md.Parameters.Count != 4) + return false; + + if (!(md.Parameters [0].ParameterType is PointerType pt) || !pt.ElementType.Is ("System", "Void")) + return false; + + if (!md.Parameters [1].ParameterType.Is ("System", "Object")) + return false; + + if (!md.Parameters [2].ParameterType.Is ("System", "Type")) + return false; + + if (!md.Parameters [3].ParameterType.Is ("System", "String")) + return false; + + return true; + } + + static bool ProcessBlockLiteralConstructor (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins, out int instructionsAddedOrRemoved) + { + instructionsAddedOrRemoved = 0; + + if (data.Optimizations.OptimizeBlockLiteralSetupBlock != true) + return false; + + // This will optimize calls to this BlockLiteral constructor: + // (void* ptr, object context, Type trampolineType, string trampolineMethod) + // by calculating the signature for the block using the last two arguments, + // and then rewrite the code to call this constructor overload instead: + // (void* ptr, object context, string signature) + // This is required to remove the dynamic registrar, because calculating the block signature + // is done in the dynamic registrar. + // + // This code is a mirror of the code in BlockLiteral.SetupBlock (to calculate the block signature). + var mr = ins.Operand as MethodReference; + if (mr is null || !mr.DeclaringType.Is (Namespaces.ObjCRuntime, "BlockLiteral")) + return false; + + var md = mr.Resolve (); + if (md is null || !IsBlockLiteralCtor_Type_String (md)) + return false; + + string? signature = null; + Instruction? sequenceStart; + try { + // We need to figure out the last argument to the call to the ctor + // + // Example sequence: + // + // ldarg.0 + // ldarg.1 + // ldtoken ... + // call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle) + // ldstr ... + // newobj BlockLiteral (void*, System.Object, System.Type, System.String) + // + + // Verify 'ldstr ...' + var loadString = GetPreviousSkippingNops (ins); + if (loadString.OpCode != OpCodes.Ldstr) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadString)); + return false; + } + + // Verify 'call System.Type System.Type::GetTypeFromHandle(System.RuntimeTypeHandle)' + var callGetTypeFromHandle = GetPreviousSkippingNops (loadString); + if (callGetTypeFromHandle.OpCode != OpCodes.Call || !(callGetTypeFromHandle.Operand is MethodReference methodOperand) || methodOperand.Name != "GetTypeFromHandle" || !methodOperand.DeclaringType.Is ("System", "Type")) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, callGetTypeFromHandle)); + return false; + } + + // Verify 'ldtoken ...' + var loadType = GetPreviousSkippingNops (callGetTypeFromHandle); + if (loadType.OpCode != OpCodes.Ldtoken) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadType)); + return false; + } + + // Then find the type of the previous instruction + var trampolineContainerTypeReference = loadType.Operand as TypeReference; + if (trampolineContainerTypeReference is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, loadType.Operand)); + return false; + } + + var trampolineContainerType = trampolineContainerTypeReference.Resolve (); + if (trampolineContainerType is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the previous instruction was unexpected ({3}) */, caller, ins.Offset, mr.Name, trampolineContainerTypeReference)); + return false; + } + + // Find the trampoline method + var trampolineMethodName = (string) loadString.Operand; + var trampolineMethods = trampolineContainerType.Methods.Where (v => v.Name == trampolineMethodName).ToArray (); + if (!trampolineMethods.Any ()) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_E1 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because no method named '{3}' was found in the type '{4}'. */, caller, ins.Offset, mr.Name, trampolineMethodName, trampolineContainerType.FullName)); + return false; + } else if (trampolineMethods.Count () > 1) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_E2 /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because more than one method named '{3}' was found in the type '{4}'. */, caller, ins.Offset, mr.Name, trampolineMethodName, trampolineContainerType.FullName)); + return false; + } + var trampolineMethod = trampolineMethods [0]; + if (!trampolineMethod.HasParameters || trampolineMethod.Parameters.Count < 1) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_F /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the method '{3}' must have at least one parameter. */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName)); + return false; + } + + // Check that the method's first parameter is either IntPtr, void* or BlockLiteral* + var firstParameterType = trampolineMethod.Parameters [0].ParameterType; + if (firstParameterType.Is ("System", "IntPtr")) { + // ok + } else if (firstParameterType is PointerType ptrType) { + var ptrTargetType = ptrType.ElementType; + if (!(ptrTargetType.Is ("System", "Void") || ptrTargetType.Is ("ObjCRuntime", "BlockLiteral"))) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_G /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the first parameter in the method '{3}' isn't 'System.IntPtr', 'void*' or 'ObjCRuntime.BlockLiteral*' (it's '{4}') */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName, firstParameterType.FullName)); + return false; + } + // ok + } else { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_G /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the first parameter in the method '{3}' isn't 'System.IntPtr', 'void*' or 'ObjCRuntime.BlockLiteral*' (it's '{4}') */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName, firstParameterType.FullName)); + return false; + } + + // Check that the method has [UnmanagedCallersOnly] + if (!trampolineMethod.HasCustomAttributes || !trampolineMethod.CustomAttributes.Any (v => v.AttributeType.Is ("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute"))) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MX2106_H /* Could not optimize the call to BlockLiteral.{2} in {0} at offset {1} because the method '{3}' does not have an [UnmanagedCallersOnly] attribute. */, caller, ins.Offset, mr.Name, trampolineContainerType.FullName + "::" + trampolineMethodName)); + return false; + } + + var userDelegateType = data.LinkContext.App.StaticRegistrar.GetUserDelegateType (trampolineMethod); + MethodReference? userMethod = null; + var blockSignature = true; + if (userDelegateType is not null) { + userMethod = data.LinkContext.App.StaticRegistrar.GetDelegateInvoke (userDelegateType); + } else { + userMethod = trampolineMethod; + } + + if (userMethod is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, caller, ins, Errors.MM2106_D, caller, ins.Offset, "Could not find delegate invoke method")); + return false; + } + + // Calculate the block signature. + var parameters = new TypeReference [userMethod.Parameters.Count]; + for (int p = 0; p < parameters.Length; p++) + parameters [p] = userMethod.Parameters [p].ParameterType; + signature = data.LinkContext.App.StaticRegistrar.ComputeSignature (userMethod.DeclaringType, false, userMethod.ReturnType, parameters, userMethod.Resolve (), isBlockSignature: blockSignature); + + sequenceStart = loadType; + } catch (Exception e) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2106, e, caller, ins, Errors.MM2106_D, caller, ins.Offset, e.Message)); + return false; + } + + // We got the information we need: rewrite the IL. + var instructions = caller.Body.Instructions; + var index = instructions.IndexOf (sequenceStart); + int instructionDiff = 0; + while (instructions [index] != ins) { + instructions.RemoveAt (index); + instructionDiff--; + } + // Inject the extra arguments + instructions.Insert (index, Instruction.Create (OpCodes.Ldstr, signature)); + instructionDiff++; + // Change the call to call the ctor with the string signature parameter instead + ins.Operand = GetBlockLiteralConstructor (data, caller, ins); + + data.App.Log (4, "Optimized call to BlockLiteral..ctor in {0} at offset {1} with signature {2}", caller, ins.Offset, signature); + instructionsAddedOrRemoved = instructionDiff; + return true; + } + + static Instruction GetPreviousSkippingNops (Instruction ins) + { + do { + ins = ins.Previous; + } while (ins.OpCode == OpCodes.Nop); + return ins; + } + + static Instruction? SkipNops (Instruction? ins) + { + if (ins is null) + return null; + + while (ins.OpCode == OpCodes.Nop) { + if (ins.Next is null) + return null; + ins = ins.Next; + } + return ins; + } + + static bool ProcessIsARM64CallingConvention (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + const string operation = "inline Runtime.IsARM64CallingConvention"; + + if (data.Optimizations.InlineIsARM64CallingConvention != true) + return false; + + if (!data.InlineIsArm64CallingConvention.HasValue) + return false; + + // Verify we're checking the right IsARM64CallingConvention field + var fr = ins.Operand as FieldReference; + if (fr is null || !fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) + return false; + + if (!ValidateInstruction (data.App, caller, ins, operation, Code.Ldsfld)) + return false; + + // We're fine, inline the Runtime.IsARM64CallingConvention value + ins.OpCode = data.InlineIsArm64CallingConvention.Value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0; + ins.Operand = null; + + return true; + } + + static bool ProcessRuntimeArch (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + const string operation = "inline Runtime.Arch"; + + if (data.Optimizations.InlineRuntimeArch != true) + return false; + + // Verify we're checking the right Arch field + var fr = ins.Operand as FieldReference; + if (fr is null || !fr.DeclaringType.Is (Namespaces.ObjCRuntime, "Runtime")) + return false; + + // Verify a few assumptions before doing anything + if (!ValidateInstruction (data.App, caller, ins, operation, Code.Ldsfld)) + return false; + + // We're fine, inline the Runtime.Arch condition + // The enum values are Runtime.DEVICE = 0 and Runtime.SIMULATOR = 1, + ins.OpCode = data.Device ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1; + ins.Operand = null; + return true; + } + + // Returns the type of the value pushed on the stack by the given instruction. + // Returns null for unknown instructions, or for instructions that don't push anything on the stack. + static TypeReference? GetPushedType (MethodDefinition method, Instruction ins) + { + var index = 0; + switch (ins.OpCode.Code) { + case Code.Ldloc_0: + case Code.Ldarg_0: + index = 0; + break; + case Code.Ldloc_1: + case Code.Ldarg_1: + index = 1; + break; + case Code.Ldloc_2: + case Code.Ldarg_2: + index = 2; + break; + case Code.Ldloc_3: + case Code.Ldarg_3: + index = 3; + break; + case Code.Ldloc: + case Code.Ldloc_S: + return ((VariableDefinition) ins.Operand).VariableType; + case Code.Ldarg: + case Code.Ldarg_S: + return ((ParameterDefinition) ins.Operand).ParameterType; + case Code.Ldfld: + case Code.Ldsfld: + return ((FieldReference) ins.Operand).FieldType; + case Code.Call: + case Code.Calli: + case Code.Callvirt: + return ((MethodReference) ins.Operand).ReturnType; + default: + return null; + } + + switch (ins.OpCode.Code) { + case Code.Ldloc: + case Code.Ldloc_0: + case Code.Ldloc_1: + case Code.Ldloc_2: + case Code.Ldloc_3: + return method.Body.Variables [index].VariableType; + case Code.Ldarg: + case Code.Ldarg_0: + case Code.Ldarg_1: + case Code.Ldarg_2: + case Code.Ldarg_3: + if (method.IsStatic) { + return method.Parameters [index].ParameterType; + } else if (index == 0) { + return method.DeclaringType; + } else { + return method.Parameters [index - 1].ParameterType; + } + default: + return null; + } + } + + static MethodReference GetBlockSetupImpl (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + if (data.SetupBlockImplDefinition is null) { + var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); + foreach (var method in type.Methods) { + if (method.Name != "SetupBlockImpl") + continue; + data.SetupBlockImplDefinition = method; + data.SetupBlockImplDefinition.IsPublic = true; // Make sure the method is callable from the optimized code. + break; + } + if (data.SetupBlockImplDefinition is null) + throw ErrorHelper.CreateError (data.LinkContext.App, 99, caller, ins, Errors.MX0099, $"could not find the method {Namespaces.ObjCRuntime}.BlockLiteral.SetupBlockImpl"); + } + return caller.Module.ImportReference (data.SetupBlockImplDefinition); + } + + static MethodReference GetBlockLiteralConstructor (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) + { + if (data.BlockCtorDefinition is null) { + var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); + foreach (var method in type.Methods) { + if (!method.IsConstructor) + continue; + if (method.IsStatic) + continue; + if (!method.HasParameters || method.Parameters.Count != 3) + continue; + if (!(method.Parameters [0].ParameterType is PointerType pt) || !pt.ElementType.Is ("System", "Void")) + continue; + if (!method.Parameters [1].ParameterType.Is ("System", "Object")) + continue; + if (!method.Parameters [2].ParameterType.Is ("System", "String")) + continue; + data.BlockCtorDefinition = method; + break; + } + if (data.BlockCtorDefinition is null) + throw ErrorHelper.CreateError (data.LinkContext.App, 99, caller, ins, Errors.MX0099, $"could not find the constructor ObjCRuntime.BlockLiteral (void*, object, string)"); + } + return caller.Module.ImportReference (data.BlockCtorDefinition); + } + + static bool ProcessProtocolInterfaceStaticConstructor (OptimizeGeneratedCodeData data, MethodDefinition method) + { + // The static cctor in protocol interfaces exists only to preserve the protocol's members, for inspection by the registrar(s). + // If we're registering protocols, then we don't need to preserve protocol members, because the registrar + // already knows everything about it => we can remove the static cctor. + + if (!(method.DeclaringType.IsInterface && method.IsStatic && method.IsConstructor && method.HasBody)) + return false; + + if (data.Optimizations.RegisterProtocols != true) { + data.App.Log (4, "Did not optimize static constructor in the protocol interface {0}: the 'register-protocols' optimization is disabled.", method.DeclaringType.FullName); + return false; + } + + if (!method.DeclaringType.HasCustomAttributes || !method.DeclaringType.CustomAttributes.Any (v => v.AttributeType.Is ("Foundation", "ProtocolAttribute"))) { + data.App.Log (4, "Did not optimize static constructor in the protocol interface {0}: no Protocol attribute found.", method.DeclaringType.FullName); + return false; + } + + var ins = SkipNops (method.Body.Instructions.First ()); + if (ins is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); + return false; + } else if (ins.OpCode != OpCodes.Ldnull) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); + return false; + } + + ins = SkipNops (ins.Next); + if (ins is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); + return false; + } + var callGCKeepAlive = ins; + if (callGCKeepAlive.OpCode != OpCodes.Call || !(callGCKeepAlive.Operand is MethodReference methodOperand) || methodOperand.Name != "KeepAlive" || !methodOperand.DeclaringType.Is ("System", "GC")) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); + return false; + } + + ins = SkipNops (ins.Next); + if (ins is null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_A /* Could not optimize the static constructor in the interface {0} because it did not have the expected instruction sequence (found end of method too soon). */, method.DeclaringType.FullName)); + return false; + } else if (ins.OpCode != OpCodes.Ret) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); + return false; + } + + ins = SkipNops (ins.Next); + if (ins is not null) { + ErrorHelper.Show (data.App, ErrorHelper.CreateWarning (data.LinkContext.App, 2112, method, ins, Errors.MX2112_B /* Could not optimize the static constructor in the interface {0} because it had an unexpected instruction {1} at offset {2}. */, method.DeclaringType.FullName, ins.OpCode, ins.Offset)); + return false; + } + + // We can just remove the entire method, however that confuses the linker later on, so just empty it out and remove all the attributes. + data.App.Log (4, "Optimized static constructor in the protocol interface {0} (static constructor was cleared and custom attributes removed)", method.DeclaringType.FullName); + method.Body.Instructions.Clear (); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + + // Only remove DynamicDependency attributes that takes a single string argument. + // The generator generates other DynamicDependency attributes, and we don't want to remove those. + for (var i = method.CustomAttributes.Count - 1; i >= 0; i--) { + var ca = method.CustomAttributes [i]; + + if (!ca.AttributeType.Is ("System.Diagnostics.CodeAnalysis", "DynamicDependencyAttribute")) + continue; + + if (!ca.HasConstructorArguments) + continue; + + if (ca.ConstructorArguments.Count != 1) + continue; + + if (!ca.ConstructorArguments [0].Type.Is ("System", "String")) + continue; + + method.CustomAttributes.RemoveAt (i); + } + + return true; + } + } + + public class OptimizeGeneratedCodeData { + public required Xamarin.Tuner.DerivedLinkContext LinkContext; + public required Optimizations Optimizations; + public required bool Device; + + public MethodDefinition? SetupBlockImplDefinition; + public MethodDefinition? BlockCtorDefinition; + public bool? InlineIsArm64CallingConvention; + + public Application App => LinkContext.App; + } + +} diff --git a/tools/linker/RegistrarRemovalTrackingStep.cs b/tools/linker/RegistrarRemovalTrackingStep.cs index b2cd10bc170c..c295752c86cc 100644 --- a/tools/linker/RegistrarRemovalTrackingStep.cs +++ b/tools/linker/RegistrarRemovalTrackingStep.cs @@ -127,7 +127,7 @@ bool RequiresDynamicRegistrar (AssemblyDefinition assembly, bool warnIfRequired) break; case ".ctor": if (mr.Resolve () is MethodDefinition md) - requires |= Xamarin.Linker.OptimizeGeneratedCodeHandler.IsBlockLiteralCtor_Type_String (md); + requires |= Xamarin.Linker.OptimizeGeneratedCode.IsBlockLiteralCtor_Type_String (md); if (requires && warnIfRequired) Warn (assembly, mr); break; From 01d6ef406087e4b3895faf41b0e290812b6ade23 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 19:08:36 +0200 Subject: [PATCH 08/13] [dotnet-linker] Refactor LinkerConfiguration to support read/write. Refactor the big switch statement in the LinkerConfiguration constructor into a GetConfigurator() method that returns a dictionary of (Load, Save) delegate pairs for each configuration key. This enables both reading configuration from file and writing/serializing the current configuration state. Also add a Save() method that uses the configurator to write out the current configuration state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/common/Application.cs | 2 + tools/dotnet-linker/LinkerConfiguration.cs | 770 +++++++++++++-------- 2 files changed, 486 insertions(+), 286 deletions(-) diff --git a/tools/common/Application.cs b/tools/common/Application.cs index 03f0d58bc48c..05ffef60c8d9 100644 --- a/tools/common/Application.cs +++ b/tools/common/Application.cs @@ -152,6 +152,7 @@ public bool IsDefaultMarshalManagedExceptionMode { // How Mono should be embedded into the app. #if !LEGACY_TOOLS AssemblyBuildTarget? libmono_link_mode; + public bool HasLibMonoLinkMode => libmono_link_mode.HasValue; public AssemblyBuildTarget LibMonoLinkMode { get { if (!libmono_link_mode.HasValue) @@ -165,6 +166,7 @@ public AssemblyBuildTarget LibMonoLinkMode { // How libxamarin should be embedded into the app. AssemblyBuildTarget? libxamarin_link_mode; + public bool HasLibXamarinLinkMode => libxamarin_link_mode.HasValue; public AssemblyBuildTarget LibXamarinLinkMode { get { if (!libxamarin_link_mode.HasValue) diff --git a/tools/dotnet-linker/LinkerConfiguration.cs b/tools/dotnet-linker/LinkerConfiguration.cs index c0152c375d5b..41d25c990fa0 100644 --- a/tools/dotnet-linker/LinkerConfiguration.cs +++ b/tools/dotnet-linker/LinkerConfiguration.cs @@ -115,6 +115,471 @@ public static LinkerConfiguration GetInstance (LinkContext context) return instance; } + public delegate void LoadValue (string key, string value); + public delegate void SaveValue (string key, List storage); + + delegate void LoadBool (string key, string value, out bool result); + delegate void LoadNullableBool (string key, string value, out bool? result); + + public class Configurator : Dictionary { } + + Configurator GetConfigurator (string linker_file) + { + var saveNonEmpty = new Action> ((key, value, storage) => { + if (string.IsNullOrEmpty (value)) + return; + storage.Add ($"{key}={value}"); + }); + var saveNullableBool = new Action> ((key, value, storage) => { + if (!value.HasValue) + return; + storage.Add ($"{key}={(value.Value ? "true" : "false")}"); + }); + var saveOptionalDefaultFalseBool = new Action> ((key, value, storage) => { + if (!value) + return; + storage.Add ($"{key}=true"); + }); + var loadBool = new LoadBool ((string key, string value, out bool result) => { + result = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); + }); + var loadNullableBool = new LoadNullableBool ((key, value, out result) => { + if (!TryParseOptionalBoolean (value, out result)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + }); + var loadWarningLevel = new Action ((key, value, level) => { + try { + ErrorHelper.ParseWarningLevel (Application, level, value); + } catch (Exception ex) { + throw new InvalidOperationException ($"Invalid {key} '{value}' in {linker_file}", ex); + } + }); + var saveWarningLevel = new Action, ErrorHelper.WarningLevel> ((key, storage, level) => { + var warningLevels = ErrorHelper.GetWarningLevels (Application); + if (warningLevels is null) + return; + foreach (var kvp in warningLevels.Where (v => v.Value == level).OrderBy (v => v.Key)) { + if (kvp.Key == -1) { + storage.Add (key); + } else { + storage.Add ($"{key}={kvp.Key}"); + } + } + }); + + var dict = new Configurator () { + { "AreAnyAssembliesTrimmed", ( + new LoadValue ((key, value) => Application.AreAnyAssembliesTrimmed = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(Application.AreAnyAssembliesTrimmed ? "true" : "false")}")) + )}, + { "AssemblyName", ( + // This is the _AssemblyName MSBuild property for the main project (which is also the root/entry assembly) + new LoadValue ((key, value) => Application.RootAssemblies.Add (value)), + new SaveValue ((key, storage) => storage.AddRange (Application.RootAssemblies.Select (v => $"{key}={v}"))) + )}, + { "AOTArgument", ( + new LoadValue ((key, value) => + { + if (!string.IsNullOrEmpty (value)) + Application.AotArguments.Add (value); + }), + new SaveValue ((key, storage) => + storage.AddRange (Application.AotArguments.Where (v => !string.IsNullOrEmpty (v)).Select (v => $"{key}={v}"))) + )}, + { "AOTCompiler", ( + new LoadValue ((key, value) => AOTCompiler = value), + new SaveValue ((key, storage) => saveNonEmpty (key, AOTCompiler, storage)) + )}, + { "AOTOutputDirectory", ( + new LoadValue ((key, value) => AOTOutputDirectory = value), + new SaveValue ((key, storage) => saveNonEmpty (key, AOTOutputDirectory, storage)) + )}, + { "AppBundleManifestPath", ( + new LoadValue ((key, value) => Application.InfoPListPath = value), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.InfoPListPath, storage)) + )}, + { "CacheDirectory", ( + new LoadValue ((key, value) => CacheDirectory = value), + new SaveValue ((key, storage) => saveNonEmpty (key, CacheDirectory, storage)) + )}, + { "CustomLinkFlags", ( + new LoadValue ((key, value) => Application.ParseCustomLinkFlags (value, "gcc_flags")), + new SaveValue ((key, storage) => storage.AddRange (Application.CustomLinkFlags?.Select (v => $"{key}={v}") ?? [])) + )}, + { "Debug", ( + new LoadValue ((key, value) => Application.EnableDebug = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(Application.EnableDebug ? "true" : "false")}")) + )}, + { "DedupAssembly", ( + new LoadValue ((key, value) => DedupAssembly = value), + new SaveValue ((key, storage) => saveNonEmpty (key, DedupAssembly, storage)) + )}, + { "DeploymentTarget", ( + new LoadValue ((key, value) => { + if (!Version.TryParse (value, out var deployment_target)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + DeploymentTarget = deployment_target; + }), + new SaveValue ((key, storage) => saveNonEmpty (key, DeploymentTarget?.ToString (), storage)) + )}, + { "Dlsym", ( + new LoadValue ((key, value) => Application.ParseDlsymOptions (value)), + new SaveValue ((key, storage) => { + switch (Application.DlsymOptions) { + case DlsymOptions.None: + storage.Add ($"Dlsym=false"); + break; + case DlsymOptions.All: + storage.Add ($"Dlsym=true"); + break; + case DlsymOptions.Custom: + if (Application.DlsymAssemblies is not null) + storage.Add ($"Dlsym={string.Join (",", Application.DlsymAssemblies.Select (v => (v.Item2 ? "+" : "-") + v.Item1 + ".dll"))}"); + break; + case DlsymOptions.Default: + // don't store default + break; + default: + throw new InvalidOperationException ($"Unknown DlsymOptions value: {Application.DlsymOptions}"); + } + }) + )}, + { "EnableSGenConc", ( + new LoadValue ((key, value) => Application.EnableSGenConc = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(Application.EnableSGenConc ? "true" : "false")}")) + )}, + { "EnvironmentVariable", ( + // Format is either of: + // NAME=VALUE + // Overwrite=BOOL|NAME=VALUE + new LoadValue ((key, value) => { + var overwrite = true; + var needle = "Overwrite="; + if (value.StartsWith (needle, StringComparison.Ordinal)) { + var pipe = value.IndexOf ('|', needle.Length); + if (pipe > 0) { + var overwriteString = value [needle.Length..pipe]; + if (!TryParseOptionalBoolean (overwriteString, out var parsedOverwrite)) + throw new InvalidOperationException ($"Unable to parse the 'Overwrite' value '{overwriteString}' for the environment variable entry '{value}' in {linker_file}"); + overwrite = parsedOverwrite.Value; + value = value [(pipe + 1)..]; + } + } + var separators = new char [] { ':', '=' }; + var equals = value.IndexOfAny (separators); + var name = value.Substring (0, equals); + var val = value.Substring (equals + 1); + Application.EnvironmentVariables.Add (name, new (val, overwrite)); + }), + new SaveValue ((key, storage) => storage.AddRange (Application.EnvironmentVariables.Select (v => $"{key}=Overwrite={v.Value.Overwrite}|{v.Key}={v.Value.Value}").OrderBy (v => v))) + )}, + { "FrameworkAssembly", ( + new LoadValue ((key, value) => FrameworkAssemblies.Add (value)), + new SaveValue ((key, storage) => storage.AddRange (FrameworkAssemblies.OrderBy (v => v).Select (v => $"{key}={v}"))) + )}, + { "InlineDlfcnMethods", ( + new LoadValue ((key, value) => { + if (Enum.TryParse (value, true, out var inlineDlfcnMode)) + InlineDlfcnMethods = inlineDlfcnMode; + else if (string.IsNullOrEmpty (value)) + InlineDlfcnMethods = InlineDlfcnMethodsMode.Disabled; + else + throw new InvalidOperationException ($"Unknown InlineDlfcnMethods value: {value}"); + }), + new SaveValue ((key, storage) => storage.Add ($"{key}={InlineDlfcnMethods}")) + )}, + { "InlineClassGetHandle", ( + new LoadValue ((key, value) => { + if (Enum.TryParse (value, true, out var inlineClassGetHandleMode)) + InlineClassGetHandle = inlineClassGetHandleMode; + else if (string.IsNullOrEmpty (value)) + InlineClassGetHandle = InlineClassGetHandleMode.Disabled; + else + throw new InvalidOperationException ($"Unknown InlineClassGetHandle value: {value}"); + }), + new SaveValue ((key, storage) => storage.Add ($"{key}={InlineClassGetHandle}")) + )}, + { "Interpreter", ( + new LoadValue ((key, value) => { + if (!string.IsNullOrEmpty (value)) + Application.ParseInterpreter (value); + }), + new SaveValue ((key, storage) => { + if (!Application.UseInterpreter) + return; + storage.Add ($"{key}={string.Join (",", Application.InterpretedAssemblies)}"); + }) + )}, + { "IntermediateLinkDir", ( + new LoadValue ((key, value) => IntermediateLinkDir = value), + new SaveValue ((key, storage) => saveNonEmpty (key, IntermediateLinkDir, storage)) + )}, + { "IntermediateOutputPath", ( + new LoadValue ((key, value) => IntermediateOutputPath = value), + new SaveValue ((key, storage) => saveNonEmpty (key, IntermediateOutputPath, storage)) + )}, + { "IsAppExtension", ( + new LoadValue ((key, value) => Application.IsExtension = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(Application.IsExtension ? "true" : "false")}")) + )}, + { "ItemsDirectory", ( + new LoadValue ((key, value) => ItemsDirectory = value), + new SaveValue ((key, storage) => saveNonEmpty (key, ItemsDirectory, storage)) + )}, + { "IsSimulatorBuild", ( + new LoadValue ((key, value) => IsSimulatorBuild = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(IsSimulatorBuild ? "true" : "false")}")) + )}, + { "LibMonoLinkMode", ( + new LoadValue ((key, value) => Application.LibMonoLinkMode = ParseLinkMode (value, key)), + new SaveValue ((key, storage) => { if (Application.HasLibMonoLinkMode) storage.Add ($"{key}={Application.LibMonoLinkMode}"); }) + )}, + { "LibXamarinLinkMode", ( + new LoadValue ((key, value) => Application.LibXamarinLinkMode = ParseLinkMode (value, key)), + new SaveValue ((key, storage) => { if (Application.HasLibXamarinLinkMode) storage.Add ($"{key}={Application.LibXamarinLinkMode}"); }) + )}, + { "MarshalManagedExceptionMode", ( + new LoadValue ((key, value) => { + if (!string.IsNullOrEmpty (value)) { + if (!Application.TryParseManagedExceptionMode (value, out var mode)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + Application.MarshalManagedExceptions = mode; + } + }), + new SaveValue ((key, storage) => storage.Add ($"{key}={Application.MarshalManagedExceptions.ToString ().ToLowerInvariant ()}")) + )}, + { "MarshalObjectiveCExceptionMode", ( + new LoadValue ((key, value) => { + if (!string.IsNullOrEmpty (value)) { + if (!Application.TryParseObjectiveCExceptionMode (value, out var mode)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + Application.MarshalObjectiveCExceptions = mode; + } + }), + new SaveValue ((key, storage) => storage.Add ($"{key}={Application.MarshalObjectiveCExceptions.ToString ().ToLowerInvariant ()}")) + )}, + { "MonoLibrary", ( + new LoadValue ((key, value) => Application.MonoLibraries.Add (value)), + new SaveValue ((key, storage) => storage.AddRange (Application.MonoLibraries.OrderBy (v => v).Select (v => $"{key}={v}"))) + )}, + { "MtouchFloat32", ( + new LoadValue ((key, value) => loadNullableBool (key, value, out Application.AotFloat32)), + new SaveValue ((key, storage) => saveNullableBool (key, Application.AotFloat32, storage)) + )}, + { "NoWarn", ( + new LoadValue ((key, value) => loadWarningLevel (key, value, ErrorHelper.WarningLevel.Disable)), + new SaveValue ((key, storage) => saveWarningLevel (key, storage, ErrorHelper.WarningLevel.Disable)) + )}, + { "Optimize", ( + new LoadValue ((key, value) => user_optimize_flags = value), + new SaveValue ((key, storage) => saveNonEmpty (key, user_optimize_flags, storage)) + )}, + { "PartialStaticRegistrarLibrary", ( + new LoadValue ((key, value) => PartialStaticRegistrarLibrary = value), + new SaveValue ((key, storage) => saveNonEmpty (key, PartialStaticRegistrarLibrary, storage)) + )}, + { "Platform", ( + new LoadValue ((key, value) => Platform = ApplePlatformExtensions.Parse (value)), + new SaveValue ((key, storage) => storage.Add ($"{key}={Platform.AsString ()}")) + )}, + { "PlatformAssembly", ( + new LoadValue ((key, value) => PlatformAssembly = Path.GetFileNameWithoutExtension (value)), + new SaveValue ((key, storage) => saveNonEmpty (key, string.IsNullOrEmpty (PlatformAssembly) ? PlatformAssembly : PlatformAssembly + ".dll", storage)) + )}, + { "PrepareAssemblies", ( + new LoadValue ((key, value) => loadBool (key, value, out Application.PrepareAssemblies)), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, Application.PrepareAssemblies, storage)) + )}, + { "PublishTrimmed", ( + new LoadValue ((key, value) => PublishTrimmed = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => storage.Add ($"{key}={(PublishTrimmed ? "true" : "false")}")) + )}, + { "ReferenceNativeSymbol", ( + new LoadValue ((key, value) => { + (string symbolType, string symbolMode, string symbol) = SplitString3 (value, ':'); + var mode = SymbolMode.Default; + switch (symbolMode) { + case "Ignore": + mode = SymbolMode.Ignore; + break; + case "": + break; + default: + throw new InvalidOperationException ($"Unknown symbol mode '{symbolMode}' for symbol '{symbol}'. Expected 'Ignore' or nothing at all."); + } + switch (symbolType) { + case "Function": + DerivedLinkContext.RequiredSymbols.AddFunction (symbol, mode); + break; + case "ObjectiveCClass": + DerivedLinkContext.RequiredSymbols.AddObjectiveCClass (symbol, mode); + break; + case "Field": + DerivedLinkContext.RequiredSymbols.AddField (symbol, mode); + break; + default: + throw new InvalidOperationException ($"Unknown symbol type '{symbolType}' for symbol '{symbol}'. Expected 'Function', 'ObjectiveCClass', or 'Field'."); + } + }), + new SaveValue ((key, storage) => { + foreach (var symbol in DerivedLinkContext.RequiredSymbols) { + var mode = symbol.Mode == SymbolMode.Ignore ? "Ignore" : ""; + switch (symbol.Type) { + case SymbolType.Function: + case SymbolType.ObjectiveCClass: + case SymbolType.Field: + storage.Add ($"{key}={symbol.Type}:{mode}:{symbol.Name}"); + break; + default: + throw new InvalidOperationException ($"Unknown symbol type '{symbol.Type}' for symbol '{symbol.Name}'. Expected 'Function', 'ObjectiveCClass', or 'Field'."); + } + } + }) + )}, + { "RelativeAppBundlePath", ( + new LoadValue ((key, value) => RelativeAppBundlePath = value), + new SaveValue ((key, storage) => saveNonEmpty (key, RelativeAppBundlePath, storage)) + )}, + { "Registrar", ( + new LoadValue ((key, value) => Application.ParseRegistrar (value)), + new SaveValue ((key, storage) => { + if (Application.Registrar == RegistrarMode.Default) + return; + storage.Add ($"{key}={Application.Registrar}"); + }) + )}, + { "RequireLinkWithAttributeForObjectiveCClassSearch", ( + new LoadValue ((key, value) => { + if (!TryParseOptionalBoolean (value, out var require_link_with_attribute_for_objectivec_class_search, defaultValue: false)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + Application.RequireLinkWithAttributeForObjectiveCClassSearch = require_link_with_attribute_for_objectivec_class_search.Value; + }), + new SaveValue ((key, storage) => storage.Add ($"{key}={(Application.RequireLinkWithAttributeForObjectiveCClassSearch ? "true" : "false")}")) + )}, + { "RequirePInvokeWrappers", ( + new LoadValue ((key, value) => { + if (!TryParseOptionalBoolean (value, out var require_pinvoke_wrappers, defaultValue: false)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + Application.RequiresPInvokeWrappers = require_pinvoke_wrappers.Value; + }), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, Application.RequiresPInvokeWrappers, storage)) + )}, + { "RuntimeConfigurationFile", ( + new LoadValue ((key, value) => Application.RuntimeConfigurationFile = value), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.RuntimeConfigurationFile, storage)) + )}, + { "SdkDevPath", ( + new LoadValue ((key, value) => Application.SdkRoot = value), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.SdkRoot, storage)) + )}, + { "SdkRootDirectory", ( + new LoadValue ((key, value) => { + SdkRootDirectory = value; + Application.FrameworkCurrentDirectory = value; + }), + new SaveValue ((key, storage) => saveNonEmpty (key, SdkRootDirectory, storage)) + )}, + { "SdkVersion", ( + new LoadValue ((key, value) => { + if (!Version.TryParse (value, out var sdk_version)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + SdkVersion = sdk_version; + }), + new SaveValue ((key, storage) => saveNonEmpty (key, SdkVersion?.ToString (), storage)) + )}, + { "SkipMarkingNSObjectsInUserAssemblies", ( + new LoadValue ((key, value) => { + if (!TryParseOptionalBoolean (value, out var skip_marking_nsobjects_in_user_assemblies)) + throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); + Application.SkipMarkingNSObjectsInUserAssemblies = skip_marking_nsobjects_in_user_assemblies.Value; + }), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, Application.SkipMarkingNSObjectsInUserAssemblies, storage)) + )}, + { "TargetArchitectures", ( + new LoadValue ((key, value) => { + if (!Enum.TryParse (value, out var abi)) + throw new InvalidOperationException ($"Unknown target architectures: {value} in {linker_file}"); + Abi = abi | (Abi & Abi.LLVM); // Preserve the LLVM flag if it was set, since TargetArchitectures is orthogonal to LLVM + }), + new SaveValue ((key, storage) => saveNonEmpty (key, (Abi & ~Abi.LLVM).ToString (), storage)) + )}, + { "TargetFramework", ( + new LoadValue ((key, value) => { + if (!TargetFramework.TryParse (value, out var tf)) + throw new InvalidOperationException ($"Invalid TargetFramework '{value}' in {linker_file}"); + Application.TargetFramework = TargetFramework.Parse (value); + }), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.TargetFramework.ToString (), storage)) + )}, + { "TypeMapAssemblyName", ( + new LoadValue ((key, value) => Application.TypeMapAssemblyName = value), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.TypeMapAssemblyName, storage)) + )}, + { "TypeMapFilePath", ( + new LoadValue ((key, value) => TypeMapFilePath = value), + new SaveValue ((key, storage) => saveNonEmpty (key, TypeMapFilePath, storage)) + )}, + { "TypeMapOutputDirectory", ( + new LoadValue ((key, value) => Application.TypeMapOutputDirectory = value), + new SaveValue ((key, storage) => saveNonEmpty (key, Application.TypeMapOutputDirectory, storage)) + )}, + { "UseLlvm", ( + new LoadValue ((key, value) => { + var use_llvm = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); + if (use_llvm) { + Abi |= Abi.LLVM; + } else { + Abi &= ~Abi.LLVM; + } + }), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, Abi.HasFlag (Abi.LLVM), storage)) + )}, + { "Verbosity", ( + new LoadValue ((key, value) => { + if (!int.TryParse (value, out var verbosity)) + throw new InvalidOperationException ($"Invalid Verbosity '{value}' in {linker_file}"); + Application.Verbosity = verbosity; + }), + new SaveValue ((key, storage) => { + if (Application.Verbosity != 0) + storage.Add ($"{key}={Application.Verbosity}"); + }) + )}, + { "Warn", ( + new LoadValue ((key, value) => loadWarningLevel (key, value, ErrorHelper.WarningLevel.Warning)), + new SaveValue ((key, storage) => saveWarningLevel (key, storage, ErrorHelper.WarningLevel.Warning)) + )}, + { "WarnAsError", ( + new LoadValue ((key, value) => loadWarningLevel (key, value, ErrorHelper.WarningLevel.Error)), + new SaveValue ((key, storage) => saveWarningLevel (key, storage, ErrorHelper.WarningLevel.Error)) + )}, + { "XamarinRuntime", ( + new LoadValue ((key, value) => { + if (!Enum.TryParse (value, out var rv)) + throw new InvalidOperationException ($"Invalid XamarinRuntime '{value}' in {linker_file}"); + Application.XamarinRuntime = rv; + }), + new SaveValue ((key, storage) => { + storage.Add ($"{key}={Application.XamarinRuntime}"); + }) + )}, + { "InvariantGlobalization", ( + new LoadValue ((key, value) => InvariantGlobalization = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, InvariantGlobalization, storage)) + )}, + { "HybridGlobalization", ( + new LoadValue ((key, value) => HybridGlobalization = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase)), + new SaveValue ((key, storage) => saveOptionalDefaultFalseBool (key, HybridGlobalization, storage)) + )}, + { "XamarinNativeLibraryDirectory", ( + new LoadValue ((key, value) => XamarinNativeLibraryDirectory = value), + new SaveValue ((key, storage) => saveNonEmpty (key, XamarinNativeLibraryDirectory, storage)) + )}, + }; + + return dict; + } + LinkerConfiguration (string linker_file) { if (!File.Exists (linker_file)) @@ -126,6 +591,7 @@ public static LinkerConfiguration GetInstance (LinkContext context) Application = new Application (this); CompilerFlags = new CompilerFlags (Application); + var configurator = GetConfigurator (linker_file); var lines = File.ReadAllLines (linker_file); var significantLines = new List (); // This is the input the cache uses to verify if the cache is still valid for (var i = 0; i < lines.Length; i++) { @@ -145,289 +611,10 @@ public static LinkerConfiguration GetInstance (LinkContext context) if (string.IsNullOrEmpty (value)) continue; - switch (key) { - case "AreAnyAssembliesTrimmed": - Application.AreAnyAssembliesTrimmed = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "AssemblyName": - // This is the AssemblyName MSBuild property for the main project (which is also the root/entry assembly) - Application.RootAssemblies.Add (value); - break; - case "AOTArgument": - if (!string.IsNullOrEmpty (value)) - Application.AotArguments.Add (value); - break; - case "AOTCompiler": - AOTCompiler = value; - break; - case "AOTOutputDirectory": - AOTOutputDirectory = value; - break; - case "DedupAssembly": - DedupAssembly = value; - break; - case "CacheDirectory": - CacheDirectory = value; - break; - case "AppBundleManifestPath": - Application.InfoPListPath = value; - break; - case "CustomLinkFlags": - Application.ParseCustomLinkFlags (value, "gcc_flags"); - break; - case "Debug": - Application.EnableDebug = string.Equals (value, "true", StringComparison.OrdinalIgnoreCase); - break; - case "DeploymentTarget": - if (!Version.TryParse (value, out var deployment_target)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - DeploymentTarget = deployment_target; - break; - case "Dlsym": - Application.ParseDlsymOptions (value); - break; - case "EnableSGenConc": - Application.EnableSGenConc = string.Equals (value, "true", StringComparison.OrdinalIgnoreCase); - break; - case "EnvironmentVariable": - var overwrite = true; - var needle = "Overwrite="; - if (value.StartsWith (needle, StringComparison.Ordinal)) { - var pipe = value.IndexOf ('|', needle.Length); - if (pipe > 0) { - var overwriteString = value [needle.Length..pipe]; - if (!TryParseOptionalBoolean (overwriteString, out var parsedOverwrite)) - throw new InvalidOperationException ($"Unable to parse the 'Overwrite' value '{overwriteString}' for the environment variable entry '{value}' in {linker_file}"); - overwrite = parsedOverwrite.Value; - value = value [(pipe + 1)..]; - } - } - var separators = new char [] { ':', '=' }; - var equals = value.IndexOfAny (separators); - var name = value.Substring (0, equals); - var val = value.Substring (equals + 1); - Application.EnvironmentVariables.Add (name, new (val, overwrite)); - break; - case "FrameworkAssembly": - FrameworkAssemblies.Add (value); - break; - case "InlineClassGetHandle": - if (Enum.TryParse (value, true, out var inlineClassGetHandleMode)) - InlineClassGetHandle = inlineClassGetHandleMode; - else if (string.IsNullOrEmpty (value)) - InlineClassGetHandle = InlineClassGetHandleMode.Disabled; - else - throw new InvalidOperationException ($"Unknown InlineClassGetHandle value: {value}"); - break; - case "InlineDlfcnMethods": - if (Enum.TryParse (value, true, out var inlineDlfcnMode)) - InlineDlfcnMethods = inlineDlfcnMode; - else if (string.IsNullOrEmpty (value)) - InlineDlfcnMethods = InlineDlfcnMethodsMode.Disabled; - else - throw new InvalidOperationException ($"Unknown InlineDlfcnMethods value: {value}"); - break; - case "IntermediateLinkDir": - IntermediateLinkDir = value; - break; - case "IntermediateOutputPath": - IntermediateOutputPath = value; - break; - case "Interpreter": - if (!string.IsNullOrEmpty (value)) - Application.ParseInterpreter (value); - break; - case "IsAppExtension": - Application.IsExtension = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "ItemsDirectory": - ItemsDirectory = value; - break; - case "IsSimulatorBuild": - IsSimulatorBuild = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "LibMonoLinkMode": - Application.LibMonoLinkMode = ParseLinkMode (value, key); - break; - case "LibXamarinLinkMode": - Application.LibXamarinLinkMode = ParseLinkMode (value, key); - break; - case "MarshalManagedExceptionMode": - if (!string.IsNullOrEmpty (value)) { - if (!Application.TryParseManagedExceptionMode (value, out var mode)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - Application.MarshalManagedExceptions = mode; - } - break; - case "MarshalObjectiveCExceptionMode": - if (!string.IsNullOrEmpty (value)) { - if (!Application.TryParseObjectiveCExceptionMode (value, out var mode)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - Application.MarshalObjectiveCExceptions = mode; - } - break; - case "MonoLibrary": - Application.MonoLibraries.Add (value); - break; - case "MtouchFloat32": - if (!TryParseOptionalBoolean (value, out Application.AotFloat32)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - break; - case "NoWarn": - try { - ErrorHelper.ParseWarningLevel (Application, ErrorHelper.WarningLevel.Disable, value); - } catch (Exception ex) { - throw new InvalidOperationException ($"Invalid WarnAsError '{value}' in {linker_file}", ex); - } - break; - case "Optimize": - user_optimize_flags = value; - break; - case "PartialStaticRegistrarLibrary": - PartialStaticRegistrarLibrary = value; - break; - case "Platform": - switch (value) { - case "iOS": - Platform = ApplePlatform.iOS; - break; - case "tvOS": - Platform = ApplePlatform.TVOS; - break; - case "macOS": - Platform = ApplePlatform.MacOSX; - break; - case "MacCatalyst": - Platform = ApplePlatform.MacCatalyst; - break; - default: - throw new InvalidOperationException ($"Unknown platform: {value} for the entry {line} in {linker_file}"); - } - break; - case "PlatformAssembly": - PlatformAssembly = Path.GetFileNameWithoutExtension (value); - break; - case "ReferenceNativeSymbol": { - (string symbolType, string symbolMode, string symbol) = SplitString3 (value, ':'); - var mode = SymbolMode.Default; - switch (symbolMode) { - case "Ignore": - mode = SymbolMode.Ignore; - break; - case "": - break; - default: - throw new InvalidOperationException ($"Unknown symbol mode '{symbolMode}' for symbol '{symbol}'. Expected 'Ignore' or nothing at all."); - } - switch (symbolType) { - case "Function": - DerivedLinkContext.RequiredSymbols.AddFunction (symbol, mode); - break; - case "ObjectiveCClass": - DerivedLinkContext.RequiredSymbols.AddObjectiveCClass (symbol, mode); - break; - case "Field": - DerivedLinkContext.RequiredSymbols.AddField (symbol, mode); - break; - default: - throw new InvalidOperationException ($"Unknown symbol type '{symbolType}' for symbol '{symbol}'. Expected 'Function', 'ObjectiveCClass', or 'Field'."); - } - break; - } - case "RelativeAppBundlePath": - RelativeAppBundlePath = value; - break; - case "Registrar": - Application.ParseRegistrar (value); - break; - case "RequireLinkWithAttributeForObjectiveCClassSearch": - if (!string.IsNullOrEmpty (value)) { // The default is 'false' - if (!TryParseOptionalBoolean (value, out var require_link_with_attribute_for_objectivec_class_search)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - Application.RequireLinkWithAttributeForObjectiveCClassSearch = require_link_with_attribute_for_objectivec_class_search.Value; - } - break; - case "RequirePInvokeWrappers": - if (!TryParseOptionalBoolean (value, out var require_pinvoke_wrappers)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - Application.RequiresPInvokeWrappers = require_pinvoke_wrappers.Value; - break; - case "RuntimeConfigurationFile": - Application.RuntimeConfigurationFile = value; - break; - case "SdkDevPath": - Application.SdkRoot = value; - break; - case "SdkRootDirectory": - SdkRootDirectory = value; - Application.FrameworkCurrentDirectory = value; - break; - case "SdkVersion": - if (!Version.TryParse (value, out var sdk_version)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - SdkVersion = sdk_version; - break; - case "SkipMarkingNSObjectsInUserAssemblies": - if (!TryParseOptionalBoolean (value, out var skip_marking_nsobjects_in_user_assemblies)) - throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); - Application.SkipMarkingNSObjectsInUserAssemblies = skip_marking_nsobjects_in_user_assemblies.Value; - break; - case "TargetArchitectures": - if (!Enum.TryParse (value, out Abi)) - throw new InvalidOperationException ($"Unknown target architectures: {value} in {linker_file}"); - break; - case "TargetFramework": - if (!TargetFramework.TryParse (value, out var tf)) - throw new InvalidOperationException ($"Invalid TargetFramework '{value}' in {linker_file}"); - Application.TargetFramework = TargetFramework.Parse (value); - break; - case "TypeMapAssemblyName": - Application.TypeMapAssemblyName = value; - break; - case "TypeMapFilePath": - TypeMapFilePath = value; - break; - case "TypeMapOutputDirectory": - Application.TypeMapOutputDirectory = value; - break; - case "UseLlvm": - use_llvm = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "Verbosity": - if (!int.TryParse (value, out var verbosity)) - throw new InvalidOperationException ($"Invalid Verbosity '{value}' in {linker_file}"); - Application.Verbosity += verbosity; - break; - case "Warn": - try { - ErrorHelper.ParseWarningLevel (Application, ErrorHelper.WarningLevel.Warning, value); - } catch (Exception ex) { - throw new InvalidOperationException ($"Invalid Warn '{value}' in {linker_file}", ex); - } - break; - case "WarnAsError": - try { - ErrorHelper.ParseWarningLevel (Application, ErrorHelper.WarningLevel.Error, value); - } catch (Exception ex) { - throw new InvalidOperationException ($"Invalid WarnAsError '{value}' in {linker_file}", ex); - } - break; - case "XamarinRuntime": - if (!Enum.TryParse (value, out var rv)) - throw new InvalidOperationException ($"Invalid XamarinRuntime '{value}' in {linker_file}"); - Application.XamarinRuntime = rv; - break; - case "InvariantGlobalization": - InvariantGlobalization = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "HybridGlobalization": - HybridGlobalization = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); - break; - case "XamarinNativeLibraryDirectory": - XamarinNativeLibraryDirectory = value; - break; - default: - throw new InvalidOperationException ($"Unknown key '{key}' in {linker_file}"); + if (configurator.TryGetValue (key, out var actions)) { + actions.Load (key, value); + } else { + throw new InvalidOperationException ($"Unknown configuration key '{key}' in {linker_file} at line {i + 1}."); } } @@ -457,8 +644,11 @@ public static LinkerConfiguration GetInstance (LinkContext context) Application.BuildTarget = IsSimulatorBuild ? BuildTarget.Simulator : BuildTarget.Device; break; case ApplePlatform.MacOSX: - default: + case ApplePlatform.MacCatalyst: break; + + default: + throw new System.InvalidOperationException ($"Unknown platform: {Platform}"); } if (Application.TargetFramework.Platform != Platform) @@ -475,6 +665,14 @@ public static LinkerConfiguration GetInstance (LinkContext context) Application.Initialize (); } + public void Save (List storage) + { + var configurator = GetConfigurator (LinkerFile); + foreach (var kvp in configurator.OrderBy (v => v.Key)) { + kvp.Value.Save (kvp.Key, storage); + } + } + // Splits a string in three based on the split character. // "a:b" => "a", "b", "" // "a:b:c" => "a", "b", "c" @@ -496,12 +694,12 @@ public static LinkerConfiguration GetInstance (LinkContext context) } - bool TryParseOptionalBoolean (string input, [NotNullWhen (true)] out bool? value) + bool TryParseOptionalBoolean (string input, [NotNullWhen (true)] out bool? value, bool defaultValue = true) { value = null; if (string.IsNullOrEmpty (input)) { - value = true; + value = defaultValue; return true; } From 8a10ef2d5be0ada229604d1eed396236781f0670 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 19:37:05 +0200 Subject: [PATCH 09/13] [tools] VSCode tasks. --- msbuild/.vscode/tasks.json | 17 +++++++++++++++++ tools/.vscode/tasks.json | 17 +++++++++++++++++ tools/ap-launcher/.vscode/tasks.json | 17 +++++++++++++++++ tools/assembly-preparer/.vscode/tasks.json | 17 +++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 msbuild/.vscode/tasks.json create mode 100644 tools/.vscode/tasks.json create mode 100644 tools/ap-launcher/.vscode/tasks.json create mode 100644 tools/assembly-preparer/.vscode/tasks.json diff --git a/msbuild/.vscode/tasks.json b/msbuild/.vscode/tasks.json new file mode 100644 index 000000000000..45465f774797 --- /dev/null +++ b/msbuild/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "command": "make", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "label": "make" + } + ] +} diff --git a/tools/.vscode/tasks.json b/tools/.vscode/tasks.json new file mode 100644 index 000000000000..45465f774797 --- /dev/null +++ b/tools/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "command": "make", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "label": "make" + } + ] +} diff --git a/tools/ap-launcher/.vscode/tasks.json b/tools/ap-launcher/.vscode/tasks.json new file mode 100644 index 000000000000..69a9b5f361b2 --- /dev/null +++ b/tools/ap-launcher/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "command": "make", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "label": "build" + } + ] +} diff --git a/tools/assembly-preparer/.vscode/tasks.json b/tools/assembly-preparer/.vscode/tasks.json new file mode 100644 index 000000000000..69a9b5f361b2 --- /dev/null +++ b/tools/assembly-preparer/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "shell", + "command": "make", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "label": "build" + } + ] +} From 3d3b7d29bbd3626fe0a43b5a6085abfb2794d989 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 4 Jun 2026 19:51:04 +0200 Subject: [PATCH 10/13] [tools] Add link to NoWarn issue --- tools/dotnet-linker/LinkerConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/dotnet-linker/LinkerConfiguration.cs b/tools/dotnet-linker/LinkerConfiguration.cs index 41d25c990fa0..1ad78a1a8313 100644 --- a/tools/dotnet-linker/LinkerConfiguration.cs +++ b/tools/dotnet-linker/LinkerConfiguration.cs @@ -366,7 +366,7 @@ Configurator GetConfigurator (string linker_file) new LoadValue ((key, value) => loadNullableBool (key, value, out Application.AotFloat32)), new SaveValue ((key, storage) => saveNullableBool (key, Application.AotFloat32, storage)) )}, - { "NoWarn", ( + { "NoWarn", ( // we should support '$(NoWarn)' at some point: https://github.com/dotnet/macios/issues/25645 new LoadValue ((key, value) => loadWarningLevel (key, value, ErrorHelper.WarningLevel.Disable)), new SaveValue ((key, storage) => saveWarningLevel (key, storage, ErrorHelper.WarningLevel.Disable)) )}, From 9e1532c9f9efb840a16bf683494df0890b58b57d Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 5 Jun 2026 08:37:46 +0200 Subject: [PATCH 11/13] [tests] Create an opt-in test to capture build performance --- tests/dotnet/UnitTests/PerformanceTests.cs | 52 ++++++++++++++++++++++ tests/dotnet/UnitTests/TestBaseClass.cs | 15 +++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/dotnet/UnitTests/PerformanceTests.cs diff --git a/tests/dotnet/UnitTests/PerformanceTests.cs b/tests/dotnet/UnitTests/PerformanceTests.cs new file mode 100644 index 000000000000..73f88a7af72e --- /dev/null +++ b/tests/dotnet/UnitTests/PerformanceTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Xamarin.Tests { + [TestFixture] + public class PerformanceTests : TestBaseClass { + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64")] + public void PrepareAssemblies (ApplePlatform platform, string runtimeIdentifiers) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + if (string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("ENABLE_PERFORMANCE_TESTS"))) + Assert.Ignore ("Test ignored because ENABLE_PERFORMANCE_TESTS is not set."); + + var projects = new string [] { "MySimpleApp", "monotouch-test" }; + var results = new List (); + var attempts = 3; + foreach (var project in projects) { + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + + var result = new PrepareAssembliesTestResult { + Project = project, + }; + foreach (var propertyValue in new bool [] { false, true }) { + var properties = GetDefaultProperties (runtimeIdentifiers); + properties ["PrepareAssemblies"] = propertyValue.ToString (); + + for (var i = 1; i <= attempts; i++) { + Clean (project_path); + var rv = DotNet.AssertBuild (project_path, properties); + (propertyValue ? result.EnabledBuildTimes : result.DisabledBuildTimes).Add (rv.Duration); + } + } + results.Add (result); + } + foreach (var result in results.OrderBy (v => v.Project)) { + Console.WriteLine ($"Results for: {result.Project}"); + Console.WriteLine ($" Enabled timings: {string.Join (", ", result.EnabledBuildTimes.Select (v => v.ToString ()))} average: {TimeSpan.FromTicks ((long) result.EnabledBuildTimes.Average (v => v.Ticks))}"); + Console.WriteLine ($" Disabled timings: {string.Join (", ", result.DisabledBuildTimes.Select (v => v.ToString ()))} average: {TimeSpan.FromTicks ((long) result.DisabledBuildTimes.Average (v => v.Ticks))}"); + } + } + + class PrepareAssembliesTestResult { + public required string Project; + public List EnabledBuildTimes = new (); + public List DisabledBuildTimes = new (); + } + } +} + diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs index e421218252eb..fbe234b535a6 100644 --- a/tests/dotnet/UnitTests/TestBaseClass.cs +++ b/tests/dotnet/UnitTests/TestBaseClass.cs @@ -133,6 +133,9 @@ protected static string GetDefaultRuntimeIdentifier (ApplePlatform platform, str protected static string GetProjectPath (string project, string? subdir = null, ApplePlatform? platform = null) { + if (TryGetTestProjectPath (project, platform ?? ApplePlatform.None, out var testProjectPath)) + return testProjectPath; + var project_dir = Path.Combine (Configuration.SourceRoot, "tests", "dotnet", project); if (!string.IsNullOrEmpty (subdir)) project_dir = Path.Combine (project_dir, subdir); @@ -154,6 +157,18 @@ protected static string GetProjectPath (string project, string? subdir = null, A return project_path; } + static bool TryGetTestProjectPath (string project, ApplePlatform platform, [NotNullWhen (true)] out string? projectPath) + { + projectPath = null; + + switch (project) { + case "monotouch-test": + projectPath = Path.Combine (Configuration.SourceRoot, "tests", project, "dotnet", platform.AsString (), project + ".csproj"); + return true; + } + return false; + } + protected string GetPlugInsRelativePath (ApplePlatform platform) { switch (platform) { From 97d66d0f4cc1c17c80cdf428a7a57e7c37ee5161 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 5 Jun 2026 08:48:48 +0200 Subject: [PATCH 12/13] [dotnet-linker] Move methods from ManagedRegistrarLookupTablesStep to AppBundleRewriter. Move FindNSObjectConstructor, FindINativeObjectConstructor, ImplementConstructNSObjectFactoryMethod, ImplementConstructINativeObjectFactoryMethod, and AddTypeInterfaceImplementation from ManagedRegistrarLookupTablesStep to AppBundleRewriter, and update all call sites accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dotnet-linker/AppBundleRewriter.cs | 171 +++++++++++++++++ .../Steps/ManagedRegistrarLookupTablesStep.cs | 174 +----------------- .../Steps/ManagedRegistrarStep.cs | 10 +- .../Steps/TrimmableRegistrarStep.cs | 6 +- 4 files changed, 183 insertions(+), 178 deletions(-) diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index 1b3908f009ea..1914b02994d7 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -1726,5 +1727,175 @@ public MethodDefinition CreateInternalPInvoke (ModuleDefinition module, string @ return rv; } + + internal static MethodDefinition? FindNSObjectConstructor (TypeDefinition type) + { + return FindConstructorWithOneParameter ("ObjCRuntime", "NativeHandle") + ?? FindConstructorWithOneParameter ("System", "IntPtr"); + + MethodDefinition? FindConstructorWithOneParameter (string ns, string cls) + => type.Methods.SingleOrDefault (method => + method.IsConstructor + && !method.IsStatic + && method.HasParameters + && method.Parameters.Count == 1 + && method.Parameters [0].ParameterType.Is (ns, cls)); + } + + internal static MethodDefinition? FindINativeObjectConstructor (TypeDefinition type) + { + return FindConstructorWithTwoParameters ("ObjCRuntime", "NativeHandle", "System", "Boolean") + ?? FindConstructorWithTwoParameters ("System", "IntPtr", "System", "Boolean"); + + MethodDefinition? FindConstructorWithTwoParameters (string ns1, string cls1, string ns2, string cls2) + => type.Methods.SingleOrDefault (method => + method.IsConstructor + && !method.IsStatic + && method.HasParameters + && method.Parameters.Count == 2 + && method.Parameters [0].ParameterType.Is (ns1, cls1) + && method.Parameters [1].ParameterType.Is (ns2, cls2)); + } + + internal void ImplementConstructNSObjectFactoryMethod (Tuner.DerivedLinkContext context, TypeDefinition type, MethodReference ctor) + { + var abr = this; + + // skip creating the factory for NSObject itself + if (type.Is ("Foundation", "NSObject")) + return; + + // Make sure the type implements INSObjectFactory, otherwise we can't override the _Xamarin_ConstructNSObject method from it. + AddTypeInterfaceImplementation (abr, context, type, abr.Foundation_INSObjectFactory); + + var createInstanceMethod = type.AddMethod ("_Xamarin_ConstructNSObject", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.NewSlot | MethodAttributes.HideBySig, abr.Foundation_NSObject); + var nativeHandleParameter = createInstanceMethod.AddParameter ("nativeHandle", abr.ObjCRuntime_NativeHandle); + abr.Foundation_INSObjectFactory.Resolve ().IsPublic = true; + createInstanceMethod.Overrides.Add (abr.INSObjectFactory__Xamarin_ConstructNSObject); + var body = createInstanceMethod.CreateBody (out var il); + + if (type.HasGenericParameters) { + ctor = type.CreateMethodReferenceOnGenericType (ctor, type.GenericParameters.ToArray ()); + } + + // return new TypeA (nativeHandle); // for NativeHandle ctor + // return new TypeA ((IntPtr) nativeHandle); // for IntPtr ctor + il.Emit (OpCodes.Ldarg, nativeHandleParameter); + if (ctor.Parameters [0].ParameterType.Is ("System", "IntPtr")) + il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); + il.Emit (OpCodes.Newobj, ctor); + il.Emit (OpCodes.Ret); + + body.GenerateILOffsets (); + + // make sure the trimmer doesn't trim it away if the type is kept + if (context.App.Registrar == RegistrarMode.TrimmableStatic) { + // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) + abr.AddDynamicDependencyAttributeToStaticConstructor (type, createInstanceMethod); + } else { + context.Annotations.Mark (createInstanceMethod); + } + } + + internal void ImplementConstructINativeObjectFactoryMethod (Tuner.DerivedLinkContext context, TypeDefinition type, MethodReference? ctor) + { + var abr = this; + + // skip creating the factory for NSObject itself + if (type.Is ("Foundation", "NSObject")) + return; + + // If the type is a subclass of NSObject, we prefer the NSObject "IntPtr" constructor + MethodReference? nsobjectConstructor = type.IsNSObject (context) ? AppBundleRewriter.FindNSObjectConstructor (type) : null; + if (nsobjectConstructor is null && ctor is null) + return; + + // Make sure the type implements INativeObject, otherwise we can't override the _Xamarin_ConstructINativeObject method from it. + AddTypeInterfaceImplementation (abr, context, type, abr.ObjCRuntime_INativeObject); + + var createInstanceMethod = type.AddMethod ("_Xamarin_ConstructINativeObject", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.NewSlot | MethodAttributes.HideBySig, abr.ObjCRuntime_INativeObject); + var nativeHandleParameter = createInstanceMethod.AddParameter ("nativeHandle", abr.ObjCRuntime_NativeHandle); + var ownsParameter = createInstanceMethod.AddParameter ("owns", abr.System_Boolean); + abr.INativeObject__Xamarin_ConstructINativeObject.Resolve ().IsPublic = true; + createInstanceMethod.Overrides.Add (abr.INativeObject__Xamarin_ConstructINativeObject); + var body = createInstanceMethod.CreateBody (out var il); + + if (nsobjectConstructor is not null) { + // var instance = new TypeA (nativeHandle); + // // alternatively with a cast: new TypeA ((IntPtr) nativeHandle); + // if (instance is not null && owns) + // Runtime.TryReleaseINativeObject (instance); + // return instance; + + if (type.HasGenericParameters) { + nsobjectConstructor = type.CreateMethodReferenceOnGenericType (nsobjectConstructor, type.GenericParameters.ToArray ()); + } + + il.Emit (OpCodes.Ldarg, nativeHandleParameter); + if (nsobjectConstructor.Parameters [0].ParameterType.Is ("System", "IntPtr")) + il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); + il.Emit (OpCodes.Newobj, nsobjectConstructor); + + var falseTarget = il.Create (OpCodes.Nop); + il.Emit (OpCodes.Dup); + il.Emit (OpCodes.Ldnull); + il.Emit (OpCodes.Cgt_Un); + il.Emit (OpCodes.Ldarg, ownsParameter); + il.Emit (OpCodes.And); + il.Emit (OpCodes.Brfalse_S, falseTarget); + + il.Emit (OpCodes.Dup); + il.Emit (OpCodes.Call, abr.Runtime_TryReleaseINativeObject); + + il.Append (falseTarget); + + il.Emit (OpCodes.Ret); + } else if (ctor is not null) { + // return new TypeA (nativeHandle, owns); // for NativeHandle ctor + // return new TypeA ((IntPtr) nativeHandle, owns); // IntPtr ctor + + if (type.HasGenericParameters) { + ctor = type.CreateMethodReferenceOnGenericType (ctor, type.GenericParameters.ToArray ()); + } + + il.Emit (OpCodes.Ldarg, nativeHandleParameter); + if (ctor.Parameters [0].ParameterType.Is ("System", "IntPtr")) + il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); + il.Emit (OpCodes.Ldarg, ownsParameter); + il.Emit (OpCodes.Newobj, ctor); + il.Emit (OpCodes.Ret); + } else { + throw new UnreachableException (); + } + + body.GenerateILOffsets (); + + // make sure the trimmer doesn't trim it away if the type is kept + if (context.App.Registrar == RegistrarMode.TrimmableStatic) { + // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) + abr.AddDynamicDependencyAttributeToStaticConstructor (type, createInstanceMethod); + } else { + context.Annotations.Mark (createInstanceMethod); + } + } + + static void AddTypeInterfaceImplementation (AppBundleRewriter abr, Tuner.DerivedLinkContext context, TypeDefinition type, TypeReference iface) + { + if (type.HasInterfaces && type.Interfaces.Any (v => v.InterfaceType == iface)) + return; + + var ifaceImplementation = new InterfaceImplementation (iface); + type.Interfaces.Add (ifaceImplementation); + + // make sure the trimmer doesn't trim it away if the type is kept + if (context.App.Registrar == RegistrarMode.TrimmableStatic) { + // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) + abr.AddAttributeToStaticConstructor (type, abr.CreateDynamicDependencyAttribute (DynamicallyAccessedMemberTypes.Interfaces, type)); + } else { + context.Annotations.Mark (ifaceImplementation); + context.Annotations.Mark (ifaceImplementation.InterfaceType); + context.Annotations.Mark (ifaceImplementation.InterfaceType.Resolve ()); + } + } } } diff --git a/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs b/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs index 68bbe468e009..4396c7319478 100644 --- a/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs +++ b/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs @@ -328,7 +328,7 @@ void GenerateConstructNSObject (TypeDefinition registrarType) var types = GetRelevantTypes (type => type.IsNSObject (DerivedLinkContext) && !type.IsAbstract && !type.IsInterface); foreach (var type in types) { - var ctorRef = FindNSObjectConstructor (type); + var ctorRef = AppBundleRewriter.FindNSObjectConstructor (type); if (ctorRef is null) { App.Log (9, $"Cannot include {type.FullName} in ConstructNSObject because it doesn't have a suitable constructor"); continue; @@ -357,7 +357,7 @@ void GenerateConstructNSObject (TypeDefinition registrarType) } // In addition to the big lookup method, implement the static factory method on the type: - ImplementConstructNSObjectFactoryMethod (abr, DerivedLinkContext, type, ctor); + abr.ImplementConstructNSObjectFactoryMethod (DerivedLinkContext, type, ctor); } // return default (NSObject); @@ -386,7 +386,7 @@ void GenerateConstructINativeObject (TypeDefinition registrarType) var types = GetRelevantTypes (type => type.IsNativeObject () && !type.IsAbstract && !type.IsInterface); foreach (var type in types) { - var ctorRef = FindINativeObjectConstructor (type); + var ctorRef = AppBundleRewriter.FindINativeObjectConstructor (type); if (ctorRef is not null) { var ctor = abr.CurrentAssembly.MainModule.ImportReference (ctorRef); @@ -415,7 +415,7 @@ void GenerateConstructINativeObject (TypeDefinition registrarType) } // In addition to the big lookup method, implement the static factory method on the type: - ImplementConstructINativeObjectFactoryMethod (abr, DerivedLinkContext, type, ctorRef); + abr.ImplementConstructINativeObjectFactoryMethod (DerivedLinkContext, type, ctorRef); } // return default (NSObject) @@ -444,172 +444,6 @@ void MarkConstructorIfTrimmed (MethodReference ctor) } } - static void AddTypeInterfaceImplementation (AppBundleRewriter abr, Tuner.DerivedLinkContext context, TypeDefinition type, TypeReference iface) - { - if (type.HasInterfaces && type.Interfaces.Any (v => v.InterfaceType == iface)) - return; - - var ifaceImplementation = new InterfaceImplementation (iface); - type.Interfaces.Add (ifaceImplementation); - - // make sure the trimmer doesn't trim it away if the type is kept - if (context.App.Registrar == RegistrarMode.TrimmableStatic) { - // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) - abr.AddAttributeToStaticConstructor (type, abr.CreateDynamicDependencyAttribute (DynamicallyAccessedMemberTypes.Interfaces, type)); - } else { - context.Annotations.Mark (ifaceImplementation); - context.Annotations.Mark (ifaceImplementation.InterfaceType); - context.Annotations.Mark (ifaceImplementation.InterfaceType.Resolve ()); - } - } - - internal static void ImplementConstructNSObjectFactoryMethod (AppBundleRewriter abr, Tuner.DerivedLinkContext context, TypeDefinition type, MethodReference ctor) - { - // skip creating the factory for NSObject itself - if (type.Is ("Foundation", "NSObject")) - return; - - // Make sure the type implements INSObjectFactory, otherwise we can't override the _Xamarin_ConstructNSObject method from it. - AddTypeInterfaceImplementation (abr, context, type, abr.Foundation_INSObjectFactory); - - var createInstanceMethod = type.AddMethod ("_Xamarin_ConstructNSObject", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.NewSlot | MethodAttributes.HideBySig, abr.Foundation_NSObject); - var nativeHandleParameter = createInstanceMethod.AddParameter ("nativeHandle", abr.ObjCRuntime_NativeHandle); - abr.Foundation_INSObjectFactory.Resolve ().IsPublic = true; - createInstanceMethod.Overrides.Add (abr.INSObjectFactory__Xamarin_ConstructNSObject); - var body = createInstanceMethod.CreateBody (out var il); - - if (type.HasGenericParameters) { - ctor = type.CreateMethodReferenceOnGenericType (ctor, type.GenericParameters.ToArray ()); - } - - // return new TypeA (nativeHandle); // for NativeHandle ctor - // return new TypeA ((IntPtr) nativeHandle); // for IntPtr ctor - il.Emit (OpCodes.Ldarg, nativeHandleParameter); - if (ctor.Parameters [0].ParameterType.Is ("System", "IntPtr")) - il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); - il.Emit (OpCodes.Newobj, ctor); - il.Emit (OpCodes.Ret); - - body.GenerateILOffsets (); - - // make sure the trimmer doesn't trim it away if the type is kept - if (context.App.Registrar == RegistrarMode.TrimmableStatic) { - // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) - abr.AddDynamicDependencyAttributeToStaticConstructor (type, createInstanceMethod); - } else { - context.Annotations.Mark (createInstanceMethod); - } - } - - internal static void ImplementConstructINativeObjectFactoryMethod (AppBundleRewriter abr, Tuner.DerivedLinkContext context, TypeDefinition type, MethodReference? ctor) - { - // skip creating the factory for NSObject itself - if (type.Is ("Foundation", "NSObject")) - return; - - // If the type is a subclass of NSObject, we prefer the NSObject "IntPtr" constructor - MethodReference? nsobjectConstructor = type.IsNSObject (context) ? FindNSObjectConstructor (type) : null; - if (nsobjectConstructor is null && ctor is null) - return; - - // Make sure the type implements INativeObject, otherwise we can't override the _Xamarin_ConstructINativeObject method from it. - AddTypeInterfaceImplementation (abr, context, type, abr.ObjCRuntime_INativeObject); - - var createInstanceMethod = type.AddMethod ("_Xamarin_ConstructINativeObject", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.NewSlot | MethodAttributes.HideBySig, abr.ObjCRuntime_INativeObject); - var nativeHandleParameter = createInstanceMethod.AddParameter ("nativeHandle", abr.ObjCRuntime_NativeHandle); - var ownsParameter = createInstanceMethod.AddParameter ("owns", abr.System_Boolean); - abr.INativeObject__Xamarin_ConstructINativeObject.Resolve ().IsPublic = true; - createInstanceMethod.Overrides.Add (abr.INativeObject__Xamarin_ConstructINativeObject); - var body = createInstanceMethod.CreateBody (out var il); - - if (nsobjectConstructor is not null) { - // var instance = new TypeA (nativeHandle); - // // alternatively with a cast: new TypeA ((IntPtr) nativeHandle); - // if (instance is not null && owns) - // Runtime.TryReleaseINativeObject (instance); - // return instance; - - if (type.HasGenericParameters) { - nsobjectConstructor = type.CreateMethodReferenceOnGenericType (nsobjectConstructor, type.GenericParameters.ToArray ()); - } - - il.Emit (OpCodes.Ldarg, nativeHandleParameter); - if (nsobjectConstructor.Parameters [0].ParameterType.Is ("System", "IntPtr")) - il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); - il.Emit (OpCodes.Newobj, nsobjectConstructor); - - var falseTarget = il.Create (OpCodes.Nop); - il.Emit (OpCodes.Dup); - il.Emit (OpCodes.Ldnull); - il.Emit (OpCodes.Cgt_Un); - il.Emit (OpCodes.Ldarg, ownsParameter); - il.Emit (OpCodes.And); - il.Emit (OpCodes.Brfalse_S, falseTarget); - - il.Emit (OpCodes.Dup); - il.Emit (OpCodes.Call, abr.Runtime_TryReleaseINativeObject); - - il.Append (falseTarget); - - il.Emit (OpCodes.Ret); - } else if (ctor is not null) { - // return new TypeA (nativeHandle, owns); // for NativeHandle ctor - // return new TypeA ((IntPtr) nativeHandle, owns); // IntPtr ctor - - if (type.HasGenericParameters) { - ctor = type.CreateMethodReferenceOnGenericType (ctor, type.GenericParameters.ToArray ()); - } - - il.Emit (OpCodes.Ldarg, nativeHandleParameter); - if (ctor.Parameters [0].ParameterType.Is ("System", "IntPtr")) - il.Emit (OpCodes.Call, abr.NativeObject_op_Implicit_IntPtr); - il.Emit (OpCodes.Ldarg, ownsParameter); - il.Emit (OpCodes.Newobj, ctor); - il.Emit (OpCodes.Ret); - } else { - throw new UnreachableException (); - } - - body.GenerateILOffsets (); - - // make sure the trimmer doesn't trim it away if the type is kept - if (context.App.Registrar == RegistrarMode.TrimmableStatic) { - // TODO: need to investigate why this is needed (https://github.com/dotnet/macios/issues/25232) - abr.AddDynamicDependencyAttributeToStaticConstructor (type, createInstanceMethod); - } else { - context.Annotations.Mark (createInstanceMethod); - } - } - - internal static MethodDefinition? FindNSObjectConstructor (TypeDefinition type) - { - return FindConstructorWithOneParameter ("ObjCRuntime", "NativeHandle") - ?? FindConstructorWithOneParameter ("System", "IntPtr"); - - MethodDefinition? FindConstructorWithOneParameter (string ns, string cls) - => type.Methods.SingleOrDefault (method => - method.IsConstructor - && !method.IsStatic - && method.HasParameters - && method.Parameters.Count == 1 - && method.Parameters [0].ParameterType.Is (ns, cls)); - } - - internal static MethodDefinition? FindINativeObjectConstructor (TypeDefinition type) - { - return FindConstructorWithTwoParameters ("ObjCRuntime", "NativeHandle", "System", "Boolean") - ?? FindConstructorWithTwoParameters ("System", "IntPtr", "System", "Boolean"); - - MethodDefinition? FindConstructorWithTwoParameters (string ns1, string cls1, string ns2, string cls2) - => type.Methods.SingleOrDefault (method => - method.IsConstructor - && !method.IsStatic - && method.HasParameters - && method.Parameters.Count == 2 - && method.Parameters [0].ParameterType.Is (ns1, cls1) - && method.Parameters [1].ParameterType.Is (ns2, cls2)); - } - void GenerateRegisterWrapperTypes (TypeDefinition type) { var method = type.AddMethod ("RegisterWrapperTypes", MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.HideBySig, abr.System_Void); diff --git a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs index 70c4cc72a7bf..28d906d981db 100644 --- a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs +++ b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs @@ -183,22 +183,22 @@ bool ProcessType (TypeDefinition type, AssemblyTrampolineInfo infos, List acti // INativeObject subclasses var inativeObjectTypes = StaticRegistrar.GetAllTypes (assembly).Where (t => !t.IsInterface && !t.IsAbstract && t.IsNativeObject ()); foreach (var tr in inativeObjectTypes.OrderBy (v => v.FullName)) { - var inativeObjCtor = ManagedRegistrarLookupTablesStep.FindINativeObjectConstructor (tr); + var inativeObjCtor = AppBundleRewriter.FindINativeObjectConstructor (tr); if (inativeObjCtor is null) continue; @@ -373,7 +373,7 @@ void addPostAction (AssemblyDefinition assembly, Action acti var createObjectMethod = proxyType.AddMethod ("CreateObject", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, abr.Foundation_NSObject); createObjectMethod.AddParameter ("handle", abr.System_IntPtr); il = createObjectMethod.Body.GetILProcessor (); - var nativeHandleCtor = ManagedRegistrarLookupTablesStep.FindNSObjectConstructor (td); + var nativeHandleCtor = AppBundleRewriter.FindNSObjectConstructor (td); if (nativeHandleCtor is not null) { il.Append (il.Create (OpCodes.Ldarg_1)); if (nativeHandleCtor.Parameters [0].ParameterType.Is ("ObjCRuntime", "NativeHandle")) @@ -512,7 +512,7 @@ void addPostAction (AssemblyDefinition assembly, Action acti createObjectMethod.AddParameter ("handle", abr.System_IntPtr); createObjectMethod.AddParameter ("owns", abr.System_Boolean); createObjectMethod.CreateBody (out il); - var nativeHandleCtor = ManagedRegistrarLookupTablesStep.FindINativeObjectConstructor (objcType.ProtocolWrapperType.Resolve ()); + var nativeHandleCtor = AppBundleRewriter.FindINativeObjectConstructor (objcType.ProtocolWrapperType.Resolve ()); if (nativeHandleCtor is not null) { il.Append (il.Create (OpCodes.Ldarg_1)); if (nativeHandleCtor.Parameters [0].ParameterType.Is ("ObjCRuntime", "NativeHandle")) From 2fbe8551adf5675e83169fc5ed0f3313ebf93713 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 21 Jan 2026 11:45:54 +0100 Subject: [PATCH 13/13] [assembly-preparer] Create a new tool to replace pre-mark custom linker steps. Create an 'assembly-preparer' tool that performs assembly pre-processing (currently done by custom linker steps) as a standalone build step, gated behind the opt-in `PrepareAssemblies=true` MSBuild property. Key changes: * New `tools/assembly-preparer/` library with scaffolding that emulates a subset of ILLink's API (this makes it easy to reuse the code for existing custom trimmer step by just running the same steps in the new assembly-preparer tool instead). * New `PrepareAssemblies` MSBuild task and `_PrepareAssemblies` target in Xamarin.Shared.targets. * When `PrepareAssemblies=true`, most custom linker steps/mark handlers are disabled in the trimmer pipeline (conditions added to Xamarin.Shared.Sdk.targets). * Refactor `LinkerConfiguration.cs` with `#if ASSEMBLY_PREPARER` to support both linker and assembly-preparer contexts. * New test project `tests/assembly-preparer/` with tests for the assembly preparation steps. * Add xharness test variation for running with assembly-preparer enabled. * New `tools/ap-launcher/` to invoke the tool from the command line; this is useful during debugging. Contributes towards https://github.com/dotnet/macios/issues/17693. --- dotnet/targets/Xamarin.Shared.Sdk.targets | 79 ++-- msbuild/ILMerge.targets | 1 + .../MSBStrings.resx | 5 + msbuild/Xamarin.MacDev.Tasks.slnx | 4 + .../ConsoleToTaskWriter.cs | 103 ++++++ .../ErrorHelper.msbuild.cs | 54 --- .../Xamarin.MacDev.Tasks/LoggingExtensions.cs | 23 ++ .../Tasks/PrepareAssemblies.cs | 109 ++++++ .../Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs | 39 +- .../Xamarin.MacDev.Tasks.csproj | 36 +- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 49 +++ src/ObjCRuntime/Registrar.core.cs | 8 + src/ObjCRuntime/Registrar.cs | 12 + src/bgen/BindingTouch.cs | 7 +- tests/assembly-preparer/BaseClass.cs | 98 +++++ tests/assembly-preparer/GlobalUsings.cs | 17 + .../InlineDlfcnMethodsStepTests.cs | 54 +++ tests/assembly-preparer/Makefile | 8 + .../MarkIProtocolHandlerTests.cs | 73 ++++ .../OptimizeGeneratedCodeHandlerTests.cs | 251 +++++++++++++ .../PreserveBlockCodeHandlerTests.cs | 52 +++ .../PreserveSmartEnumConversionsTest.cs | 119 ++++++ tests/assembly-preparer/ReproTest.cs | 203 +++++++++++ .../assembly-preparer-tests.csproj | 50 +++ .../assembly-preparer/assembly-preparer.slnx | 4 + tests/common/Configuration.cs | 57 +++ tests/common/DotNet.cs | 12 +- tests/common/test-variations.csproj | 6 + tests/linker/link all/dotnet/shared.csproj | 1 + .../xharness/Jenkins/TestVariationsFactory.cs | 34 ++ tools/Makefile | 1 + tools/ap-launcher/.gitignore | 2 + tools/ap-launcher/Makefile | 8 + tools/ap-launcher/Program.cs | 74 ++++ tools/ap-launcher/README.md | 2 + tools/ap-launcher/ap-launcher.csproj | 17 + tools/ap-launcher/ap-launcher.slnx | 5 + tools/assembly-preparer/.gitignore | 2 + tools/assembly-preparer/AssemblyPreparer.cs | 331 +++++++++++++++++ .../DynamicallyAccessedMemberTypes.cs | 176 +++++++++ tools/assembly-preparer/GlobalUsings.cs | 16 + .../assembly-preparer/IAssemblyPreparerLog.cs | 26 ++ tools/assembly-preparer/Makefile | 25 ++ .../NetStandardExtensions.cs | 109 ++++++ tools/assembly-preparer/README.md | 35 ++ .../Scaffolding/AnnotationStore.cs | 167 +++++++++ .../Scaffolding/AssemblyAction.cs | 15 + .../assembly-preparer/Scaffolding/BaseStep.cs | 85 +++++ tools/assembly-preparer/Scaffolding/IStep.cs | 38 ++ .../Scaffolding/LinkContext.cs | 48 +++ tools/assembly-preparer/Scaffolding/Target.cs | 11 + ...System_Diagnostics_UnreachableException.cs | 48 +++ tools/assembly-preparer/System_Index.cs | 170 +++++++++ tools/assembly-preparer/System_Range.cs | 141 ++++++++ .../assembly-preparer.csproj | 341 ++++++++++++++++++ .../assembly-preparer/assembly-preparer.slnx | 5 + tools/common/Application.cs | 52 ++- tools/common/Assembly.cs | 9 +- tools/common/CoreResolver.cs | 2 + tools/common/DerivedLinkContext.cs | 17 +- tools/common/Driver.cs | 7 +- tools/common/ErrorHelper.tools.cs | 6 + tools/common/IToolLog.cs | 18 +- tools/common/Make.common | 3 + tools/common/Makefile | 5 + tools/common/NullableAttributes.cs | 18 + tools/common/Optimizations.cs | 2 +- tools/common/StaticRegistrar.cs | 10 +- tools/common/cache.cs | 2 + tools/common/error.cs | 7 + .../Program.cs | 122 ++++++- tools/dotnet-linker/AppBundleRewriter.cs | 77 +++- .../ApplyPreserveAttributeBase.cs | 3 +- .../ApplyPreserveAttributeStep.cs | 6 + tools/dotnet-linker/Compat.cs | 2 +- tools/dotnet-linker/DotNetGlobals.cs | 1 + tools/dotnet-linker/DotNetResolver.cs | 6 + tools/dotnet-linker/LinkerConfiguration.cs | 143 +++++++- tools/dotnet-linker/MarkIProtocolHandler.cs | 20 +- .../Steps/ExceptionalMarkHandler.cs | 4 + .../Steps/InlineDlfcnMethodsStep.cs | 2 +- .../Steps/ManagedRegistrarLookupTablesStep.cs | 34 +- .../Steps/ManagedRegistrarStep.cs | 80 +++- .../Steps/PreserveBlockCodeHandler.cs | 3 +- .../Steps/SetBeforeFieldInitStep.cs | 33 +- .../Steps/TrimmableRegistrarStep.cs | 19 +- tools/linker/CoreTypeMapStep.cs | 6 +- tools/linker/MarkNSObjects.cs | 3 +- tools/linker/MobileExtensions.cs | 2 +- tools/linker/MonoTouch.Tuner/Extensions.cs | 4 + tools/linker/ObjCExtensions.cs | 6 +- tools/linker/OptimizeGeneratedCode.cs | 7 +- tools/mtouch/Errors.designer.cs | 9 + tools/mtouch/Errors.resx | 4 + tools/mtouch/mtouch.cs | 3 +- tools/tools.slnx | 1 + 96 files changed, 4005 insertions(+), 219 deletions(-) create mode 100644 msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs delete mode 100644 msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs create mode 100644 msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs create mode 100644 tests/assembly-preparer/BaseClass.cs create mode 100644 tests/assembly-preparer/GlobalUsings.cs create mode 100644 tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs create mode 100644 tests/assembly-preparer/Makefile create mode 100644 tests/assembly-preparer/MarkIProtocolHandlerTests.cs create mode 100644 tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs create mode 100644 tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs create mode 100644 tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs create mode 100644 tests/assembly-preparer/ReproTest.cs create mode 100644 tests/assembly-preparer/assembly-preparer-tests.csproj create mode 100644 tests/assembly-preparer/assembly-preparer.slnx create mode 100644 tools/ap-launcher/.gitignore create mode 100644 tools/ap-launcher/Makefile create mode 100644 tools/ap-launcher/Program.cs create mode 100644 tools/ap-launcher/README.md create mode 100644 tools/ap-launcher/ap-launcher.csproj create mode 100644 tools/ap-launcher/ap-launcher.slnx create mode 100644 tools/assembly-preparer/.gitignore create mode 100644 tools/assembly-preparer/AssemblyPreparer.cs create mode 100644 tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs create mode 100644 tools/assembly-preparer/GlobalUsings.cs create mode 100644 tools/assembly-preparer/IAssemblyPreparerLog.cs create mode 100644 tools/assembly-preparer/Makefile create mode 100644 tools/assembly-preparer/NetStandardExtensions.cs create mode 100644 tools/assembly-preparer/README.md create mode 100644 tools/assembly-preparer/Scaffolding/AnnotationStore.cs create mode 100644 tools/assembly-preparer/Scaffolding/AssemblyAction.cs create mode 100644 tools/assembly-preparer/Scaffolding/BaseStep.cs create mode 100644 tools/assembly-preparer/Scaffolding/IStep.cs create mode 100644 tools/assembly-preparer/Scaffolding/LinkContext.cs create mode 100644 tools/assembly-preparer/Scaffolding/Target.cs create mode 100644 tools/assembly-preparer/System_Diagnostics_UnreachableException.cs create mode 100644 tools/assembly-preparer/System_Index.cs create mode 100644 tools/assembly-preparer/System_Range.cs create mode 100644 tools/assembly-preparer/assembly-preparer.csproj create mode 100644 tools/assembly-preparer/assembly-preparer.slnx create mode 100644 tools/common/Makefile diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 02926aa4ed8c..ec3045cc5df3 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -271,6 +271,7 @@ _ResolveAppExtensionReferences; _ExtendAppExtensionReferences; _ComputeLinkerArguments; + _PrepareAssemblies; _ComputeFrameworkFilesToPublish; _ComputeDynamicLibrariesToPublish; ComputeFilesToPublish; @@ -633,6 +634,7 @@ AOTOutputDirectory=$(_AOTOutputDirectory) DedupAssembly=$(_DedupAssembly) AppBundleManifestPath=$(_AppBundleManifestPath) + AssemblyPublishDir=$(_AssemblyPublishDir) CacheDirectory=$(_LinkerCacheDirectory) @(_BundlerDlsym -> 'Dlsym=%(Identity)') Debug=$(_BundlerDebug) @@ -662,6 +664,8 @@ PartialStaticRegistrarLibrary=$(_LibPartialStaticRegistrar) Platform=$(_PlatformName) PlatformAssembly=$(_PlatformAssemblyName).dll + PrepareAssemblies=$(PrepareAssemblies) + PublishTrimmed=$(PublishTrimmed) RelativeAppBundlePath=$(_RelativeAppBundlePath) Registrar=$(Registrar) @(ReferenceNativeSymbol -> 'ReferenceNativeSymbol=%(SymbolType):%(SymbolMode):%(Identity)') @@ -674,9 +678,11 @@ SkipMarkingNSObjectsInUserAssemblies=$(_SkipMarkingNSObjectsInUserAssemblies) TargetArchitectures=$(TargetArchitectures) TargetFramework=$(_ComputedTargetFrameworkMoniker) + TrimMode=$(TrimMode) TypeMapAssemblyName=$(_TypeMapAssemblyName) TypeMapFilePath=$(_TypeMapFilePath) TypeMapOutputDirectory=$(_TypeMapOutputDirectory) + UnmanagedCallersOnlyMapPath=$(_UnmanagedCallersOnlyMapPath) UseLlvm=$(MtouchUseLlvm) Verbosity=$(_BundlerVerbosity) Warn=$(_BundlerWarn) @@ -784,44 +790,45 @@ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.CollectAssembliesStep" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.CoreTypeMapStep" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.ProcessExportedFields" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.PreserveProtocolsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveBlockCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.OptimizeGeneratedCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.ApplyPreserveAttributeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseLinkDescriptionForApplyPreserveAttribute)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkForStaticRegistrarStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkStaticRegistrar)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkNSObjectsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkNSObjects)' == 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineDlfcnMethodsStep" Condition="'$(InlineDlfcnMethods)' != ''" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.PreserveProtocolsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveBlockCodeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.OptimizeGeneratedCodeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.ApplyPreserveAttributeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseLinkDescriptionForApplyPreserveAttribute)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkForStaticRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkStaticRegistrar)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkNSObjectsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkNSObjects)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineDlfcnMethodsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(InlineDlfcnMethods)' != ''" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" Condition="'$(PrepareAssemblies)' != 'true'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreMarkDispatcher" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(Registrar)' == 'trimmable-static'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineClassGetHandleStep" Condition="'$(InlineClassGetHandle)' != '' And '$(InlineClassGetHandle)' != 'disabled'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreMarkDispatcher" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And ('$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static')" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(Registrar)' == 'trimmable-static'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineClassGetHandleStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(InlineClassGetHandle)' != '' And '$(InlineClassGetHandle)' != 'disabled'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveBlockCodeHandler" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' != 'true'" Type="Xamarin.Linker.OptimizeGeneratedCodeHandler" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.BackingFieldDelayHandler" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' != 'true'" Type="Xamarin.Linker.MarkIProtocolHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveBlockCodeHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' != 'true'" Type="Xamarin.Linker.OptimizeGeneratedCodeHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.BackingFieldDelayHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' != 'true'" Type="Xamarin.Linker.MarkIProtocolHandler" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkDispatcher)' != 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkDispatcher)' != 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(Registrar)' == 'managed-static'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(Registrar)' == 'managed-static'" /> - <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" AfterStep="SweepStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PostSweepDispatcher" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" AfterStep="SweepStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PostSweepDispatcher" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(PrepareAssemblies)' == 'true' And ('$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static')" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(PrepareAssemblies)' == 'true' And '$(Registrar)' == 'trimmable-static'" /> + <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(PrepareAssemblies)' == 'true' And '$(Registrar)' == 'managed-static'" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.RegistrarStep" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.GenerateMainStep" /> <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.GenerateReferencesStep" /> @@ -1328,6 +1338,9 @@ <_TypeMapFilePath Condition="'$(_TypeMapFilePath)' == ''">$(DeviceSpecificIntermediateOutputPath)type-map.txt + + + <_UnmanagedCallersOnlyMapPath Condition="'$(_UnmanagedCallersOnlyMapPath)' == ''">$(DeviceSpecificIntermediateOutputPath)unmanaged_callers_only_map.txt @@ -1413,13 +1426,27 @@ - - - - + + + + + + + + + + + <_IntermediateAssemblyProperty>@(IntermediateAssembly) + <_PreparedIntermediateAssemblyProperty>@(PreparedIntermediateAssembly->WithMetadataValue('BeforePrepareAssembliesPath','$(_IntermediateAssemblyProperty)')) + + + <_PreparedRootedIntermediateAssembly Include="@(TrimmerRootAssembly->'$(_PreparedIntermediateAssemblyProperty)')" Condition="'%(Identity)' == '$(_IntermediateAssemblyProperty)'" /> + + + + diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx index 5de6bb55b800..0741be004f37 100644 --- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx +++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx @@ -1661,4 +1661,9 @@ Shown when the tool reports a simulator runtime version mismatch. {0} - The platform name (e.g. "iOS" or "tvOS"). + + Console.StandardOutput or Console.StandardError was accessed during a build task. This should not happen, use the MSBuild logging infrastructure instead. Stack trace: {0} + Shown when an MSBuild task writes to the console instead of using MSBuild logging. +{0} - The stack trace of the offending call. + diff --git a/msbuild/Xamarin.MacDev.Tasks.slnx b/msbuild/Xamarin.MacDev.Tasks.slnx index e3c799a54619..1f17eb9aa95f 100644 --- a/msbuild/Xamarin.MacDev.Tasks.slnx +++ b/msbuild/Xamarin.MacDev.Tasks.slnx @@ -36,4 +36,8 @@ + + + + diff --git a/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs b/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs new file mode 100644 index 000000000000..1e97dc541697 --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Text; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +#nullable enable + +namespace Xamarin.Utils; + +class ConsoleToTaskWriter : TextWriter { + TaskLoggingHelper helper; + bool errorShown; + + public ConsoleToTaskWriter (TaskLoggingHelper helper) + { + this.helper = helper; + } + + public override Encoding Encoding => Encoding.UTF8; + + public override void Write (char value) + { + ShowError (); + helper.LogMessage (MessageImportance.Low, value.ToString ()); + } + + public override void Write (char [] buffer, int index, int count) + { + ShowError (); + helper.LogMessage (MessageImportance.Low, new string (buffer, index, count)); + } + + public override void Write (string? value) + { + ShowError (); + helper.LogMessage (MessageImportance.Low, value ?? string.Empty); + } + + public override void WriteLine () + { + ShowError (); + } + + public override void WriteLine (string? value) + { + ShowError (); + helper.LogMessage (MessageImportance.Low, value ?? string.Empty); + } + + void ShowError () + { + if (errorShown) + return; + errorShown = true; + + helper.LogError (null, "MT7178" /* Console.StandardOutput or Console.StandardError was accessed during a build task. This should not happen, use the MSBuild logging infrastructure instead. Stack trace: {0} */, null, null, 0, 0, 0, 0, Xamarin.Localization.MSBuild.MSBStrings.E7178, Environment.StackTrace); + } + + public static IDisposable EnsureNoConsoleUsage (TaskLoggingHelper log) + { + return new NoConsoleUsage (new ConsoleToTaskWriter (log)); + } + + class NoConsoleUsage : IDisposable { + TextWriter? originalStdout; + TextWriter? originalStderr; + + public NoConsoleUsage (ConsoleToTaskWriter redirector) + { + originalStdout = Console.Out; + originalStderr = Console.Error; + Console.SetOut (redirector); + Console.SetError (redirector); + } + + ~NoConsoleUsage () + { + Restore (); + } + + void IDisposable.Dispose () + { + Restore (); + GC.SuppressFinalize (this); + } + + void Restore () + { + if (originalStdout is not null) { + Console.SetOut (originalStdout); + originalStdout = null; + } + if (originalStderr is not null) { + Console.SetError (originalStderr); + originalStderr = null; + } + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs b/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs deleted file mode 100644 index 8bda5c48e690..000000000000 --- a/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2021, Microsoft Corp. All rights reserved, - -using System.Collections.Generic; - -using Xamarin.Utils; - -#nullable enable - -namespace Xamarin.Bundler { - public static partial class ErrorHelper { - public static ApplePlatform Platform; - - internal static string GetPrefix (IToolLog? log) - { - return Xamarin.MacDev.Tasks.LoggingExtensions.ErrorPrefix; - } - - public enum WarningLevel { - Error = -1, - Warning = 0, - Disable = 1, - } - - static Dictionary? warning_levels; - - public static WarningLevel GetWarningLevel (IToolLog log, int code) - { - WarningLevel level; - - if (warning_levels is null) - return WarningLevel.Warning; - - // code -1: all codes - if (warning_levels.TryGetValue (-1, out level)) - return level; - - if (warning_levels.TryGetValue (code, out level)) - return level; - - return WarningLevel.Warning; - } - - public static void SetWarningLevel (WarningLevel level, int? code = null /* if null, apply to all warnings */) - { - if (warning_levels is null) - warning_levels = new Dictionary (); - if (code.HasValue) { - warning_levels [code.Value] = level; - } else { - warning_levels [-1] = level; // code -1: all codes. - } - } - } -} diff --git a/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs b/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs index 367f3f5bd46a..2f0f9b7a5538 100644 --- a/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs +++ b/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs @@ -74,6 +74,19 @@ public static void LogTaskProperty (this TaskLoggingHelper log, string propertyN log.LogMessage (TaskPropertyImportance, " {0}: {1}", propertyName, value); } + /// + /// Creates an MSBuild error following our MTErrors convention. + /// + /// For every new error we need to update "docs/website/mtouch-errors.md" and "tools/mtouch/error.cs". + /// In the 7xxx range for MSBuild error. + /// The error's message to be displayed in the error pad. + /// Path to the known guilty file or null. + /// Line number in the file where the error was found, or 0 if unknown. + public static void LogError (this TaskLoggingHelper log, int errorCode, string? fileName, int lineNumber, string message, params object? [] args) + { + log.LogError (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, message, args); + } + /// /// Creates an MSBuild error following our MTErrors convention. /// @@ -91,6 +104,16 @@ public static void LogWarning (this TaskLoggingHelper log, int errorCode, string log.LogWarning (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", 0, 0, 0, 0, message, args); } + public static void LogWarning (this TaskLoggingHelper log, int errorCode, string? fileName, int lineNumber, string message, params object? [] args) + { + log.LogWarning (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, message, args); + } + + public static void LogMessage (this TaskLoggingHelper log, MessageImportance importance, int errorCode, string? fileName, int lineNumber, string message, params object? [] args) + { + log.LogMessage (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, importance, message, args); + } + public static bool LogErrorsFromException (this TaskLoggingHelper log, Exception exception, bool showStackTrace = true, bool showDetail = true) { var exceptions = new List (); diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs new file mode 100644 index 000000000000..8be422665932 --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +using Xamarin.Build; +using Xamarin.Bundler; +using Xamarin.Utils; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + public class PrepareAssemblies : XamarinTask { + const string ErrorPrefix = "MX"; + + #region Inputs + [Required] + public ITaskItem [] InputAssemblies { get; set; } = []; + + public string MakeReproPath { get; set; } = ""; + + public string OutputDirectory { get; set; } = ""; + + [Required] + public ITaskItem? OptionsFile { get; set; } + #endregion + + #region Outputs + [Output] + public ITaskItem [] OutputAssemblies { get; set; } = []; + #endregion + + Dictionary map = new (); + + AssemblyPreparerInfo GetAssemblyInfo (ITaskItem item) + { + var inputPath = item.ItemSpec; + var outputPath = Path.Combine (OutputDirectory, Path.GetFileName (inputPath)); + var isTrimmableString = item.GetMetadata ("IsTrimmable"); + var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase); + var trimMode = item.GetMetadata ("TrimMode"); + var rv = new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode); + map [rv] = item; + return rv; + } + + public override bool Execute () + { + // Capture Console usage and show an error if anything uses Console.[Error.]Write* + using var consoleToLog = ConsoleToTaskWriter.EnsureNoConsoleUsage (Log); + + try { + var infos = InputAssemblies.Select (GetAssemblyInfo).ToArray (); + using var preparer = new AssemblyPreparer (this, infos, OptionsFile?.ItemSpec ?? ""); + preparer.MakeReproPath = MakeReproPath; + var rv = preparer.Prepare (out var exceptions); + + foreach (var pe in exceptions) { + if (pe.IsError (this)) { + ((IToolLog) this).LogError (pe); + } else { + ((IToolLog) this).LogWarning (pe); + } + } + + var outputAssemblies = preparer.Assemblies.Select (v => { + var item = new TaskItem (v.OutputPath); + map [v].CopyMetadataTo (item); + item.SetMetadata ("BeforePrepareAssembliesPath", v.InputPath); + return (ITaskItem) item; + }).ToList (); + + outputAssemblies.AddRange (preparer.AddedAssemblies.Select (v => { + var rv = new TaskItem (v.Path); + rv.SetMetadata ("PostprocessAssembly", "true"); + rv.SetMetadata ("RelativePath", preparer.Configuration.AssemblyPublishDir + Path.GetFileName (v.Path)); + if (v.OriginatingAssembly is not null) { + var originatingItem = map.SingleOrDefault (kvp => Path.GetFileName (kvp.Key.InputPath) == Path.GetFileName (v.OriginatingAssembly)).Value; + if (originatingItem is null) { + Log.LogMessage (MessageImportance.Low, $"Could not find originating assembly for {v.Path} with originating assembly name {v.OriginatingAssembly}"); + } else { + var metadata = originatingItem.MetadataNames.Cast ().ToList (); + if (metadata.Contains ("TrimMode")) + rv.SetMetadata ("TrimMode", originatingItem.GetMetadata ("TrimMode")); + if (metadata.Contains ("IsTrimmable")) + rv.SetMetadata ("IsTrimmable", originatingItem.GetMetadata ("IsTrimmable")); + } + } + return rv; + })); + + OutputAssemblies = outputAssemblies.ToArray (); + return rv && !Log.HasLoggedErrors; + } catch (Exception e) { + ((IToolLog) this).LogException (e); + return false; + } + } + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs index 3cd5a9386821..e50cee0ce94a 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs @@ -13,7 +13,6 @@ using Xamarin.Localization.MSBuild; using Xamarin.Messaging.Build.Client; using Xamarin.Utils; -using static Xamarin.Bundler.FileCopier; #nullable enable @@ -246,7 +245,7 @@ internal static bool ExecuteRemotely (T task) where T : Task, IHasSessionId } #if NET - internal static bool ExecuteRemotely (T task, [NotNullWhen (true)] out TaskRunner? taskRunner, Action? preprocessTaskRunner = null) where T: Task, IHasSessionId + internal static bool ExecuteRemotely (T task, [NotNullWhen (true)] out TaskRunner? taskRunner, Action? preprocessTaskRunner = null) where T : Task, IHasSessionId #else internal static bool ExecuteRemotely (T task, out TaskRunner taskRunner, Action? preprocessTaskRunner = null) where T : Task, IHasSessionId #endif @@ -371,8 +370,11 @@ void ICustomLogger.LogError (string message, Exception? ex) { if (!string.IsNullOrEmpty (message)) Log.LogError (message); - if (ex is not null) + if (ex is ProductException pe) { + LogDiagnostic (pe); + } else if (ex is not null) { Log.LogErrorFromException (ex); + } } void ICustomLogger.LogWarning (string messageFormat, params object? [] args) @@ -404,12 +406,37 @@ void IToolLog.LogError (string message) void IToolLog.LogException (Exception exception) { - ((ICustomLogger) this).LogError ("", exception); + if (exception is ProductException pe) { + LogDiagnostic (pe); + } else { + ((ICustomLogger) this).LogError ($"Unexpected exception '{GetType ().Name}': {exception.Message}", exception); + } + } + + void IToolLog.LogError (ProductException exception) + { + LogDiagnostic (exception); } - void IToolLog.LogError (Exception exception) + void IToolLog.LogWarning (ProductException exception) { - ((ICustomLogger) this).LogError ("", exception); + LogDiagnostic (exception); + } + + protected void LogDiagnostic (ProductException exception) + { + switch (exception.GetWarningLevel (this)) { + case ErrorHelper.WarningLevel.Warning: + Log.LogWarning (exception.Code, exception.FileName, exception.LineNumber, exception.Message); + break; + case ErrorHelper.WarningLevel.Error: + Log.LogError (exception.Code, exception.FileName, exception.LineNumber, exception.Message); + break; + case ErrorHelper.WarningLevel.Disable: + default: + Log.LogMessage (MessageImportance.Low, exception.Code, exception.FileName, exception.LineNumber, exception.Message); + break; + } } #endregion } diff --git a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj index 713db306ebef..cbd090125523 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj +++ b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj @@ -1,7 +1,8 @@ - netstandard2.0;net$(BundledNETCoreAppTargetFrameworkVersion) + net$(BundledNETCoreAppTargetFrameworkVersion);netstandard2.0 + net$(BundledNETCoreAppTargetFrameworkVersion) false compile true @@ -37,6 +38,7 @@ + ProjectReference @@ -69,45 +71,15 @@ - - ApplePlatform.cs - - JsonExtensions.cs - - - IToolLog.cs + external\JsonExtensions.cs StringUtils.cs - - Symbols.cs - - - FileCopier.cs - - - TargetFramework.cs - - - Execution.cs - - - PathUtils.cs - PListExtensions.cs - - MachO.cs - - - error.cs - - - ErrorHelper.cs - external\RuntimeException.cs diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 8307a85ebf48..84749fe8f117 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -84,6 +84,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. + @@ -3395,6 +3396,54 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + false + + + + <_PrepareAssembliesForPreparationDependsOn> + _ComputeAssembliesToPostprocessOnPublish; + _ComputeLinkerInputs; + + + + + + <_AssembliesToPrepare Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.PostprocessAssembly)' == 'true'" /> + + + $([MSBuild]::EnsureTrailingSlash('$(DeviceSpecificIntermediateOutputPath)prepared-assemblies')) + + + + + + + + + + + + + + + + diff --git a/src/ObjCRuntime/Registrar.core.cs b/src/ObjCRuntime/Registrar.core.cs index 958a17174874..86cd2436e8d0 100644 --- a/src/ObjCRuntime/Registrar.core.cs +++ b/src/ObjCRuntime/Registrar.core.cs @@ -8,7 +8,11 @@ abstract partial class Registrar { [return: NotNullIfNotNull (nameof (getterSelector))] internal static string? CreateSetterSelector (string? getterSelector) { +#if NET if (string.IsNullOrEmpty (getterSelector)) +#else + if (string.IsNullOrEmpty (getterSelector) || getterSelector is null) +#endif return getterSelector; var first = (int) getterSelector [0]; @@ -21,7 +25,11 @@ abstract partial class Registrar { [return: NotNullIfNotNull (nameof (name))] public static string? SanitizeObjectiveCName (string? name) { +#if NET if (string.IsNullOrEmpty (name)) +#else + if (string.IsNullOrEmpty (name) || name is null) +#endif return name; StringBuilder? sb = null; diff --git a/src/ObjCRuntime/Registrar.cs b/src/ObjCRuntime/Registrar.cs index d33f63746e84..303004248263 100644 --- a/src/ObjCRuntime/Registrar.cs +++ b/src/ObjCRuntime/Registrar.cs @@ -220,7 +220,11 @@ public void VerifyRegisterAttribute ([NotNullIfNotNull (nameof (exceptions))] re return; var name = RegisterAttribute.Name; +#if NET if (string.IsNullOrEmpty (name)) +#else + if (string.IsNullOrEmpty (name) || name is null) +#endif return; for (int i = 0; i < name.Length; i++) { @@ -540,7 +544,11 @@ abstract internal class ObjCMember { public bool SetExportAttribute (ExportAttribute ea, [NotNullIfNotNull (nameof (exceptions))] ref List? exceptions) { +#if NET if (string.IsNullOrEmpty (ea.Selector)) { +#else + if (string.IsNullOrEmpty (ea.Selector) || ea.Selector is null) { +#endif AddException (ref exceptions, Registrar.CreateException (4135, this, Errors.MT4135, FullName)); return false; } @@ -2181,7 +2189,11 @@ void FlattenInterfaces (TType? [] ifaces) objcType.Add (objcGetter, ref exceptions); +#if NET if (!string.IsNullOrEmpty (attrib.SetterSelector)) { +#else + if (!string.IsNullOrEmpty (attrib.SetterSelector) && attrib.SetterSelector is not null) { +#endif var objcSetter = new ObjCMethod (this, objcType, null) { Name = attrib.Name, Selector = attrib.SetterSelector, diff --git a/src/bgen/BindingTouch.cs b/src/bgen/BindingTouch.cs index 7b9e2d32dadb..2744ae3f2547 100644 --- a/src/bgen/BindingTouch.cs +++ b/src/bgen/BindingTouch.cs @@ -587,7 +587,12 @@ public void LogError (string message) Console.Error.WriteLine (message); } - public void LogError (Exception exception) + public void LogError (BindingException exception) + { + ErrorHelper.Show (exception); + } + + public void LogWarning (BindingException exception) { ErrorHelper.Show (exception); } diff --git a/tests/assembly-preparer/BaseClass.cs b/tests/assembly-preparer/BaseClass.cs new file mode 100644 index 000000000000..93e3f02a0011 --- /dev/null +++ b/tests/assembly-preparer/BaseClass.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AssemblyPreparerTests; + +public abstract class BaseClass { + public void AssertPrepare (AssemblyPreparer preparer) + { + if (!preparer.Prepare (out var exceptions)) { + foreach (var ex in exceptions) { + Console.WriteLine (ex.ToString ()); + if (ex.InnerException is not null) + Console.WriteLine ($" Inner: {ex.InnerException}"); + } + Assert.Fail ($"Prepare failed, exceptions:\n\t{string.Join ("\n\t", exceptions.Select (v => v.ToString ()))}"); + } + Assert.That (exceptions, Is.Empty, "Exceptions"); + } + + public bool AssertPrepare (ApplePlatform platform, bool isCoreCLR, string code, out AssemblyDefinition assemblyDefinition) + { + return AssertPrepare (platform, isCoreCLR, RegistrarMode.Dynamic, code, out assemblyDefinition); + } + + // returns true if the test assembly was modified + public bool AssertPrepare (ApplePlatform platform, bool isCoreCLR, RegistrarMode registrar, string code, out AssemblyDefinition assemblyDefinition) + { + AssemblyPreparer? preparer = null; + var rv = AssertPrepareCode (platform, isCoreCLR, p => { + p.Registrar = registrar; + preparer = p; + }, code, out string outputPath); + var resolver = new DefaultAssemblyResolver (); + var dirs = preparer!.Assemblies.Select (v => Path.GetDirectoryName (v.OutputPath)).Distinct ().ToList (); + dirs.ForEach (v => resolver.AddSearchDirectory (v)); + var readerParameters = new ReaderParameters { + ReadSymbols = true, + AssemblyResolver = resolver, + }; + assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath, readerParameters); + return rv; + } + + // returns true if the test assembly was modified + public bool AssertPrepareCode (ApplePlatform platform, bool isCoreCLR, Action? configure, string code, out string outputPath) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + var csproj = $@" + + + net$(BundledNETCoreAppTargetFrameworkVersion)-{platform.AsString ().ToLower ()} + false + true + + + "; + + var tmpdir = Xamarin.Cache.CreateTemporaryDirectory (); + + var config = $@" + AreAnyAssembliesTrimmed=true + PublishTrimmed=true + IntermediateOutputPath={Path.Combine (tmpdir, "intermediate")} + Platform={platform.AsString ()} + PlatformAssembly=Microsoft.{platform.AsString ()}.dll + SdkDevPath={Configuration.XcodeLocation} + SdkVersion={Configuration.GetSdkVersion (platform)} + TargetFramework={TargetFramework.GetTargetFramework (platform)} + "; + var configpath = Path.Combine (tmpdir, "config.txt"); + File.WriteAllText (configpath, config); + + File.WriteAllText (Path.Combine (tmpdir, "Test.cs"), code); + var csprojPath = Path.Combine (tmpdir, "Test.csproj"); + File.WriteAllText (csprojPath, csproj); + var properties = new Dictionary { + { "TreatWarningsAsErrors", "false" }, + }; + DotNet.AssertBuild (csprojPath, properties); + var assemblyDir = Path.Combine (tmpdir, "bin", "Debug"); + + var assemblies = Configuration.GetImplementationAssemblies (platform, isCoreCLR); + assemblies.Add (Path.Combine (assemblyDir, "Test.dll")); + var infos = assemblies.Select (v => new AssemblyPreparerInfo (v, Path.Combine (assemblyDir, "out", Path.GetFileName (v)), true, "link")).ToArray (); + var logger = new TestLogger () { Platform = platform }; + var preparer = new AssemblyPreparer (logger, infos, configpath); + if (configure is not null) + configure (preparer); + AssertPrepare (preparer); + + var testInfo = infos.Single (v => Path.GetFileNameWithoutExtension (v.InputPath) == "Test"); + outputPath = testInfo.OutputPath; + Console.WriteLine ("Output assembly: " + outputPath); + preparer.Dispose (); + return testInfo.InputPath != testInfo.OutputPath; + } +} diff --git a/tests/assembly-preparer/GlobalUsings.cs b/tests/assembly-preparer/GlobalUsings.cs new file mode 100644 index 000000000000..4e06615734f1 --- /dev/null +++ b/tests/assembly-preparer/GlobalUsings.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.IO; +global using System.Runtime.InteropServices; + +global using Mono.Cecil; +global using NUnit.Framework; + +global using Xamarin; +global using Xamarin.Bundler; +global using Xamarin.Build; +global using Xamarin.Tests; +global using Xamarin.Utils; + +// These tests are rather memory hungry, so running them in parallel really makes my machine crawl. +// [assembly: Parallelizable (ParallelScope.Children)] diff --git a/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs b/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs new file mode 100644 index 000000000000..fa0d1f66f6a4 --- /dev/null +++ b/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace AssemblyPreparerTests; + +public class InlineDlfcnMethodsStepTests : BaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void MarkedTest (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using CoreAnimation; + using Foundation; + using ObjCRuntime; + + class MyClass : NSObject { + void GetIntPtr () + { + Console.WriteLine (Dlfcn.GetIntPtr (0, ""NativeSymbol"")); + } + }"; + + AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition); + + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var platformReference = assemblyDefinition.MainModule.AssemblyReferences.Single (v => v.Name == $"Microsoft.{platform.AsString ()}"); + var platformAssembly = assemblyDefinition.MainModule.AssemblyResolver.Resolve (platformReference); + var dlfcn = platformAssembly.MainModule.Types.Single (v => v.Name == "Dlfcn"); + + var cctor = type.GetStaticConstructor (); + Assert.That (cctor, Is.Null, "No static constructor should be needed."); + + void AssertHasDlfcnPInvokeCall (MethodDefinition method) + { + var instructions = method.Body.Instructions; + var call = instructions.FirstOrDefault (v => v.OpCode == OpCodes.Call && v.Operand is MethodReference mr && mr.DeclaringType.FullName == dlfcn.FullName); + Assert.That (call, Is.Not.Null, $"Expected a call to Dlfcn in {method}"); + var resolvedMethod = ((MethodReference) call.Operand).Resolve (); + Assert.That (resolvedMethod, Is.Not.Null, $"Expected the call to resolve to a method in Dlfcn for {method}"); + Assert.That (resolvedMethod.PInvokeInfo, Is.Null, $"Expected the method to not be a PInvoke method for {method}"); + } + + Assert.Multiple (() => { + AssertHasDlfcnPInvokeCall (type.Methods.Single (v => v.Name == "GetIntPtr")); + }); + } +} diff --git a/tests/assembly-preparer/Makefile b/tests/assembly-preparer/Makefile new file mode 100644 index 000000000000..b01174cb5734 --- /dev/null +++ b/tests/assembly-preparer/Makefile @@ -0,0 +1,8 @@ +TOP=../.. +include $(TOP)/Make.config + +build: + $(DOTNET) build *.csproj + +run-tests: + $(DOTNET) test *.csproj diff --git a/tests/assembly-preparer/MarkIProtocolHandlerTests.cs b/tests/assembly-preparer/MarkIProtocolHandlerTests.cs new file mode 100644 index 000000000000..fb7f0fcd41dd --- /dev/null +++ b/tests/assembly-preparer/MarkIProtocolHandlerTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Mono.Cecil.Rocks; + +namespace AssemblyPreparerTests; + +public class MarkIProtocolHandlerTests : BaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void DynamicRegistrar (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using Foundation; + using ObjCRuntime; + + [Protocol] + interface IProtocol { + } + + class MyClass : NSObject, IProtocol { + } + "; + + AssertPrepare (platform, isCoreCLR, RegistrarMode.Dynamic, code, out var assemblyDefinition); + + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var cctor = type.GetStaticConstructor (); + Assert.That (cctor, Is.Not.Null, "Expected a static constructor to be added"); + var attribs = cctor.CustomAttributes?.Where (v => v.AttributeType.Name == "DynamicDependencyAttribute").OrderBy (v => string.Join (", ", v.ConstructorArguments.Select (v => v.Value?.ToString ()))).ToArray (); + Assert.That (attribs, Is.Not.Null, "Attributes"); + Assert.That (attribs.Count, Is.EqualTo (1), "Attribute count"); + // PreserveProtocolsStep adds DDA(DynamicallyAccessedMemberTypes.Interfaces, typeof(MyClass)) + Assert.That ((int) attribs [0].ConstructorArguments [0].Value, Is.EqualTo (0x2000), "First attribute's first argument (DynamicallyAccessedMemberTypes.Interfaces)"); + Assert.That (((TypeReference) attribs [0].ConstructorArguments [1].Value).FullName, Is.EqualTo ("MyClass"), "First attribute's second argument"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void ManagedStaticRegistrar (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using Foundation; + using ObjCRuntime; + + [Protocol] + interface IProtocol { + } + + class MyClass : NSObject, IProtocol { + } + "; + + // With ManagedStatic registrar, PreserveProtocolsStep doesn't run, + // so MyClass's cctor should not have any DDA for protocol preservation. + AssertPrepare (platform, isCoreCLR, RegistrarMode.ManagedStatic, code, out var assemblyDefinition); + + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var cctor = type.GetStaticConstructor (); + if (cctor is not null) { + var ddaAttribs = cctor.CustomAttributes?.Where (v => v.AttributeType.Name == "DynamicDependencyAttribute").ToArray (); + Assert.That (ddaAttribs, Is.Empty, "No DynamicDependencyAttributes expected on MyClass's cctor"); + } + } +} diff --git a/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs b/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs new file mode 100644 index 000000000000..5a5ee83d47e4 --- /dev/null +++ b/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace AssemblyPreparerTests; + +public class OptimizeGeneratedCodeHandlerTests : BaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void RemoveEnsureUIThread (ApplePlatform platform, bool isCoreCLR) + { + var ensureUIThreadCall = platform == ApplePlatform.MacOSX + ? "AppKit.NSApplication.EnsureUIThread ();" + : "UIKit.UIApplication.EnsureUIThread ();"; + + var usingDirective = platform == ApplePlatform.MacOSX + ? "using AppKit;" + : "using UIKit;"; + + var code = $@" + using System; + using Foundation; + using ObjCRuntime; + {usingDirective} + + class MyClass : NSObject {{ + [BindingImpl (BindingImplOptions.Optimizable)] + [Export (""myMethod"")] + public void MyMethod () {{ + {ensureUIThreadCall} + }} + }}"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.RemoveUIThreadChecks = true; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var method = type.Methods.Single (v => v.Name == "MyMethod"); + + var hasEnsureUIThread = method.Body.Instructions.Any (i => + i.OpCode.Code == Code.Call && + (i.Operand as MethodReference)?.Name == "EnsureUIThread"); + Assert.That (hasEnsureUIThread, Is.False, "EnsureUIThread call should be removed"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void KeepEnsureUIThreadWhenOptimizationDisabled (ApplePlatform platform, bool isCoreCLR) + { + var ensureUIThreadCall = platform == ApplePlatform.MacOSX + ? "AppKit.NSApplication.EnsureUIThread ();" + : "UIKit.UIApplication.EnsureUIThread ();"; + + var usingDirective = platform == ApplePlatform.MacOSX + ? "using AppKit;" + : "using UIKit;"; + + var code = $@" + using System; + using Foundation; + using ObjCRuntime; + {usingDirective} + + class MyClass : NSObject {{ + [BindingImpl (BindingImplOptions.Optimizable)] + [Export (""myMethod"")] + public void MyMethod () {{ + {ensureUIThreadCall} + }} + }}"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.RemoveUIThreadChecks = false; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var method = type.Methods.Single (v => v.Name == "MyMethod"); + + var hasEnsureUIThread = method.Body.Instructions.Any (i => + i.OpCode.Code == Code.Call && + (i.Operand as MethodReference)?.Name == "EnsureUIThread"); + Assert.That (hasEnsureUIThread, Is.True, "EnsureUIThread call should be preserved when optimization is disabled"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void OptimizeProtocolInterfaceStaticConstructor (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using Foundation; + using ObjCRuntime; + + [Protocol] + interface IMyProtocol { + [BindingImpl (BindingImplOptions.Optimizable)] + static IMyProtocol () { + GC.KeepAlive (null); + } + } + + class MyClass : NSObject, IMyProtocol { + }"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.RegisterProtocols = true; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "IMyProtocol"); + var cctor = type.GetStaticConstructor (); + + Assert.That (cctor, Is.Not.Null, "Static constructor should still exist"); + Assert.That (cctor.Body.Instructions.Count, Is.EqualTo (1), "Static constructor should have only a ret instruction"); + Assert.That (cctor.Body.Instructions [0].OpCode.Code, Is.EqualTo (Code.Ret), "Static constructor should only contain ret"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void KeepProtocolStaticConstructorWhenOptimizationDisabled (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using Foundation; + using ObjCRuntime; + + [Protocol] + interface IMyProtocol { + [BindingImpl (BindingImplOptions.Optimizable)] + static IMyProtocol () { + GC.KeepAlive (null); + } + } + + class MyClass : NSObject, IMyProtocol { + }"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.RegisterProtocols = false; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "IMyProtocol"); + var cctor = type.GetStaticConstructor (); + + Assert.That (cctor, Is.Not.Null, "Static constructor should still exist"); + Assert.That (cctor.Body.Instructions.Count, Is.GreaterThan (1), "Static constructor should not be optimized"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void NoOptimizationWithoutBindingAttributes (ApplePlatform platform, bool isCoreCLR) + { + var ensureUIThreadCall = platform == ApplePlatform.MacOSX + ? "AppKit.NSApplication.EnsureUIThread ();" + : "UIKit.UIApplication.EnsureUIThread ();"; + + var usingDirective = platform == ApplePlatform.MacOSX + ? "using AppKit;" + : "using UIKit;"; + + var code = $@" + using System; + using Foundation; + using ObjCRuntime; + {usingDirective} + + class MyClass : NSObject {{ + [Export (""myMethod"")] + public void MyMethod () {{ + {ensureUIThreadCall} + }} + }}"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.RemoveUIThreadChecks = true; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var method = type.Methods.Single (v => v.Name == "MyMethod"); + + var hasEnsureUIThread = method.Body.Instructions.Any (i => + i.OpCode.Code == Code.Call && + (i.Operand as MethodReference)?.Name == "EnsureUIThread"); + Assert.That (hasEnsureUIThread, Is.True, "EnsureUIThread call should be preserved without [BindingImpl(Optimizable)]"); + } + + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void DeadCodeElimination (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using Foundation; + using ObjCRuntime; + + class MyClass : NSObject { + [BindingImpl (BindingImplOptions.Optimizable)] + [Export (""myMethod"")] + public int MyMethod () { + if (true) { + return 1; + } + return 2; + } + }"; + + AssertPrepareCode (platform, isCoreCLR, preparer => { + preparer.Registrar = RegistrarMode.Dynamic; + preparer.Optimizations.DeadCodeElimination = true; + }, code, out var outputPath); + + using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath); + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var method = type.Methods.Single (v => v.Name == "MyMethod"); + + // After dead code elimination, there should be no ldc.i4.2 (the unreachable return 2) + var hasDeadCode = method.Body.Instructions.Any (i => + i.OpCode.Code == Code.Ldc_I4_2); + Assert.That (hasDeadCode, Is.False, "Dead code (return 2) should be eliminated"); + } +} diff --git a/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs b/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs new file mode 100644 index 000000000000..ad39cd811c5c --- /dev/null +++ b/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Mono.Cecil.Rocks; + +namespace AssemblyPreparerTests; + +public class PreserveBlockCodeHandlerTests : BaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void MarkedTest (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using ObjCRuntime; + namespace ObjCRuntime; + class Trampolines { + static internal class SDInnerBlock { + // this field is not preserved by other means, but it must not be linked away + static internal readonly DInnerBlock Handler = Invoke; + + [MonoPInvokeCallback (typeof (DInnerBlock))] + static internal void Invoke (IntPtr block, int magic_number) + { + } + + public delegate void DInnerBlock (IntPtr block, int magic_number); + } + }"; + + AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition); + + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "Trampolines").NestedTypes.Single (v => v.Name == "SDInnerBlock"); + var cctor = type.GetStaticConstructor (); + var attribs = cctor.CustomAttributes?.OrderBy (v => string.Join (", ", v.ConstructorArguments.Select (v => v.Value?.ToString ()))).ToArray (); + Assert.That (attribs, Is.Not.Null, "Attributes"); + Assert.That (attribs.Count, Is.EqualTo (2), "Attribute count"); + Assert.That (attribs.All (v => v.AttributeType.Name == "DynamicDependencyAttribute"), Is.True, "Attribute name"); + // Handler field: DDA(string memberSignature, string typeName, string assemblyName) + Assert.That (attribs [0].ConstructorArguments.Count, Is.EqualTo (3), "First attribute's argument count"); + Assert.That ((string) attribs [0].ConstructorArguments [0].Value, Is.EqualTo ("Handler"), "First attribute's first argument"); + Assert.That ((string) attribs [0].ConstructorArguments [1].Value, Is.EqualTo ("ObjCRuntime.Trampolines.SDInnerBlock"), "First attribute's second argument"); + Assert.That ((string) attribs [0].ConstructorArguments [2].Value, Is.EqualTo ("Test"), "First attribute's third argument"); + + // Invoke method: DDA(string memberSignature) - same declaring type, so simpler constructor + Assert.That (attribs [1].ConstructorArguments.Count, Is.EqualTo (1), "Second attribute's argument count"); + Assert.That ((string) attribs [1].ConstructorArguments [0].Value, Is.EqualTo ("Invoke(System.IntPtr,System.Int32)"), "Second attribute's first argument"); + } +} diff --git a/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs b/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs new file mode 100644 index 000000000000..7c690caa9037 --- /dev/null +++ b/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Mono.Cecil.Rocks; + +namespace AssemblyPreparerTests; + +public class PreserveSmartEnumConversionsTests : BaseClass { + [Test] + [TestCase (ApplePlatform.MacCatalyst, false)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + public void MarkedTest (ApplePlatform platform, bool isCoreCLR) + { + var code = @" + using System; + using CoreAnimation; + using Foundation; + using ObjCRuntime; + + class MyClass : NSObject { + [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] + public CAToneMapMode RWProperty { get; set; } + + [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] + public CAToneMapMode ROProperty { get { return default; } } + + [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] + public CAToneMapMode WOProperty { set { } } + + [return: BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] + public CAToneMapMode Method1 () { return default; } + + public void Method2 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1) {} + + public void Method3 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1, [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p2) {} + + [return: BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] + public CAToneMapMode Method4 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1, [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p2) { return default;} + }"; + + AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition); + + var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass"); + var cctor = type.GetStaticConstructor (); + Assert.That (cctor, Is.Null, "No static constructor should be needed."); + + void AssertHasDynamicDependency (ICustomAttributeProvider provider, string memberSignature, string typeName, string assemblyName) + { + var ddaAttributes = provider.CustomAttributes.Where (v => v.AttributeType.FullName == "System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute").ToArray (); + var found = 0; + foreach (var ca in ddaAttributes) { + if (ca.ConstructorArguments.Count != 3) + continue; + var attribMemberSignature = (string) ca.ConstructorArguments [0].Value!; + if (attribMemberSignature != memberSignature) + continue; + var attribTypeName = (string) ca.ConstructorArguments [1].Value!; + if (attribTypeName != typeName) + continue; + var attribAssemblyName = (string) ca.ConstructorArguments [2].Value!; + if (attribAssemblyName != assemblyName) + continue; + + found++; + } + + if (found == 1) + return; + + var attributesAsString = ddaAttributes + .Select (v => { + switch (v.ConstructorArguments.Count) { + case 3: + return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\", \"{v.ConstructorArguments [1].Value}\", \"{v.ConstructorArguments [2].Value}\")]"; + case 2: + return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\", \"{v.ConstructorArguments [1].Value}\")]"; + case 1: + return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\")]"; + default: + return string.Join (", ", v.ConstructorArguments.Select (x => x.Value?.ToString () ?? "null")); + } + }) + .OrderBy (v => v) + .ToArray (); + + string msg; + if (found == 0) { + if (attributesAsString.Length == 0) { + msg = $"Expected [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got no attributes."; + } else { + msg = $"Expected [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got:\n\t{string.Join ("\n\t", attributesAsString)}"; + } + } else { + msg = $"Expected exactly one [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got {found}:\n\t{string.Join ("\n\t", attributesAsString)}"; + } + Console.WriteLine (msg); + Assert.Fail (msg); + } + + void AssertHasDynamicDependencies (ICustomAttributeProvider provider) + { + AssertHasDynamicDependency (provider, "GetConstant(CoreAnimation.CAToneMapMode)", "CoreAnimation.CAToneMapModeExtensions", $"Microsoft.{platform.AsString ()}"); + AssertHasDynamicDependency (provider, "GetValue(Foundation.NSString)", "CoreAnimation.CAToneMapModeExtensions", $"Microsoft.{platform.AsString ()}"); + } + + Assert.Multiple (() => { + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "get_RWProperty")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "set_RWProperty")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "get_ROProperty")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "set_WOProperty")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method1")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method2")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method3")); + AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method4")); + }); + } +} diff --git a/tests/assembly-preparer/ReproTest.cs b/tests/assembly-preparer/ReproTest.cs new file mode 100644 index 000000000000..267f7f417568 --- /dev/null +++ b/tests/assembly-preparer/ReproTest.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Build.Framework; +using Microsoft.Build.Logging.StructuredLogger; + +namespace AssemblyPreparerTests; + +public class ReproTest : BaseClass { + [TestCase (ApplePlatform.iOS, false)] + public void RoundTrip (ApplePlatform platform, bool isCoreCLR) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + + // build once with a repro path + // load everything from the repro path, prepare again (with a different repro path this time), + // and verify that the arguments.txt files from each preparation are identical + // + // this test can also be repurposed to run an existing repro by setting the _PrepareAssembliesMakeReproPath variable + + var reproPath = Environment.GetEnvironmentVariable ("_PrepareAssembliesMakeReproPath"); + var referenceAssemblies = Configuration.GetReferenceAssemblies (platform, false); + if (string.IsNullOrEmpty (reproPath)) { + var code = @"public class SomeLibrary {}"; + + reproPath = Xamarin.Cache.CreateTemporaryDirectory (); + Directory.Delete (reproPath); // the repro path can't exist prior to Prepare + AssertPrepareCode (platform, isCoreCLR, (preparer) => { + preparer.MakeReproPath = reproPath; + preparer.Registrar = RegistrarMode.Dynamic; + }, code, out string _); + } + + var lines = File.ReadAllLines (Path.Combine (reproPath, "arguments.txt")); + + var ap = AssemblyPreparer.LoadFromReproPath (reproPath); + ap.MakeReproPath = Xamarin.Cache.CreateTemporaryDirectory (); + Directory.Delete (ap.MakeReproPath); // the repro path can't exist prior to Prepare + AssertPrepare (ap); + + var lines2 = File.ReadAllLines (Path.Combine (ap.MakeReproPath, "arguments.txt")); + + // Normalize repro paths before comparison, since they will always differ + var normalizedLines = lines.Select (l => l.Replace (reproPath, "")).ToArray (); + var normalizedLines2 = lines2.Select (l => l.Replace (ap.MakeReproPath, "")).ToArray (); + Assert.That (normalizedLines, Is.EqualTo (normalizedLines2), "Repro arguments match"); + } + + // This test is here only to easily debug the PrepareAssemblies task from a build that produced a binlog. + // Just copy the binlog to /tmp/assembly-preparer.binlog and run this test. It will find the PrepareAssemblies + // task in the binlog, extract the relevant parameters, and run the preparation logic with those parameters. + [Test] + public void FromBinlog () + { + var binlogPath = "/tmp/assembly-preparer.binlog"; + if (!File.Exists (binlogPath)) + Assert.Ignore (); // The binlog doesn't exist, so nothing to do + + var reader = new BinLogReader (); + var records = reader.ReadRecords (binlogPath).ToArray (); + var originalBinlogPath = records + .Select (r => r.Args) + .OfType () + .Where (r => r.SenderName == "BinaryLogger" && r.Message?.StartsWith ("BinLogFilePath=") == true) + .Select (v => v.Message?.Substring ("BinLogFilePath=".Length) ?? "") + .SingleOrDefault (); + var originalBinlogDirectory = Path.GetDirectoryName (originalBinlogPath)!; + foreach (var record in records) { + if (record is null) + continue; + + if (record.Args is null) + continue; + + if (record.Args is TaskStartedEventArgs tsea && tsea.TaskName == "PrepareAssemblies") { + var taskId = tsea.BuildEventContext?.TaskId; + if (taskId is null) + continue; + var relevantRecords = records. + Select (v => v.Args). + Where (v => v is not null). + Where (v => v.BuildEventContext?.TaskId == taskId). + ToArray (); + + var taskParameters = relevantRecords.Where (v => v is TaskParameterEventArgs).Cast ().ToArray (); + + string? getProperty (string name) + { + var param = taskParameters.SingleOrDefault (v => v.ItemType == name); + if (param is null) + return null; + if (param.Items is null) + return null; + if (param.Items.Count != 1) + return null; + var item = param.Items [0]; + if (item is null) + return null; + return ((ITaskItem) item).ItemSpec; + } + ITaskItem []? getItems (string name) + { + var param = taskParameters.SingleOrDefault (v => v.ItemType == name); + if (param is null) + return null; + if (param.Items is null) + return null; + return param.Items.Cast ().ToArray (); + } + var outputDirectory = getProperty ("OutputDirectory"); + var optionsFile = getProperty ("OptionsFile"); + var targetFrameworkMoniker = getProperty ("TargetFrameworkMoniker"); + var makeReproPath = getProperty ("MakeReproPath"); + var inputAssemblies = getItems ("InputAssemblies"); + + if (string.IsNullOrEmpty (outputDirectory)) + throw new InvalidOperationException ("OutputDirectory is required"); + outputDirectory = Path.GetFullPath (outputDirectory, originalBinlogDirectory); + + if (string.IsNullOrEmpty (optionsFile)) + throw new InvalidOperationException ("OptionsFile is required"); + optionsFile = Path.GetFullPath (optionsFile, originalBinlogDirectory); + + if (string.IsNullOrEmpty (targetFrameworkMoniker)) + throw new InvalidOperationException ("TargetFrameworkMoniker is required"); + + if (inputAssemblies is null || inputAssemblies.Length == 0) + throw new InvalidOperationException ("InputAssemblies is required"); + + AssemblyPreparerInfo GetAssemblyInfo (ITaskItem item) + { + var inputPath = Path.GetFullPath (item.ItemSpec, originalBinlogDirectory); + var outputPath = Path.Combine (outputDirectory, Path.GetFileName (inputPath)); + var metadataNames = item.MetadataNames.Cast ().Select (v => v.ToLowerInvariant ()).ToHashSet (); + var isTrimmableString = item.GetMetadata ("IsTrimmable"); + var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase); + var trimMode = item.GetMetadata ("TrimMode"); + + var rv = new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode); + return rv; + } + + var platformString = File.ReadAllLines (optionsFile).Single (v => v.StartsWith ("Platform=")).Substring ("Platform=".Length); + var platform = ApplePlatformExtensions.Parse (platformString); + + var infos = inputAssemblies.Select (GetAssemblyInfo).ToArray (); + var logger = new TestLogger () { Platform = platform }; + using var preparer = new AssemblyPreparer (logger, infos, optionsFile); + preparer.MakeReproPath = makeReproPath ?? ""; + var rv = preparer.Prepare (out var exceptions); + return; + } + } + + Assert.Fail ("The task 'PrepareAssemblies' was not found in the provided binlog."); + } +} + + +class TestLogger : IToolLog { + public int Verbosity => 0; + public required ApplePlatform Platform { get; set; } + + public void Log (string value) + { + Console.WriteLine (value); + } + + public void Log (string format, params object? [] args) + { + Console.WriteLine (format, args); + } + + public void LogException (Exception ex) + { + Console.WriteLine (ex.ToString ()); + } + + public void LogError (ProductException ex) + { + Console.WriteLine (ex.ToString ()); + } + + public void LogError (Exception ex) + { + Console.WriteLine (ex.ToString ()); + } + + public void LogError (string value) + { + Console.WriteLine (value); + } + + public void LogWarning (ProductException ex) + { + Console.WriteLine (ex.ToString ()); + } + + public void LogWarning (Exception ex) + { + Console.WriteLine (ex.ToString ()); + } +} diff --git a/tests/assembly-preparer/assembly-preparer-tests.csproj b/tests/assembly-preparer/assembly-preparer-tests.csproj new file mode 100644 index 000000000000..aab0b2d3d64e --- /dev/null +++ b/tests/assembly-preparer/assembly-preparer-tests.csproj @@ -0,0 +1,50 @@ + + + net$(BundledNETCoreAppTargetFrameworkVersion) + latest + enable + enable + false + true + true + + + + + + + + + + + + + + + + external/tests/common/BinLog.cs + + + external/tests/mtouch/Cache.cs + + + external/tests/common/Configuration.cs + + + external/tests/common/ConfigurationNUnit.cs + + + external/tests/common/DotNet.cs + + + external/tests/common/ExecutionHelper.cs + + + external/tools/common/SdkVersions.cs + + + external/tools/common/StringUtils.cs + + + + diff --git a/tests/assembly-preparer/assembly-preparer.slnx b/tests/assembly-preparer/assembly-preparer.slnx new file mode 100644 index 000000000000..990bc0a7a9fb --- /dev/null +++ b/tests/assembly-preparer/assembly-preparer.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/tests/common/Configuration.cs b/tests/common/Configuration.cs index 1735ac8df815..20f92ed994ff 100644 --- a/tests/common/Configuration.cs +++ b/tests/common/Configuration.cs @@ -577,6 +577,63 @@ public static string GetRefLibrary (ApplePlatform platform) { return GetBaseLibrary (platform); } + + public static List GetBCLAssemblies (ApplePlatform platform, bool isCoreCLR) + { + var assemblies = new List (); + string rid; + string packageName; + switch (platform) { + case ApplePlatform.MacCatalyst: + rid = "maccatalyst-arm64"; + packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.maccatalyst-arm64" : $"microsoft.netcore.app.runtime.mono.maccatalyst-arm64"; + break; + case ApplePlatform.iOS: + rid = "ios-arm64"; + packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.ios-arm64" : $"microsoft.netcore.app.runtime.mono.ios-arm64"; + break; + case ApplePlatform.TVOS: + rid = "tvos-arm64"; + packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.tvOS-arm64" : $"microsoft.netcore.app.runtime.mono.tvOS-arm64"; + break; + case ApplePlatform.MacOSX: + rid = "osx-arm64"; + packageName = "microsoft.netcore.app.runtime.osx-arm64"; + if (!isCoreCLR) + throw new InvalidOperationException ($"Mono doesn't support macOS, but was asked for the BCL assemblies for macOS"); + break; + default: + throw new NotSupportedException ($"Unsupported platform: {platform}"); + } + var microsoftNetCoreAppRefPackageVersion = File.ReadAllLines (Path.Combine (RootPath, "dotnet.config")).Single (v => v.StartsWith ("BUNDLED_NETCORE_PLATFORMS_PACKAGE_VERSION=", StringComparison.Ordinal)).Replace ("BUNDLED_NETCORE_PLATFORMS_PACKAGE_VERSION=", ""); + var bclDir = Path.Combine (RootPath, "packages", packageName, microsoftNetCoreAppRefPackageVersion, "runtimes", rid, "lib", DotNetTfm); + var nativeDir = Path.Combine (RootPath, "packages", packageName, microsoftNetCoreAppRefPackageVersion, "runtimes", rid, "native"); + + assemblies.AddRange (Directory.GetFiles (bclDir, "*.dll")); + assemblies.AddRange (Directory.GetFiles (nativeDir, "*.dll")); + + return assemblies; + } + + public static List GetReferenceAssemblies (ApplePlatform platform, bool isCoreCLR) + { + var assemblies = new List (); + + assemblies.AddRange (GetBCLAssemblies (platform, isCoreCLR)); + assemblies.AddRange (GetRefLibrary (platform)); + + return assemblies; + } + + public static List GetImplementationAssemblies (ApplePlatform platform, bool isCoreCLR) + { + var assemblies = new List (); + + assemblies.AddRange (GetBCLAssemblies (platform, isCoreCLR)); + assemblies.AddRange (GetBaseLibraryImplementations (platform).First ()); + + return assemblies; + } #endif // !XAMMAC_TESTS public static IEnumerable GetIncludedPlatforms () diff --git a/tests/common/DotNet.cs b/tests/common/DotNet.cs index 1f7a5c73e868..9e01afeebe45 100644 --- a/tests/common/DotNet.cs +++ b/tests/common/DotNet.cs @@ -109,7 +109,7 @@ public static ExecutionResult AssertNew (string outputDirectory, string template Console.WriteLine (output); Assert.That (rv.ExitCode, Is.EqualTo (0), $"Exit code: {Executable} {StringUtils.FormatArguments (args)}"); } - return new ExecutionResult (output, output, rv.ExitCode); + return new ExecutionResult (output, output, rv.ExitCode, rv.Duration); } public static ExecutionResult InstallWorkload (params string [] workloads) @@ -136,7 +136,7 @@ public static ExecutionResult InstallWorkload (params string [] workloads) Console.WriteLine (msg); Assert.Fail (msg.ToString ()); } - return new ExecutionResult (output, output, rv.ExitCode); + return new ExecutionResult (output, output, rv.ExitCode, rv.Duration); } public static ExecutionResult InstallTool (string tool, string path) @@ -206,7 +206,7 @@ public static ExecutionResult ExecuteCommand (string exe, Dictionary? properties, bool assert_success = true, string? target = null, bool? msbuildParallelism = null, TimeSpan? timeout = null, params string [] extraArguments) @@ -338,7 +338,7 @@ public static ExecutionResult Execute (string verb, string project, Dictionary + @@ -190,6 +191,11 @@ <_TestVariationApplied>true + + true + <_TestVariationApplied>true + + <_InvalidTestVariations Include="$(TestVariation.Split('|'))" Exclude="@(TestVariations)" /> diff --git a/tests/linker/link all/dotnet/shared.csproj b/tests/linker/link all/dotnet/shared.csproj index 6b75ec2a2d4e..33fc00ceb233 100644 --- a/tests/linker/link all/dotnet/shared.csproj +++ b/tests/linker/link all/dotnet/shared.csproj @@ -9,6 +9,7 @@ $(MtouchLink) --optimize=all,-remove-dynamic-registrar,-force-rejected-types-removal $(MtouchExtraArgs) --optimize=-remove-uithread-checks + $(MtouchExtraArgs) --nowarn:2003 $(MtouchExtraArgs) $(RootTestsDirectory)\linker\link all true diff --git a/tests/xharness/Jenkins/TestVariationsFactory.cs b/tests/xharness/Jenkins/TestVariationsFactory.cs index 167d8f6247bb..b623ed22710c 100644 --- a/tests/xharness/Jenkins/TestVariationsFactory.cs +++ b/tests/xharness/Jenkins/TestVariationsFactory.cs @@ -33,6 +33,7 @@ IEnumerable GetTestData (RunTestTask test) var arm64_sim_runtime_identifier = string.Empty; var x64_sim_runtime_identifier = string.Empty; var supports_coreclr = test.Platform == TestPlatform.Mac || jenkins.Harness.DotNetVersion.Major >= 11; + var supports_monovm = test.Platform != TestPlatform.Mac; var supports_x64 = string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("ACES")); // x64 is not supported on ACES machines switch (test.Platform) { @@ -55,7 +56,39 @@ IEnumerable GetTestData (RunTestTask test) } switch (test.TestName) { + case "dont link": + if (supports_coreclr) { + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Dynamic Registrar)", TestVariation = "coreclr|prepare-assemblies|dynamic-registrar", Ignored = ignore }; + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Managed Static Registrar)", TestVariation = "coreclr|prepare-assemblies|managed-static-registrar", Ignored = ignore }; + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } + if (supports_monovm) { + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Dynamic Registrar)", TestVariation = "monovm|prepare-assemblies|dynamic-registrar", Ignored = ignore }; + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Managed Static Registrar)", TestVariation = "monovm|prepare-assemblies|managed-static-registrar", Ignored = ignore }; + if (jenkins.Harness.DotNetVersion.Major >= 11) { // on Mono, the trimmable static registrar only works in .NET 11 + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } + } + break; + case "link sdk": + if (supports_coreclr) { + // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which only works on CoreCLR in .NET 10 + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } + if (supports_monovm && jenkins.Harness.DotNetVersion.Major >= 11) { + // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which, on Mono, only works in .NET 11 + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } + break; case "link all": + if (supports_coreclr) { + // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which only works on CoreCLR in .NET 10 + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } + if (supports_monovm && jenkins.Harness.DotNetVersion.Major >= 11) { + // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which, on Mono, only works in .NET 11 + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore }; + } if (test.ProjectConfiguration == "Debug") { yield return new TestData { Variation = "Debug (don't bundle original resources)", TestVariation = "do-not-bundle-original-resources" }; } @@ -63,6 +96,7 @@ IEnumerable GetTestData (RunTestTask test) case "monotouch-test": yield return new TestData { Variation = "Release (link sdk)", TestVariation = "release|linksdk", Ignored = ignore }; yield return new TestData { Variation = "Release (link all)", TestVariation = "release|linkall", Ignored = ignore }; + yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies)", TestVariation = "prepare-assemblies", Ignored = ignore }; break; } diff --git a/tools/Makefile b/tools/Makefile index 00d896286809..c4c969bf82b7 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -14,3 +14,4 @@ SUBDIRS+=mlaunch SUBDIRS += dotnet-linker +SUBDIRS += assembly-preparer diff --git a/tools/ap-launcher/.gitignore b/tools/ap-launcher/.gitignore new file mode 100644 index 000000000000..af1e7137432d --- /dev/null +++ b/tools/ap-launcher/.gitignore @@ -0,0 +1,2 @@ +.build-stamp + diff --git a/tools/ap-launcher/Makefile b/tools/ap-launcher/Makefile new file mode 100644 index 000000000000..adb26c8c4e3c --- /dev/null +++ b/tools/ap-launcher/Makefile @@ -0,0 +1,8 @@ +TOP=../.. +include $(TOP)/Make.config +include $(TOP)/mk/rules.mk + +build: + $(Q_BUILD) $(DOTNET) build /bl:$@.binlog *.csproj $(DOTNET_BUILD_VERBOSITY) + +all-local:: build diff --git a/tools/ap-launcher/Program.cs b/tools/ap-launcher/Program.cs new file mode 100644 index 000000000000..44da342c46a9 --- /dev/null +++ b/tools/ap-launcher/Program.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xamarin.Build; +using Xamarin.Bundler; +using Xamarin.Utils; + +class Program { + public static int Main (string [] args) + { + var optionsFile = args.Single (v => v.StartsWith ("--options-file=")).Substring ("--options-file=".Length); + var makeReproPath = args.SingleOrDefault (v => v.StartsWith ("--make-repro-path="))?.Substring ("--make-repro-path=".Length) ?? ""; + + var api = new List (); + foreach (var inputArgs in args.Where (v => v.StartsWith ("--input-assembly=")).Select (v => v.Substring ("--input-assembly=".Length))) { + var ia = inputArgs.Split ('|'); + var inputPath = ia.Single (v => v.StartsWith ("InputPath="))?.Substring ("InputPath=".Length) ?? throw new InvalidOperationException ("InputPath is required"); + var outputPath = ia.Single (v => v.StartsWith ("OutputPath="))?.Substring ("OutputPath=".Length) ?? throw new InvalidOperationException ("OutputPath is required"); + var isTrimmableString = ia.Single (v => v.StartsWith ("IsTrimmable="))?.Substring ("IsTrimmable=".Length); + var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase); + var trimMode = ia.Single (v => v.StartsWith ("TrimMode="))?.Substring ("TrimMode=".Length) ?? ""; + + api.Add (new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode)); + } + + var platformString = File.ReadAllLines (optionsFile).Single (v => v.StartsWith ("Platform=")).Substring ("Platform=".Length); + var platform = ApplePlatformExtensions.Parse (platformString); + + var infos = api.ToArray (); + var logger = new TestLogger () { + Platform = platform, + }; + using var preparer = new AssemblyPreparer (logger, infos, optionsFile); + preparer.MakeReproPath = makeReproPath ?? ""; + var rv = preparer.Prepare (out var exceptions); + + return rv && exceptions.Count == 0 ? 0 : 1; + } +} + +class TestLogger : IToolLog { + public int Verbosity => 0; + public required ApplePlatform Platform { get; set; } + + public void Log (string value) + { + Console.WriteLine (value); + } + + public void LogError (string value) + { + Console.WriteLine (value); + } + + public void Log (string format, params object? [] args) + { + Console.WriteLine (format, args); + } + + public void LogException (Exception ex) + { + Console.Error.WriteLine (ex.ToString ()); + } + + public void LogError (ProductException ex) + { + Console.Error.WriteLine (ex.ToString ()); + } + + public void LogWarning (ProductException ex) + { + Console.WriteLine (ex.ToString ()); + } +} diff --git a/tools/ap-launcher/README.md b/tools/ap-launcher/README.md new file mode 100644 index 000000000000..5007c9cc4226 --- /dev/null +++ b/tools/ap-launcher/README.md @@ -0,0 +1,2 @@ +This is just a simple project that references the assembly-preparer library, to make it easy to debug the library in a debugger. + diff --git a/tools/ap-launcher/ap-launcher.csproj b/tools/ap-launcher/ap-launcher.csproj new file mode 100644 index 000000000000..ea7daa76d3f5 --- /dev/null +++ b/tools/ap-launcher/ap-launcher.csproj @@ -0,0 +1,17 @@ + + + + Exe + net$(BundledNETCoreAppTargetFrameworkVersion) + false + false + ap_launcher + enable + enable + + + + + + + diff --git a/tools/ap-launcher/ap-launcher.slnx b/tools/ap-launcher/ap-launcher.slnx new file mode 100644 index 000000000000..f543c9a91276 --- /dev/null +++ b/tools/ap-launcher/ap-launcher.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/tools/assembly-preparer/.gitignore b/tools/assembly-preparer/.gitignore new file mode 100644 index 000000000000..65bf06abaee5 --- /dev/null +++ b/tools/assembly-preparer/.gitignore @@ -0,0 +1,2 @@ +*.csproj.inc + diff --git a/tools/assembly-preparer/AssemblyPreparer.cs b/tools/assembly-preparer/AssemblyPreparer.cs new file mode 100644 index 000000000000..9f2936c32eaa --- /dev/null +++ b/tools/assembly-preparer/AssemblyPreparer.cs @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Runtime.Serialization; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.CompilerServices.SymbolWriter; +using Mono.Linker; +using Mono.Linker.Steps; +using Mono.Tuner; +using MonoTouch.Tuner; +using Xamarin.Bundler; +using Xamarin.Linker; +using Xamarin.Linker.Steps; +using Xamarin.Tuner; +using Xamarin.Utils; + +namespace Xamarin.Build; + +public class AssemblyPreparer : IDisposable { + AggregateLog log = new AggregateLog (); + + LinkerConfiguration configuration; + + public LinkerConfiguration Configuration => configuration; + + public string MakeReproPath { get; set; } = string.Empty; + + public RegistrarMode Registrar { + get => configuration.Application.Registrar; + set => configuration.Application.Registrar = value; + } + + public string IntermediateOutputPath { + get => configuration.IntermediateOutputPath; + } + + public Optimizations Optimizations => configuration.Application.Optimizations; + + public List Assemblies { get; set; } = new List (); + + public IList<(string Path, AssemblyDefinition Assembly, string? OriginatingAssembly)> AddedAssemblies => configuration.AddedAssemblies; + + LinkerConfiguration.Configurator GetConfigurator (string? reproPath = null, Func? assemblyPreparerInfoFactory = null) + { + var dict = new LinkerConfiguration.Configurator () { + { "AssemblyPreparer", ( + new LinkerConfiguration.LoadValue ((key, value) => { + var split = value.Split ('|'); + var input = split[0]; + var output = split[1]; + var isTrimmableString = split[2]; + var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase); + var trimMode = split[3]; + var apinfo = assemblyPreparerInfoFactory is not null ? assemblyPreparerInfoFactory (input, output) : new AssemblyPreparerInfo (input, output, isTrimmable, trimMode); + Assemblies.Add (apinfo); + }), + new LinkerConfiguration.SaveValue ((key, storage) => SaveAssemblies (key, storage, reproPath, Assemblies)) + )}, + }; + return dict; + } + + static void SaveAssemblies (string key, List storage, string? reproPath, IList assemblies) + { + foreach (var assembly in assemblies) { + var input = assembly.InputPath; + var output = assembly.OutputPath; + if (!string.IsNullOrEmpty (reproPath)) { + output = Path.Combine (reproPath, Path.GetFileName (output)); + File.Copy (input, output); + } + storage.Add ($"{key}={input}|{output}|{(assembly.IsTrimmable.HasValue ? (assembly.IsTrimmable.Value ? "true" : "false") : "")}|{assembly.TrimMode}"); + } + } + + public AssemblyPreparer (IToolLog log, AssemblyPreparerInfo [] assemblies, string linker_file) + { + var lines = File.ReadAllLines (linker_file).ToList (); + SaveAssemblies ("AssemblyPreparer", lines, null, assemblies); + configuration = new LinkerConfiguration (log, lines, linker_file, GetConfigurator (null, assemblies.Length == 0 ? null : (input, output) => assemblies.Single (a => a.InputPath == input && a.OutputPath == output))); + } + + public void AddLog (IAssemblyPreparerLog log) + { + if (log is null) + throw new ArgumentNullException (nameof (log)); + this.log.Add (log); + } + + bool SaveToReproPath (List exceptions) + { + if (File.Exists (MakeReproPath) || Directory.Exists (MakeReproPath)) { + exceptions.Add (ErrorHelper.CreateError (99, $"Repro location already exists: {MakeReproPath}")); + return false; + } + Directory.CreateDirectory (MakeReproPath); + var lines = new List (); + configuration.Save (lines, GetConfigurator (MakeReproPath)); + File.WriteAllLines (Path.Combine (MakeReproPath, "arguments.txt"), lines); + log.Log ($"Created repro in {MakeReproPath}"); + + return true; + } + + public static AssemblyPreparer LoadFromReproPath (string reproPath) + { + var file = Path.Combine (reproPath, "arguments.txt"); + if (!File.Exists (file)) + throw new FileNotFoundException ($"Repro arguments file not found: {file}"); + return new AssemblyPreparer (ConsoleLog.Instance, Array.Empty (), file); + } + + public bool Prepare (out List exceptions) + { + exceptions = configuration.Exceptions; + + if (Registrar == RegistrarMode.Default) { + exceptions.Add (ErrorHelper.CreateError (99, "RegistrarMode must be explicitly set.")); + return false; + } + + if (!string.IsNullOrEmpty (MakeReproPath) && !SaveToReproPath (exceptions)) + return false; + + var steps = new ConfigurationAwareStep [] { + // All the same steps as the custom trimmer steps that are run before MarkStep in Xamarin.Shared.Sdk.targets (and in the same order). + // CollectAssembliesStep + new CoreTypeMapStep (), + // ProcessExportedFields + new PreserveProtocolsStep (), + new PreserveSmartEnumConversionsStep (), + new PreserveBlockCodeStep (), + new OptimizeGeneratedCodeStep (), + new ApplyPreserveAttributeStep (), + new MarkForStaticRegistrarStep (), + new MarkNSObjectsStep (), + new InlineDlfcnMethodsStep (), + new RegistrarRemovalTrackingStep (), + // PreMarkDispatcher: I don't think we need this one + new ManagedRegistrarStep (), + new TrimmableRegistrarStep (), + new ManagedRegistrarLookupTablesStep (), + }; + + var linkContext = configuration.DerivedLinkContext; + + var parameters = new ReaderParameters { + AssemblyResolver = configuration.AssemblyResolver, + MetadataResolver = configuration.MetadataResolver, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider (throwIfNoSymbol: false), + }; + + foreach (var assembly in Assemblies) { + var assemblyDefinition = AssemblyDefinition.ReadAssembly (assembly.InputPath, parameters); + linkContext.Assemblies.Add (assemblyDefinition); + assembly.Assembly = assemblyDefinition; + configuration.Context.Annotations.SetAction (assemblyDefinition, ComputeAssemblyAction (assemblyDefinition, assembly)); + configuration.AssemblyResolver.ResolverCache.Add (assemblyDefinition.Name.Name, assemblyDefinition); + } + + configuration.Context.Annotations.CollectOverrides (linkContext.Assemblies, linkContext); + + foreach (var step in steps) { + step.Process (linkContext); + } + + // save assemblies + + foreach (var assembly in Assemblies) { + var assemblyDefinition = assembly.Assembly!; + + var action = configuration.Context.Annotations.GetAction (assemblyDefinition); + switch (action) { + case AssemblyAction.Copy: + case AssemblyAction.CopyUsed: + assembly.OutputPath = assembly.InputPath; + continue; + case AssemblyAction.Link: + case AssemblyAction.Save: + log.Log ($"Saving {assembly.InputPath} to {assembly.OutputPath}"); + break; + default: + exceptions.Add (ErrorHelper.CreateError (99, $"Unknown link action: {action} for assembly {assemblyDefinition.Name}")); + return false; + } + + Directory.CreateDirectory (Path.GetDirectoryName (assembly.OutputPath)!); + var writerParameters = new WriterParameters (); + if (assemblyDefinition.MainModule.HasSymbols) { + var provider = new CustomSymbolWriterProvider (); + try { + using (var tmp = provider.GetSymbolWriter (assemblyDefinition.MainModule, Path.ChangeExtension (assembly.OutputPath, ".pdb"))) { } + File.Delete (Path.ChangeExtension (assembly.OutputPath, ".pdb")); + writerParameters.WriteSymbols = true; + writerParameters.SymbolWriterProvider = provider; + } catch (Exception e) { + log.Log ($"Failed to create symbol writer for {assembly.OutputPath}, not writing symbols: {e.Message}"); + } + } + + RemoveCrossGen (assemblyDefinition); + + try { + assemblyDefinition.Write (assembly.OutputPath, writerParameters); + ModuleAttributes m = assemblyDefinition.MainModule.Attributes; + } catch (Exception e) { + exceptions.Add (ErrorHelper.CreateError (99, e, $"Failed to write {assembly.OutputPath}: {e.Message}")); + log.Log ($"Failed to write {assembly.OutputPath}: {e}"); + return false; + } + } + + return exceptions.Count == 0; + } + + void RemoveCrossGen (AssemblyDefinition assemblyDefinition) + { + // Drop crossgened code from the assembly + // Ref: https://github.com/dotnet/runtime/blob/b86458593223f866effa63122b05bec37f83015e/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs#L95-L105 + foreach (var module in assemblyDefinition.Modules) { + var moduleAttributes = module.Attributes; + var isCrossGened = (moduleAttributes & ModuleAttributes.ILOnly) == 0 && (moduleAttributes & ModuleAttributes.ILLibrary) == ModuleAttributes.ILLibrary; + if (isCrossGened) { + moduleAttributes |= ModuleAttributes.ILOnly; + moduleAttributes &= ~ModuleAttributes.ILLibrary; + module.Attributes = moduleAttributes; + module.Architecture = TargetArchitecture.I386; + module.Characteristics |= ModuleCharacteristics.NoSEH; + } + } + } + + // Figure out if an assembly is trimmed or not. + // This must be identical to how it's done for ILLink/ILC. + AssemblyAction ComputeAssemblyAction (AssemblyDefinition assembly, AssemblyPreparerInfo info) + { + // Unless 'PublishTrimmed=true', nothing is trimmed, because we won't run the trimmer. + if (!configuration.PublishTrimmed) + return AssemblyAction.Copy; + + // Then if 'TrimMode' is set on the assembly, then that takes precedence + switch (info.TrimMode?.ToLowerInvariant () ?? "") { + case "link": + return AssemblyAction.Link; + case "copy": + return AssemblyAction.Copy; + case "": + break; + default: + throw new ArgumentException ($"Unknown trim mode: {info.TrimMode} for assembly {assembly.Name}"); + } + + // Then if 'IsTrimmable' is set on the assembly, that takes precedence over the default for the platform. + if (info.IsTrimmable == false) + return AssemblyAction.CopyUsed; + else if (info.IsTrimmable == true) + return AssemblyAction.Link; + + // Check the global 'TrimMode' property, if it's not 'link', 'partial' or 'full', then we're not trimming anything + var globalTrimMode = configuration.TrimMode.ToLowerInvariant (); + switch (globalTrimMode) { + case "copy": + case "": + return AssemblyAction.Copy; + case "partial": + case "full": + case "link": + break; + default: + throw new ArgumentException ($"Unknown global trim mode: {configuration.TrimMode}"); + } + + // Check the [AssemblyMetadata] attribute + var isTrimmableAttribute = assembly.CustomAttributes + .Where (v => v.AttributeType.FullName == "System.Reflection.AssemblyMetadataAttribute") + .Where (v => v.HasConstructorArguments && v.ConstructorArguments.Count == 2 && v.ConstructorArguments [0].Type.Is ("System", "String") && v.ConstructorArguments [1].Type.Is ("System", "String")) + .Where (v => (v.ConstructorArguments [0].Value as string) == "IsTrimmable" && string.Equals (v.ConstructorArguments [1].Value as string, "true", StringComparison.OrdinalIgnoreCase)) + .SingleOrDefault (); + + if (isTrimmableAttribute is null) { + // If the attribute is not present, then we trim if the global 'TrimMode' is 'full' + return globalTrimMode switch { + "link" => AssemblyAction.Copy, + "partial" => AssemblyAction.Copy, + "full" => AssemblyAction.Link, + _ => throw new ArgumentException ($"Unknown global trim mode: {configuration.TrimMode}"), + }; + } + + // if the attribute is present, then we trim if the global 'TrimMode' is 'partial', 'full' or 'link', which are the only values it should have at this point + switch (globalTrimMode) { + case "partial": + case "full": + case "link": + break; + default: + // we shouldn't get here for any other trim mode value + throw new ArgumentException ($"Unexpected global trim mode: {configuration.TrimMode}"); + } + + return AssemblyAction.Link; + } + + public void Dispose () + { + foreach (var assembly in Assemblies) + assembly.Assembly?.Dispose (); + configuration.AssemblyResolver.ResolverCache.Clear (); + configuration.DerivedLinkContext.Assemblies.Clear (); + } +} + +public class AssemblyPreparerInfo { + internal AssemblyDefinition? Assembly { get; set; } + + public string InputPath { get; private set; } + public bool? IsTrimmable { get; set; } + public string TrimMode { get; set; } + public string OutputPath { get; set; } + + public AssemblyPreparerInfo (string inputPath, string outputPath, bool? isTrimmable, string trimMode) + { + InputPath = inputPath; + OutputPath = outputPath; + IsTrimmable = isTrimmable; + TrimMode = trimMode; + } +} diff --git a/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs b/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 000000000000..4a42e76ab54c --- /dev/null +++ b/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// This file is needed because we compile the current project under netstandard2.0, and this type doesn't exist there. + +#if !NET + +// From https://raw.githubusercontent.com/dotnet/dotnet/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs + +global using DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes2; + +namespace System.Diagnostics.CodeAnalysis { + using System.ComponentModel; + + /// + /// Specifies the types of members that are dynamically accessed. + /// + /// This enumeration has a attribute that allows a + /// bitwise combination of its member values. + /// + [Flags] + enum DynamicallyAccessedMemberTypes2 { + /// + /// Specifies no members. + /// + None = 0, + + /// + /// Specifies the default, parameterless public constructor. + /// + PublicParameterlessConstructor = 0x0001, + + /// + /// Specifies all public constructors. + /// + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + + /// + /// Specifies all non-public constructors. + /// + NonPublicConstructors = 0x0004, + + /// + /// Specifies all public methods. + /// + PublicMethods = 0x0008, + + /// + /// Specifies all non-public methods. + /// + NonPublicMethods = 0x0010, + + /// + /// Specifies all public fields. + /// + PublicFields = 0x0020, + + /// + /// Specifies all non-public fields. + /// + NonPublicFields = 0x0040, + + /// + /// Specifies all public nested types. + /// + PublicNestedTypes = 0x0080, + + /// + /// Specifies all non-public nested types. + /// + NonPublicNestedTypes = 0x0100, + + /// + /// Specifies all public properties. + /// + PublicProperties = 0x0200, + + /// + /// Specifies all non-public properties. + /// + NonPublicProperties = 0x0400, + + /// + /// Specifies all public events. + /// + PublicEvents = 0x0800, + + /// + /// Specifies all non-public events. + /// + NonPublicEvents = 0x1000, + + /// + /// Specifies all interfaces implemented by the type. + /// + Interfaces = 0x2000, + + /// + /// Specifies all non-public constructors, including those inherited from base classes. + /// + NonPublicConstructorsWithInherited = NonPublicConstructors | 0x4000, + + /// + /// Specifies all non-public methods, including those inherited from base classes. + /// + NonPublicMethodsWithInherited = NonPublicMethods | 0x8000, + + /// + /// Specifies all non-public fields, including those inherited from base classes. + /// + NonPublicFieldsWithInherited = NonPublicFields | 0x10000, + + /// + /// Specifies all non-public nested types, including those inherited from base classes. + /// + NonPublicNestedTypesWithInherited = NonPublicNestedTypes | 0x20000, + + /// + /// Specifies all non-public properties, including those inherited from base classes. + /// + NonPublicPropertiesWithInherited = NonPublicProperties | 0x40000, + + /// + /// Specifies all non-public events, including those inherited from base classes. + /// + NonPublicEventsWithInherited = NonPublicEvents | 0x80000, + + /// + /// Specifies all public constructors, including those inherited from base classes. + /// + PublicConstructorsWithInherited = PublicConstructors | 0x100000, + + /// + /// Specifies all public nested types, including those inherited from base classes. + /// + PublicNestedTypesWithInherited = PublicNestedTypes | 0x200000, + + /// + /// Specifies all constructors, including those inherited from base classes. + /// + AllConstructors = PublicConstructorsWithInherited | NonPublicConstructorsWithInherited, + + /// + /// Specifies all methods, including those inherited from base classes. + /// + AllMethods = PublicMethods | NonPublicMethodsWithInherited, + + /// + /// Specifies all fields, including those inherited from base classes. + /// + AllFields = PublicFields | NonPublicFieldsWithInherited, + + /// + /// Specifies all nested types, including those inherited from base classes. + /// + AllNestedTypes = PublicNestedTypesWithInherited | NonPublicNestedTypesWithInherited, + + /// + /// Specifies all properties, including those inherited from base classes. + /// + AllProperties = PublicProperties | NonPublicPropertiesWithInherited, + + /// + /// Specifies all events, including those inherited from base classes. + /// + AllEvents = PublicEvents | NonPublicEventsWithInherited, + + /// + /// Specifies all members. + /// + [EditorBrowsable (EditorBrowsableState.Never)] + All = ~None + } +} + +#endif // !NET diff --git a/tools/assembly-preparer/GlobalUsings.cs b/tools/assembly-preparer/GlobalUsings.cs new file mode 100644 index 000000000000..ba0a28435170 --- /dev/null +++ b/tools/assembly-preparer/GlobalUsings.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System; +global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; +global using System.Linq; +global using System.Runtime.InteropServices; + +global using Foundation; +global using ObjCRuntime; + +global using Xamarin.Linker; + +namespace Xamarin.Tuner { } +namespace Mono.Linker.Steps { } diff --git a/tools/assembly-preparer/IAssemblyPreparerLog.cs b/tools/assembly-preparer/IAssemblyPreparerLog.cs new file mode 100644 index 000000000000..f41de7ca475e --- /dev/null +++ b/tools/assembly-preparer/IAssemblyPreparerLog.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Xamarin.Bundler; + +namespace Xamarin.Build; + +public interface IAssemblyPreparerLog { + void Log (string message); +} + +class AggregateLog : IAssemblyPreparerLog { + List logs = new (); + + public void Add (IAssemblyPreparerLog log) + { + logs.Add (log); + } + + public void Log (string message) + { + foreach (var log in logs) + log.Log (message); + } +} diff --git a/tools/assembly-preparer/Makefile b/tools/assembly-preparer/Makefile new file mode 100644 index 000000000000..587a1d6116f5 --- /dev/null +++ b/tools/assembly-preparer/Makefile @@ -0,0 +1,25 @@ +TOP=../.. +include $(TOP)/Make.config +include $(TOP)/mk/rules.mk +include ../common/Make.common + +# assembly-preparer.csproj.inc contains the $(assembly_preparer_dependencies) variable used to determine if assembly-preparer needs to be rebuilt or not. +assembly-preparer.csproj.inc: export BUILD_VERBOSITY=$(MSBUILD_VERBOSITY) +-include assembly-preparer.csproj.inc + +ASSEMBLY_PREPARER_NETCORE=bin/Debug/$(DOTNET_TFM)/assembly-preparer.dll +ASSEMBLY_PREPARER_NETSTD=bin/Debug/netstandard2.0/assembly-preparer.dll +ASSEMBLIES=$(ASSEMBLY_PREPARER_NETCORE) $(ASSEMBLY_PREPARER_NETSTD) + +.build-stamp: $(assembly_preparer_dependencies) + $(Q_BUILD) $(DOTNET) build /bl:$@.binlog *.csproj $(DOTNET_BUILD_VERBOSITY) + $(Q) touch $(ASSEMBLIES) + +$(ASSEMBLIES): .build-stamp + +all-local:: .build-stamp + +run-tests: + $(Q_BUILD) $(DOTNET) test $(TOP)/tests/assembly-preparer/assembly-preparer-tests.csproj --logger "console;verbosity=detailed" $(DOTNET_BUILD_VERBOSITY) -bl:$@.binlog + +generated-files: $(abspath ../common/SdkVersions.cs) $(abspath ../common/ProductConstants.cs) diff --git a/tools/assembly-preparer/NetStandardExtensions.cs b/tools/assembly-preparer/NetStandardExtensions.cs new file mode 100644 index 000000000000..5f81fd7f1b93 --- /dev/null +++ b/tools/assembly-preparer/NetStandardExtensions.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// This file is needed because we compile the current project under netstandard2.0, and the types here don't exist in netstandard2.0 + +#if !NET + +public static class QueueExtensions { + public static bool TryDequeue (this Queue queue, [MaybeNullWhen (false)] out T item) + { + if (queue.Count == 0) { + item = default; + return false; + } + item = queue.Dequeue (); + return true; + } + + public static bool TryAdd (this Dictionary dictionary, T key, V value) + { + if (dictionary.ContainsKey (key)) + return false; + dictionary.Add (key, value); + return true; + } +} + +public static class DictionaryExtensions { + public static bool Remove (this Dictionary dictionary, T key, [MaybeNullWhen (false)] out V value) + { + if (dictionary.TryGetValue (key, out value)) { + dictionary.Remove (key); + return true; + } + return false; + } +} + +public static class EnumerableExtensions { + public static IEnumerable SkipLast (this IEnumerable source, int count) + { + // very naive implementation, but it's only for netstandard2.0, which will go away soon, so no need to optimize it + var rv = source.ToList (); + if (rv.Count <= count) + return []; + rv.RemoveRange (rv.Count - count, count); + return rv; + } +} + +// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RequiredMemberAttribute.cs +namespace System.Runtime.CompilerServices { + using System.ComponentModel; + + /// Specifies that a type has required members or that a member is required. + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [EditorBrowsable (EditorBrowsableState.Never)] + internal sealed class RequiredMemberAttribute : Attribute { } +} + +// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs +namespace System.Runtime.CompilerServices { + using System.ComponentModel; + + /// + /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. + /// + [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute { + public CompilerFeatureRequiredAttribute (string featureName) + { + FeatureName = featureName; + } + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof (RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof (RequiredMembers); + } +} + +// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs +namespace System.Runtime.CompilerServices { + using System.ComponentModel; + + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable (EditorBrowsableState.Never)] + internal static class IsExternalInit { + } +} +#endif diff --git a/tools/assembly-preparer/README.md b/tools/assembly-preparer/README.md new file mode 100644 index 000000000000..696974bf073f --- /dev/null +++ b/tools/assembly-preparer/README.md @@ -0,0 +1,35 @@ +# Assembly preparer + +This is a library that will modify assemblies when a project is built for a few reasons: + +* Collect required information for a successful build +* Transform some code patterns so that they can be properly recognized and handled correctly by trimmers. +* Optimize some code patterns we can easily recognize +* Precompute some things at build time to be able to make apps smaller and run faster. + +Currently it can: + +* PreserveCodeBlockHandler: in some cases user assemblies might contain code + created by the generator that's not trimmer safe; this handler will inject + code to ensure that trimmers don't trim way some things that shouldn't be + trimmed away. + +## Design principles + +* Easy to test (there's a unit test project, VSCode can run & debug its tests) +* Can be called from an MSBuild task (which means it currently needs to target `netstandard2.0`). +* Good error handling/reporting. +* We have two main scenarios: + * Debug loop, where we shouldn't do more than absolutely necessary to make + debug builds as fast as possible. In particular, we'll only do whatever + is necessary for a correct build, and if possible, no assembly + modification, only information gathering. + * Release builds, where we want to optimize as much as possible. +* To ease integration with existing custom linker steps, it's provides a + very simplified API of ILLink's custom linker step API - much of it is + stubbed out until it's needed. +* Until fully complete and both correctness and performance have been + validated, it should be possible to use either custom linker steps or the + assembly preparer, and as such any code that's used by both will have to + keep working in both modes. + diff --git a/tools/assembly-preparer/Scaffolding/AnnotationStore.cs b/tools/assembly-preparer/Scaffolding/AnnotationStore.cs new file mode 100644 index 000000000000..e1fc186bbf11 --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/AnnotationStore.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using Mono.Cecil; + +namespace Mono.Linker; + +public class AnnotationStore { + Dictionary assemblyActions = new Dictionary (); + Dictionary> overrides = new Dictionary> (); + public AssemblyAction GetAction (AssemblyDefinition assembly) + { + if (assemblyActions.TryGetValue (assembly, out var action)) + return action; + throw new InvalidOperationException ($"Assembly {assembly.Name} not found in the annotation store"); + } + + public void SetAction (AssemblyDefinition assembly, AssemblyAction action) + { + assemblyActions [assembly] = action; + } + + public IEnumerable? GetOverrides (MethodDefinition method) + { + if (overrides.TryGetValue (method, out var list)) + return list; + return null; + } + + // Scan all types in the given assemblies and build the overrides map. + // For each method that overrides a base virtual method (or explicitly implements an interface method), + // record an OverrideInformation entry keyed by the base method. + public void CollectOverrides (IEnumerable assemblies, LinkContext context) + { + foreach (var assembly in assemblies) { + foreach (var module in assembly.Modules) { + foreach (var type in module.GetTypes ()) { + CollectOverridesForType (type, context); + } + } + } + } + + void CollectOverridesForType (TypeDefinition type, LinkContext context) + { + if (!type.HasMethods) + return; + + foreach (var method in type.Methods) { + // Handle explicit overrides (.override directive in IL / method.Overrides in Cecil) + if (method.HasOverrides) { + foreach (var overriddenRef in method.Overrides) { + var baseMethod = TryResolve (overriddenRef); + if (baseMethod is not null) + AddOverride (baseMethod, method); + } + } + + // Handle implicit virtual method overrides via the type hierarchy + if (method.IsVirtual && !method.IsNewSlot) { + var baseMethod = GetBaseMethodInTypeHierarchy (type, method, context); + if (baseMethod is not null) + AddOverride (baseMethod, method); + } + } + } + + void AddOverride (MethodDefinition baseMethod, MethodDefinition overridingMethod) + { + if (!overrides.TryGetValue (baseMethod, out var list)) { + list = new List (); + overrides [baseMethod] = list; + } + list.Add (new OverrideInformation (overridingMethod)); + } + + static MethodDefinition? GetBaseMethodInTypeHierarchy (TypeDefinition type, MethodDefinition method, LinkContext context) + { + var baseTypeRef = type.BaseType; + while (baseTypeRef is not null) { + TypeDefinition? baseType; + try { + baseType = context.Resolve (baseTypeRef); + } catch { + break; + } + + if (baseType.HasMethods) { + foreach (var candidate in baseType.Methods) { + if (candidate.IsVirtual && MethodMatch (candidate, method)) + return candidate; + } + } + + baseTypeRef = baseType.BaseType; + } + return null; + } + + static bool MethodMatch (MethodDefinition candidate, MethodDefinition method) + { + if (candidate.Name != method.Name) + return false; + if (candidate.HasGenericParameters != method.HasGenericParameters) + return false; + if (candidate.GenericParameters.Count != method.GenericParameters.Count) + return false; + if (candidate.ReturnType.FullName != method.ReturnType.FullName) + return false; + if (candidate.HasParameters != method.HasParameters) + return false; + if (!candidate.HasParameters) + return true; + if (candidate.Parameters.Count != method.Parameters.Count) + return false; + for (int i = 0; i < candidate.Parameters.Count; i++) { + if (candidate.Parameters [i].ParameterType.FullName != method.Parameters [i].ParameterType.FullName) + return false; + } + return true; + } + + static MethodDefinition? TryResolve (MethodReference methodRef) + { + try { + return methodRef.Resolve (); + } catch { + // FIXME: figure out a way that doesn't require throwing and catching exceptions. + return null; + } + } + + Dictionary> custom_annotations = new (); + + public void SetCustomAnnotation (object key, IMetadataTokenProvider item, object value) + { + if (!custom_annotations.TryGetValue (key, out var annotations)) + custom_annotations [key] = annotations = new Dictionary (); + annotations [item] = value; + } + + public object? GetCustomAnnotation (object key, IMetadataTokenProvider item) + { + if (custom_annotations.TryGetValue (key, out var annotations) && annotations.TryGetValue (item, out var value)) + return value; + + return null; + } + + // This should not be called; once closer to done, just remove this method. + public void Mark (object obj) + { + // Console.WriteLine ($"Annotations.Mark () called from {new StackTrace (1).GetFrame (0)?.GetMethod ()}"); + } +} + +[DebuggerDisplay ("{Override}")] +public class OverrideInformation { + + public MethodDefinition Override { get; } + + internal OverrideInformation (MethodDefinition @override) + { + Override = @override; + } +} diff --git a/tools/assembly-preparer/Scaffolding/AssemblyAction.cs b/tools/assembly-preparer/Scaffolding/AssemblyAction.cs new file mode 100644 index 000000000000..8a71e3197566 --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/AssemblyAction.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Mono.Linker; + +public enum AssemblyAction { + Copy, + Link, + Save, + Skip, + CopyUsed, + AddBypassNGen, + AddBypassNGenUsed, + Delete, +} diff --git a/tools/assembly-preparer/Scaffolding/BaseStep.cs b/tools/assembly-preparer/Scaffolding/BaseStep.cs new file mode 100644 index 000000000000..f0d31bf7e00b --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/BaseStep.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// +// BaseStep.cs +// +// Author: +// Jb Evain (jbevain@novell.com) +// +// (C) 2007 Novell, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System.Diagnostics; +using Mono.Cecil; + +using Xamarin.Tuner; + +namespace Mono.Linker.Steps; + +public abstract class BaseStep : IStep { + DerivedLinkContext? context; + + public DerivedLinkContext Context { + get { + Debug.Assert (context is not null); + return context!; + } + } + + public AnnotationStore Annotations { + get { return Context.Annotations; } + } + + public void Process (DerivedLinkContext context) + { + this.context = context; + + if (!ConditionToProcess ()) + return; + + Process (); + + foreach (var assembly in context.GetAssemblies ()) { + ProcessAssembly (assembly); + } + + EndProcess (); + } + + protected virtual bool ConditionToProcess () + { + return true; + } + + protected virtual void Process () + { + } + + protected virtual void EndProcess () + { + } + + protected virtual void ProcessAssembly (AssemblyDefinition assembly) + { + } +} diff --git a/tools/assembly-preparer/Scaffolding/IStep.cs b/tools/assembly-preparer/Scaffolding/IStep.cs new file mode 100644 index 000000000000..0a01c17d215b --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/IStep.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// +// IStep.cs +// +// Author: +// Jb Evain (jbevain@gmail.com) +// +// (C) 2006 Jb Evain +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using Xamarin.Tuner; + +namespace Mono.Linker.Steps; + +public interface IStep { + void Process (DerivedLinkContext context); +} diff --git a/tools/assembly-preparer/Scaffolding/LinkContext.cs b/tools/assembly-preparer/Scaffolding/LinkContext.cs new file mode 100644 index 000000000000..5921dbe364ca --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/LinkContext.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Mono.Cecil; +using Xamarin.Bundler; + +namespace Mono.Linker; + +public class LinkContext { + Dictionary custom_data = new Dictionary (); + + AnnotationStore annotations = new AnnotationStore (); + public AnnotationStore Annotations { get => annotations; } + + public List Assemblies = new List (); + + public AssemblyDefinition [] GetAssemblies () { return Assemblies.ToArray (); } + + public LinkerConfiguration Configuration { get; private set; } + + public LinkerConfiguration LinkerConfiguration { get => Configuration; } + + public LinkContext (LinkerConfiguration configuration) + { + Configuration = configuration; + } + + public TypeDefinition Resolve (TypeReference type) + { + return Configuration.MetadataResolver.Resolve (type); + } + + public AssemblyDefinition? GetLoadedAssembly (string name) + { + return Assemblies.SingleOrDefault (v => v.Name.Name == name); + } + + public void SetCustomData (string key, string value) + { + custom_data [key] = value; + } + + public bool TryGetCustomData (string key, [NotNullWhen (true)] out string? value) + { + return custom_data.TryGetValue (key, out value); + } +} diff --git a/tools/assembly-preparer/Scaffolding/Target.cs b/tools/assembly-preparer/Scaffolding/Target.cs new file mode 100644 index 000000000000..5d4e99fa509b --- /dev/null +++ b/tools/assembly-preparer/Scaffolding/Target.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xamarin.Utils; + +namespace Xamarin.Bundler; + +public class Target { + Target () { } + public Application App { get => throw new NotImplementedException (); } +} diff --git a/tools/assembly-preparer/System_Diagnostics_UnreachableException.cs b/tools/assembly-preparer/System_Diagnostics_UnreachableException.cs new file mode 100644 index 000000000000..1f48b65b411c --- /dev/null +++ b/tools/assembly-preparer/System_Diagnostics_UnreachableException.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from https://raw.githubusercontent.com/dotnet/dotnet/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Diagnostics/UnreachableException.cs + +#if !NET + +namespace System.Diagnostics { + /// + /// Exception thrown when the program executes an instruction that was thought to be unreachable. + /// + public sealed class UnreachableException : Exception { + /// + /// Initializes a new instance of the class with the default error message. + /// + public UnreachableException () + : base ("SR.Arg_UnreachableException") + { + } + + /// + /// Initializes a new instance of the + /// class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public UnreachableException (string? message) + : base (message ?? "SR.Arg_UnreachableException") + { + } + + /// + /// Initializes a new instance of the + /// class with a specified error message and a reference to the inner exception that is the cause of + /// this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public UnreachableException (string? message, Exception? innerException) + : base (message ?? "SR.Arg_UnreachableException", innerException) + { + } + } +} + +#endif // !NET diff --git a/tools/assembly-preparer/System_Index.cs b/tools/assembly-preparer/System_Index.cs new file mode 100644 index 000000000000..9dde742e6c73 --- /dev/null +++ b/tools/assembly-preparer/System_Index.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from https://raw.githubusercontent.com/dotnet/dotnet/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Index.cs +// to make newer compiler features compile in netstandard2.0 + +#if !NET + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System { + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// +#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY + public +#else + internal +#endif + readonly struct Index : IEquatable { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public Index (int value, bool fromEnd = false) + { + if (value < 0) { + ThrowValueArgumentOutOfRange_NeedNonNegNumException (); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index (int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index (0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index (~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static Index FromStart (int value) + { + if (value < 0) { + ThrowValueArgumentOutOfRange_NeedNonNegNumException (); + } + + return new Index (value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public static Index FromEnd (int value) + { + if (value < 0) { + ThrowValueArgumentOutOfRange_NeedNonNegNumException (); + } + + return new Index (~value); + } + + /// Returns the index value. + public int Value { + get { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public int GetOffset (int length) + { + int offset = _value; + if (IsFromEnd) { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals ([NotNullWhen (true)] object? value) => value is Index && _value == ((Index) value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals (Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode () => _value; + + /// Converts integer number to an Index. + public static implicit operator Index (int value) => FromStart (value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString () + { + if (IsFromEnd) + return ToStringFromEnd (); + + return ((uint) Value).ToString (); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException () + { +#if SYSTEM_PRIVATE_CORELIB + throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_NeedNonNegNum); +#else + throw new ArgumentOutOfRangeException ("value", "value must be non-negative"); +#endif + } + + private string ToStringFromEnd () + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char [11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint) Value).TryFormat (span.Slice (1), out int charsWritten); + Debug.Assert (formatted); + span [0] = '^'; + return new string (span.Slice (0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} + +#endif // !NET diff --git a/tools/assembly-preparer/System_Range.cs b/tools/assembly-preparer/System_Range.cs new file mode 100644 index 000000000000..453dd1a4ccc7 --- /dev/null +++ b/tools/assembly-preparer/System_Range.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from https://raw.githubusercontent.com/dotnet/dotnet/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Range.cs +// to make newer compiler features compile in netstandard2.0 + +#if !NET + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#if NETSTANDARD2_0 || NETFRAMEWORK +using System.Numerics.Hashing; +#endif + +namespace System { + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// +#if SYSTEM_PRIVATE_CORELIB || MICROSOFT_BCL_MEMORY + public +#else + internal +#endif + readonly struct Range : IEquatable { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range (Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals ([NotNullWhen (true)] object? value) => + value is Range r && + r.Start.Equals (Start) && + r.End.Equals (End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals (Range other) => other.Start.Equals (Start) && other.End.Equals (End); + + /// Returns the hash code for this instance. + public override int GetHashCode () + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + return HashCode.Combine (Start.GetHashCode (), End.GetHashCode ()); +#elif !NET + return Start.GetHashCode () ^ End.GetHashCode (); +#else + return HashHelpers.Combine(Start.GetHashCode(), End.GetHashCode()); +#endif + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString () + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char [2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) { + span [0] = '^'; + pos = 1; + } + bool formatted = ((uint) Start.Value).TryFormat (span.Slice (pos), out int charsWritten); + Debug.Assert (formatted); + pos += charsWritten; + + span [pos++] = '.'; + span [pos++] = '.'; + + if (End.IsFromEnd) { + span [pos++] = '^'; + } + formatted = ((uint) End.Value).TryFormat (span.Slice (pos), out charsWritten); + Debug.Assert (formatted); + pos += charsWritten; + + return new string (span.Slice (0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt (Index start) => new Range (start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt (Index end) => new Range (Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range (Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl (MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength (int length) + { + int start = Start.GetOffset (length); + int end = End.GetOffset (length); + + if ((uint) end > (uint) length || (uint) start > (uint) end) { + ThrowArgumentOutOfRangeException (); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException () + { + throw new ArgumentOutOfRangeException ("length"); + } + } +} + +#endif // !NET diff --git a/tools/assembly-preparer/assembly-preparer.csproj b/tools/assembly-preparer/assembly-preparer.csproj new file mode 100644 index 000000000000..89a35b82fccf --- /dev/null +++ b/tools/assembly-preparer/assembly-preparer.csproj @@ -0,0 +1,341 @@ + + + + assembly-preparer + net$(BundledNETCoreAppTargetFrameworkVersion);netstandard2.0 + net$(BundledNETCoreAppTargetFrameworkVersion) + $(DefineConstants);BUNDLER;ASSEMBLY_PREPARER + Library + true + latest + enable + + + + + + + + + + + + + + external/tools/common/ApplePlatform.cs + + + external/tools/common/Application.cs + + + external/tools/common/Assembly.cs + + + external/tools/common/AssemblyBuildTarget.cs + + + external/tools/common/cache.cs + + + external/tools/common/CoreResolver.cs + + + external/tools/common/DerivedLinkContext.cs + + + external/tools/common/DlsymOptions.cs + + + external/tools/common/Driver.cs + + + external/tools/common/Driver.execution.cs + + + external/tools/common/error.cs + + + external/tools/common/ErrorHelper.tools.cs + + + external/tools/common/Execution.cs + + + external/tools/common/FileCopier.cs + + + external/tools/common/FileUtils.cs + + + external/tools/common/Frameworks.cs + + + external/tools/common/MachO.cs + + + external/tools/common/NormalizedStringComparer.cs + + + external/tools/common/NullableAttributes.cs + + + external/tools/common/CSToObjCMap.cs + + + external/tools/common/ObjCNameIndex.cs + + + external/tools/common/Optimizations.cs + + + external/tools/common/OSPlatformAttributeExtensions.cs + + + external/tools/common/PInvokeWrapperGenerator.cs + + + external/tools/common/RegistrarMode.cs + + + external/tools/common/PathUtils.cs + + + external/tools/common/PListExtensions.cs + + + external/tools/common/ProductConstants.cs + + + external/tools/common/StaticRegistrar.cs + + + external/tools/common/StringUtils.cs + + + external/tools/common/Symbols.cs + + + external/tools/common/Target.cs + + + external/tools/common/TargetFramework.cs + + + external/tools/common/IToolLog.cs + + + external/tools/common/XamarinRuntime.cs + + + external/tools/dotnet-linker/AppBundleRewriter.cs + + + external/tools/dotnet-linker/ApplyPreserveAttributeBase.cs + + + external/tools/dotnet-linker/ApplyPreserveAttributeStep.cs + + + external/tools/dotnet-linker/Compat.cs + + + external/tools/dotnet-linker/DotNetResolver.cs + + + external/tools/dotnet-linker/Extensions.cs + + + external/tools/dotnet-linker/LinkerConfiguration.cs + + + external/tools/dotnet-linker/MarkForStaticRegistrarStep.cs + + + external/tools/dotnet-linker/MarkNSObjectsStep.cs + + + external/tools/dotnet-linker/OptimizeGeneratedCodeStep.cs + + + external/tools/dotnet-linker/PreserveProtocolsStep.cs + + + external/tools/dotnet-linker/PreserveSmartEnumConversionsStep.cs + + + external/tools/dotnet-linker/Steps/AssemblyModifierStep.cs + + + external/tools/dotnet-linker/Steps/ConfigurationAwareStep.cs + + + external/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs + + + external/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs + + + external/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs + + + external/tools/dotnet-linker/Steps/PreserveBlockCodeStep.cs + + + external/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs + + + external/tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs + + + external/tools/linker/CoreTypeMapStep.cs + + + external/tools/linker/CustomSymbolWriter.cs + + + external/tools/linker/MarkNSObjects.cs + + + external/tools/linker/MobileExtensions.cs + + + external/tools/linker/MonoTouch.Tuner/Extensions.cs + + + external/tools/linker/ObjCExtensions.cs + + + external/tools/linker/OptimizeGeneratedCode.cs + + + external/tools/linker/RegistrarRemovalTrackingStep.cs + + + external/src/Foundation/ExportAttribute.cs + + + external/src/Foundation/ConnectAttribute.cs + + + external/src/ObjCRuntime/ArgumentSemantic.cs + + + external/src/ObjCRuntime/BindingImplAttribute.cs + + + external/src/ObjCRuntime/Constants.cs + + + external/src/ObjCRuntime/ErrorHelper.cs + + + external/src/ObjCRuntime/ExceptionMode.cs + + + external/src/ObjCRuntime/LinkWithAttribute.cs + + + external/src/ObjCRuntime/NativeNameAttribute.cs + + + external/src/ObjCRuntime/Registrar.core.cs + + + external/src/ObjCRuntime/Registrar.cs + + + external/builds/mono-ios-sdk-destdir/ios-sources/external/linker/src/tuner/Mono.Tuner/Extensions.cs + + + external/builds/mono-ios-sdk-destdir/ios-sources/external/linker/src/linker/Linker/MethodDefinitionExtensions.cs + + + mono-archive/Linker/Linker/TypeReferenceExtensions.cs + + + external/builds/mono-ios-sdk-destdir/ios-sources/external/linker/src/tuner/Mono.Tuner/CecilRocks.cs + + + external/tools/dotnet-linker/CecilExtensions.cs + + + external/tools/dotnet-linker/DocumentionComments.cs + + + external/tools/common/SdkVersions.cs + + + + + + external/tools/mtouch/Errors.designer.cs + Errors.resx + + + external/tools/mtouch/Errors.resx + ResXFileCodeGenerator + Errors.designer.cs + Xamarin.Bundler + Xamarin.Bundler.Errors + + + external/tools/mtouch/Errors.cs.resx + Xamarin.Bundler.Errors.cs + + + external/tools/mtouch/Errors.de.resx + Xamarin.Bundler.Errors.de + + + external/tools/mtouch/Errors.es.resx + Xamarin.Bundler.Errors.es + + + external/tools/mtouch/Errors.fr.resx + Xamarin.Bundler.Errors.fr + + + external/tools/mtouch/Errors.it.resx + Xamarin.Bundler.Errors.it + + + external/tools/mtouch/Errors.ja.resx + Xamarin.Bundler.Errors.ja + + + external/tools/mtouch/Errors.ko.resx + Xamarin.Bundler.Errors.ko + + + external/tools/mtouch/Errors.pl.resx + Xamarin.Bundler.Errors.pl + + + external/tools/mtouch/Errors.pt-BR.resx + Xamarin.Bundler.Errors.pt-BR + + + external/tools/mtouch/Errors.ru.resx + Xamarin.Bundler.Errors.ru + + + external/tools/mtouch/Errors.tr.resx + Xamarin.Bundler.Errors.tr + + + external/tools/mtouch/Errors.zh-Hans.resx + Xamarin.Bundler.Errors.zh-Hans + + + external/tools/mtouch/Errors.zh-Hant.resx + Xamarin.Bundler.Errors.zh-Hant + + + + + + + diff --git a/tools/assembly-preparer/assembly-preparer.slnx b/tools/assembly-preparer/assembly-preparer.slnx new file mode 100644 index 000000000000..a2411d955a58 --- /dev/null +++ b/tools/assembly-preparer/assembly-preparer.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/tools/common/Application.cs b/tools/common/Application.cs index 05ffef60c8d9..8da7ac74c63a 100644 --- a/tools/common/Application.cs +++ b/tools/common/Application.cs @@ -20,16 +20,14 @@ using Registrar; -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER using ClassRedirector; #endif #if LEGACY_TOOLS using PlatformResolver = MonoTouch.Tuner.MonoTouchResolver; -#elif NET -using PlatformResolver = Xamarin.Linker.DotNetResolver; #else -#error Invalid defines +using PlatformResolver = Xamarin.Linker.DotNetResolver; #endif #nullable enable @@ -82,6 +80,13 @@ public partial class Application : IToolLog { public List AotArguments = new List (); public List? AotOtherArguments = null; public bool? AotFloat32 = null; + public bool PrepareAssemblies; // True if '$(PrepareAssemblies)' == 'true' +#if ASSEMBLY_PREPARER + public bool InCustomTrimmerStep = false; +#else + public bool InCustomTrimmerStep = true; +#endif + public bool IsPostProcessingAssemblies => PrepareAssemblies && InCustomTrimmerStep; #if !LEGACY_TOOLS public DlsymOptions DlsymOptions; @@ -273,7 +278,11 @@ public Version GetMacCatalystiOSVersion (Version macOSVersion) return value; } +#if !LEGACY_TOOLS public Application (LinkerConfiguration configuration) +#else + public Application () +#endif { #if !LEGACY_TOOLS this.configuration = configuration; @@ -557,6 +566,7 @@ void InitializeDeploymentTarget () } } +#if !ASSEMBLY_PREPARER public void RunRegistrar () { // The static registrar. @@ -623,6 +633,7 @@ public void RunRegistrar () registrar.Generate (resolver, resolvedAssemblies.Values, Path.ChangeExtension (registrar_m, "h"), registrar_m, out var _); } } +#endif // !ASSEMBLY_PREPARER public Abi Abi { get { return abi; } @@ -700,7 +711,7 @@ public void ParseAbi (string abi) } #if !LEGACY_TOOLS - public void ParseRegistrar (string v) + public void ParseRegistrar (string? v) { if (StringUtils.IsNullOrEmpty (v)) return; @@ -708,7 +719,7 @@ public void ParseRegistrar (string v) var split = v.Split ('='); var name = split [0]; var value = split.Length > 1 ? split [1] : string.Empty; - switch (name) { + switch (name.ToLowerInvariant ()) { case "static": Registrar = RegistrarMode.Static; break; @@ -720,12 +731,15 @@ public void ParseRegistrar (string v) break; case "partial": case "partial-static": + case "partialstatic": Registrar = RegistrarMode.PartialStatic; break; case "managed-static": + case "managedstatic": Registrar = RegistrarMode.ManagedStatic; break; case "trimmable-static": + case "trimmablestatic": Registrar = RegistrarMode.TrimmableStatic; break; default: @@ -1200,27 +1214,41 @@ public void SetDefaultHiddenWarnings () ErrorHelper.ParseWarningLevel (this, ErrorHelper.WarningLevel.Disable, "4190"); // The class '{0}' will not be registered because the {1} framework has been deprecated from the {2} SDK. } + IToolLog GetLog () + { +#if LEGACY_TOOLS + return ConsoleLog.Instance; +#else + return Configuration.Logger ?? ConsoleLog.Instance; +#endif + } + public void Log (string message) { - Console.WriteLine (message); + GetLog ().Log (message); } public void LogError (string message) { - Console.Error.WriteLine (message); + GetLog ().LogError (message); + } + + public void LogError (ProductException exception) + { + GetLog ().LogError (exception); } - public void LogError (Exception exception) + public void LogWarning (ProductException exception) { - ErrorHelper.Show (this, exception); + GetLog ().LogWarning (exception); } public void LogException (Exception exception) { - ErrorHelper.Show (this, exception); + GetLog ().LogException (exception); } - int verbosity = Driver.GetDefaultVerbosity (); + int verbosity = Driver.GetDefaultVerbosity (Driver.NAME); public int Verbosity { get => verbosity; set => verbosity = value; diff --git a/tools/common/Assembly.cs b/tools/common/Assembly.cs index 8e100d6d9609..c03e249d68d3 100644 --- a/tools/common/Assembly.cs +++ b/tools/common/Assembly.cs @@ -8,7 +8,6 @@ using System.Xml; using Mono.Cecil; using Mono.Tuner; -using MonoTouch.Tuner; using ObjCRuntime; using Xamarin; using Xamarin.Utils; @@ -58,6 +57,7 @@ public partial class Assembly { public AssemblyDefinition AssemblyDefinition; public bool? IsFrameworkAssembly { get { return is_framework_assembly; } } + public string FullPath { get { return full_path; @@ -66,7 +66,10 @@ public string FullPath { set { full_path = value; if (!is_framework_assembly.HasValue && !string.IsNullOrEmpty (full_path)) { -#if !LEGACY_TOOLS +#if ASSEMBLY_PREPARER + is_framework_assembly = false; // silence compiler warning + throw new InvalidOperationException (); +#elif !LEGACY_TOOLS is_framework_assembly = App.Configuration.FrameworkAssemblies.Contains (GetIdentity (full_path)); #else var real_full_path = Application.GetRealPath (App, full_path); @@ -126,7 +129,7 @@ public void LoadSymbols () symbols_loaded = false; try { var pdb = Path.ChangeExtension (FullPath, ".pdb"); - if (File.Exists (pdb)) { + if (File.Exists (pdb) && !string.IsNullOrEmpty (AssemblyDefinition.MainModule.FileName)) { AssemblyDefinition.MainModule.ReadSymbols (); symbols_loaded = true; } diff --git a/tools/common/CoreResolver.cs b/tools/common/CoreResolver.cs index b35f575c9fc1..d869736ee3ba 100644 --- a/tools/common/CoreResolver.cs +++ b/tools/common/CoreResolver.cs @@ -4,6 +4,8 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using Xamarin.Utils; + #nullable enable namespace Xamarin.Bundler { diff --git a/tools/common/DerivedLinkContext.cs b/tools/common/DerivedLinkContext.cs index 0f639be2e7b5..e6f21acdc947 100644 --- a/tools/common/DerivedLinkContext.cs +++ b/tools/common/DerivedLinkContext.cs @@ -6,12 +6,13 @@ using Mono.Collections.Generic; using Registrar; + using Mono.Tuner; using Xamarin.Bundler; using Xamarin.Linker; using Xamarin.Utils; -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER using LinkContext = Xamarin.Bundler.DotNetLinkContext; #endif @@ -65,6 +66,11 @@ public DerivedLinkContext (LinkerConfiguration configuration, Application app) } AssemblyDefinition? corlib; + +#if !LEGACY_TOOLS + public RegistrarMode Registrar => App.Registrar; +#endif // !LEGACY_TOOLS + public AssemblyDefinition Corlib { get { if (corlib is null) { @@ -341,6 +347,15 @@ public bool HasAvailabilityAttributesShowingUnavailableInSimulator (ICustomAttri return false; } + public AssemblyDefinition GetProductAssembly () + { + var productAssemblyName = Driver.GetProductAssembly (App); + var rv = this.GetAssembly (productAssemblyName); + if (rv is null) + throw ErrorHelper.CreateError (1504, Errors.MX1504 /* Can not find the product assembly '{0}' in the list of loaded assemblies. */, productAssemblyName); + return rv; + } + class AttributeStorage : ICustomAttribute { public CustomAttribute Attribute { get; } public TypeReference AttributeType { get; set; } diff --git a/tools/common/Driver.cs b/tools/common/Driver.cs index fe912e00e4a8..37a4cbd8bf8d 100644 --- a/tools/common/Driver.cs +++ b/tools/common/Driver.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Text; +using Xamarin.Bundler; using Xamarin.MacDev; using Xamarin.Utils; @@ -80,14 +81,14 @@ static void ParseOptions (Application app, Mono.Options.OptionSet options, strin } #endif // !LEGACY_TOOLS - public static int GetDefaultVerbosity () + public static int GetDefaultVerbosity (string toolName) { var v = 0; - var fn = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), $".{NAME}-verbosity"); + var fn = Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), $".{toolName}-verbosity"); if (File.Exists (fn)) { v = (int) new FileInfo (fn).Length; if (v == 0) - v = 4; // this is the magic verbosity level we give everybody. + v = 4; // this is the magic verbosity level we give everybody if the file exists, but has no size. } return v; } diff --git a/tools/common/ErrorHelper.tools.cs b/tools/common/ErrorHelper.tools.cs index e930f95731f6..7f29a8980733 100644 --- a/tools/common/ErrorHelper.tools.cs +++ b/tools/common/ErrorHelper.tools.cs @@ -38,6 +38,12 @@ public enum WarningLevel { static ConditionalWeakTable> warning_levels = new (); + public static Dictionary? GetWarningLevels (IToolLog log) + { + warning_levels.TryGetValue (log, out var log_warning_levels); + return log_warning_levels; + } + public static WarningLevel GetWarningLevel (IToolLog log, int code) { if (warning_levels.TryGetValue (log, out var log_warning_levels)) { diff --git a/tools/common/IToolLog.cs b/tools/common/IToolLog.cs index 758cb136eaf4..2f9fd150a3ac 100644 --- a/tools/common/IToolLog.cs +++ b/tools/common/IToolLog.cs @@ -1,3 +1,7 @@ +#if BGENERATOR +using ProductException = BindingException; +#endif + using Xamarin.Utils; namespace Xamarin.Bundler; @@ -8,7 +12,8 @@ public interface IToolLog { void Log (string message); void LogError (string message); // Log an error we raise ourselves (through an exception) - void LogError (Exception exception); + void LogError (ProductException exception); + void LogWarning (ProductException exception); // Log an unexpected exception void LogException (Exception exception); } @@ -42,8 +47,10 @@ public class ConsoleLog : IToolLog { #if TESTS int verbosity = 0; -#else +#elif BGENERATOR int verbosity = Driver.GetDefaultVerbosity (); +#else + int verbosity = Driver.GetDefaultVerbosity (Driver.NAME); #endif public int Verbosity { get => verbosity; } @@ -60,11 +67,16 @@ public void LogError (string message) Console.Error.WriteLine (message); } - public void LogError (Exception exception) + public void LogError (ProductException exception) { Console.Error.WriteLine (exception); } + public void LogWarning (ProductException exception) + { + Console.WriteLine (exception); + } + public void LogException (Exception exception) { Console.Error.WriteLine (exception); diff --git a/tools/common/Make.common b/tools/common/Make.common index a83dd8a8c63b..d2dddec4ea22 100644 --- a/tools/common/Make.common +++ b/tools/common/Make.common @@ -3,6 +3,7 @@ LAST_XCODE_BUMP:=$(shell git blame -- $(TOP)/Make.config HEAD | grep " XCODE_VERSION=" | sed 's/ .*//') XCODE_BUMP_COMMIT_DISTANCE:=$(shell git log "$(LAST_XCODE_BUMP)..HEAD" --oneline | wc -l | sed -e 's/ //g') +all-local:: $(abspath ../common/SdkVersions.cs) $(abspath ../common/SdkVersions.cs): ../common/SdkVersions.in.cs Makefile $(TOP)/Make.config $(TOP)/Make.versions $(Q_GEN) sed \ -e 's/@IOS_SDK_VERSION@/$(IOS_SDK_VERSION)/g' -e 's/@TVOS_SDK_VERSION@/$(TVOS_SDK_VERSION)/' -e 's/@MACOS_SDK_VERSION@/$(MACOS_SDK_VERSION)/' \ @@ -49,6 +50,7 @@ $(abspath ../common/SdkVersions.cs): ../common/SdkVersions.in.cs Makefile $(TOP) $(Q) if ! diff $@ $@.tmp >/dev/null; then $(CP) $@.tmp $@; git diff "$@"; echo "The file $(TOP)/tools/common/SdkVersions.cs has been automatically re-generated; please commit the changes."; exit 1; fi $(Q) touch $@ +all-local:: $(abspath ../common/ProductConstants.cs) $(abspath ../common/ProductConstants.cs): ../common/ProductConstants.in.cs Makefile $(TOP)/Make.config $(GIT_DIRECTORY)/index $(Q_GEN) sed \ $(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),-e 's/@$(platform)_REVISION@/$($(platform)_COMMIT_DISTANCE) ($(CURRENT_BRANCH_SED_ESCAPED): $(CURRENT_HASH))/g') \ @@ -60,3 +62,4 @@ $(abspath ../common/ProductConstants.cs): ../common/ProductConstants.in.cs Makef -e "s/@XCODE_VERSION@/$(XCODE_VERSION)/g" \ -e "s/@XCODE_BUMP_COMMIT_DISTANCE@/$(XCODE_BUMP_COMMIT_DISTANCE)/g" \ $< > $@ + diff --git a/tools/common/Makefile b/tools/common/Makefile new file mode 100644 index 000000000000..6a27eba55f77 --- /dev/null +++ b/tools/common/Makefile @@ -0,0 +1,5 @@ +TOP=../.. + +include $(TOP)/Make.config +include $(TOP)/mk/rules.mk +include ../common/Make.common diff --git a/tools/common/NullableAttributes.cs b/tools/common/NullableAttributes.cs index dc8b0d8591ed..3f54b47e7c72 100644 --- a/tools/common/NullableAttributes.cs +++ b/tools/common/NullableAttributes.cs @@ -44,6 +44,24 @@ internal sealed class MemberNotNullAttribute : Attribute { /// Gets field or property member names. public string [] Members { get; } } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage (AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute (bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage (AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + } #endif // !NET diff --git a/tools/common/Optimizations.cs b/tools/common/Optimizations.cs index d243a9b39415..133bb0406d44 100644 --- a/tools/common/Optimizations.cs +++ b/tools/common/Optimizations.cs @@ -172,7 +172,7 @@ public void Initialize (Application app, out List messages) continue; // The remove-dynamic-registrar optimization is required when using NativeAOT - if (app.XamarinRuntime == XamarinRuntime.NativeAOT && (Opt) i == Opt.RemoveDynamicRegistrar && values [i] == false) { + if (app.XamarinRuntime == XamarinRuntime.NativeAOT && (Opt) i == Opt.RemoveDynamicRegistrar && value == false) { messages.Add (ErrorHelper.CreateWarning (2016, Errors.MX2016 /* Keeping the dynamic registrar (by passing '--optimize=-remove-dynamic-registrar') is not possible, because the dynamic registrar is not supported when using NativeAOT. Support for dynamic registration will still be removed. */)); values [i] = true; continue; diff --git a/tools/common/StaticRegistrar.cs b/tools/common/StaticRegistrar.cs index f6b55ec0a015..fc2f5d3f7da3 100644 --- a/tools/common/StaticRegistrar.cs +++ b/tools/common/StaticRegistrar.cs @@ -2735,7 +2735,7 @@ public CSToObjCMap GetTypeMapDictionary (List exceptions) public void Rewrite () { -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER if (App.Optimizations.RedirectClassHandles == true) { var exceptions = new List (); var map_dict = GetTypeMapDictionary (exceptions); @@ -3964,7 +3964,7 @@ void Specialize (AutoIndentStringBuilder sb, ObjCMethod method, List nslog_start.AppendLine (");"); } -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER // Generate the native trampoline to call the generated UnmanagedCallersOnly method if we're using the managed static registrar. if (LinkContext.App.Registrar == RegistrarMode.ManagedStatic || LinkContext.App.Registrar == RegistrarMode.TrimmableStatic) { GenerateCallToUnmanagedCallersOnlyMethod (sb, method, isCtor, isVoid, num_arg, descriptiveMethodName, exceptions); @@ -4194,7 +4194,7 @@ void Specialize (AutoIndentStringBuilder sb, ObjCMethod method, List } } -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER void GenerateCallToUnmanagedCallersOnlyMethod (AutoIndentStringBuilder sb, ObjCMethod method, bool isCtor, bool isVoid, int num_arg, string descriptiveMethodName, List exceptions) { // Generate the native trampoline to call the generated UnmanagedCallersOnly method. @@ -5170,7 +5170,7 @@ bool TryCreateTokenReferenceUncached (MemberReference member, TokenType implied_ { var token = member.MetadataToken; -#if !LEGACY_TOOLS +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER if (App.Registrar == RegistrarMode.TrimmableStatic) throw ErrorHelper.CreateError (99, $"Can't create a token reference when using the trimmable static registrar (for: {member.FullName})"); @@ -5388,7 +5388,7 @@ public void Register (PlatformResolver? resolver, IEnumerable r.Args) + .OfType () + .Where (r => r.SenderName == "BinaryLogger" && r.Message?.StartsWith ("BinLogFilePath=") == true) + .Select (v => v.Message?.Substring ("BinLogFilePath=".Length) ?? "") + .SingleOrDefault (); + var originalBinlogDirectory = Path.GetDirectoryName (originalBinlogPath)!; + foreach (var record in records) { if (record is null) continue; @@ -60,14 +73,99 @@ static int Main (string [] args) if (record.Args is null) continue; - if (record.Args is TaskStartedEventArgs tsea && tsea.TaskName == "ILLink") { + if (record.Args is not TaskStartedEventArgs tsea) + continue; + + switch (tsea.TaskName) { + case "PrepareAssemblies": { + var taskId = tsea.BuildEventContext?.TaskId; + if (taskId is null) + continue; + var relevantRecords = records. + Select (v => v.Args). + Where (v => v is not null). + Where (v => v.BuildEventContext?.TaskId == taskId). + ToArray (); + + var taskParameters = relevantRecords.Where (v => v is TaskParameterEventArgs).Cast ().ToArray (); + + string? getProperty (string name) + { + var param = taskParameters.SingleOrDefault (v => v.ItemType == name); + if (param is null) + return null; + if (param.Items is null) + return null; + if (param.Items.Count != 1) + return null; + var item = param.Items [0]; + if (item is null) + return null; + return ((ITaskItem) item).ItemSpec; + } + ITaskItem []? getItems (string name) + { + var param = taskParameters.SingleOrDefault (v => v.ItemType == name); + if (param is null) + return null; + if (param.Items is null) + return null; + return param.Items.Cast ().ToArray (); + } + var outputDirectory = getProperty ("OutputDirectory"); + var optionsFile = getProperty ("OptionsFile"); + var targetFrameworkMoniker = getProperty ("TargetFrameworkMoniker"); + var makeReproPath = getProperty ("MakeReproPath"); + var inputAssemblies = getItems ("InputAssemblies"); + + if (string.IsNullOrEmpty (outputDirectory)) + throw new InvalidOperationException ("OutputDirectory is required"); + outputDirectory = Path.GetFullPath (outputDirectory, originalBinlogDirectory); + + if (string.IsNullOrEmpty (optionsFile)) + throw new InvalidOperationException ("OptionsFile is required"); + optionsFile = Path.GetFullPath (optionsFile, originalBinlogDirectory); + + if (string.IsNullOrEmpty (targetFrameworkMoniker)) + throw new InvalidOperationException ("TargetFrameworkMoniker is required"); + + if (inputAssemblies is null || inputAssemblies.Length == 0) + throw new InvalidOperationException ("InputAssemblies is required"); + + string GetAssemblyInfo (ITaskItem item) + { + var inputPath = Path.GetFullPath (item.ItemSpec, originalBinlogDirectory); + var outputPath = Path.Combine (outputDirectory, Path.GetFileName (inputPath)); + var metadataNames = item.MetadataNames.Cast ().Select (v => v.ToLowerInvariant ()).ToHashSet (); + var isTrimmableString = item.GetMetadata ("IsTrimmable"); + var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase); + var trimMode = item.GetMetadata ("TrimMode"); + + return $"InputPath={inputPath}|OutputPath={outputPath}|IsTrimmable={isTrimmable}|TrimMode={trimMode}"; + } + + var launcherArgs = new List (); + launcherArgs.Add ("${workspaceFolder}/bin/Debug/ap-launcher.dll"); + if (!string.IsNullOrEmpty (makeReproPath)) + launcherArgs.Add ("--make-repro=" + makeReproPath); + if (!string.IsNullOrEmpty (optionsFile)) + launcherArgs.Add ("--options-file=" + optionsFile); + + foreach (var ia in inputAssemblies) { + launcherArgs.Add ("--input-assembly=" + GetAssemblyInfo (ia)); + } + + WriteApLauncherLaunchJson (CreateLaunchJson (rootDirectory, launcherArgs.ToArray ())); + + break; + } + case "ILLink": { if (skippedLinkerCommands > 0) { Console.WriteLine ($"Skipped an ILLink task invocation, {skippedLinkerCommands} left to skip..."); skippedLinkerCommands--; continue; } - var relevantRecords = records.Where (v => v?.Args?.BuildEventContext?.TaskId == tsea.BuildEventContext?.TaskId).Select (v => v.Args).ToArray (); var cla = relevantRecords.Where (v => v is BuildMessageEventArgs).Cast ().Where (v => v?.ToString ()?.Contains ("CommandLineArguments") == true).ToArray (); foreach (var rr in relevantRecords) { @@ -77,10 +175,12 @@ static int Main (string [] args) return 1; } - WriteLaunchJson (CreateLaunchJson (rootDirectory, arguments)); + WriteDotNetLinkerLaunchJson (CreateLaunchJson (rootDirectory, arguments)); return 0; } } + break; + } } } @@ -88,12 +188,22 @@ static int Main (string [] args) return 1; } - static void WriteLaunchJson (string contents) + static void WriteApLauncherLaunchJson (string contents) + { + WriteLaunchJson ("ap-launcher", contents); + } + + static void WriteDotNetLinkerLaunchJson (string contents) + { + WriteLaunchJson ("dotnet-linker", contents); + } + + static void WriteLaunchJson (string toolName, string contents) { var dir = Environment.CurrentDirectory!; - while (!Directory.Exists (Path.Combine (dir, "tools", "dotnet-linker"))) + while (!Directory.Exists (Path.Combine (dir, "tools", toolName))) dir = Path.GetDirectoryName (dir)!; - var path = Path.Combine (dir, "tools", "dotnet-linker", ".vscode", "launch.json"); + var path = Path.Combine (dir, "tools", toolName, ".vscode", "launch.json"); File.WriteAllText (path, contents); Console.WriteLine ($"Created {path}"); } diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index 1914b02994d7..e9257f6727a5 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -80,6 +80,8 @@ public AppBundleRewriter (LinkerConfiguration configuration) // Find corlib and the platform assemblies foreach (var asm in configuration.Assemblies) { if (asm.Name.Name == Driver.CorlibName) { + if (corlib_assembly is not null) + throw new InvalidOperationException ($"Already have a corlib assembly named {corlib_assembly.Name}"); corlib_assembly = asm; } else if (asm.Name.Name == configuration.PlatformAssembly) { platform_assembly = asm; @@ -250,6 +252,13 @@ public TypeReference System_Exception { return GetTypeReference (CorlibAssembly, "System.Exception", out var _); } } + + public TypeReference System_GC { + get { + return GetTypeReference (CorlibAssembly, "System.GC", out var _); + } + } + public TypeReference System_Int32 { get { return CurrentAssembly.MainModule.ImportReference (CorlibAssembly.MainModule.TypeSystem.Int32); @@ -528,6 +537,17 @@ public MethodReference System_Console__WriteLine_String_Object { } } + public MethodReference System_GC__KeepAlive { + get { + return GetMethodReference (CorlibAssembly, System_GC, "KeepAlive", (v) => + v.IsStatic + && v.HasParameters + && v.Parameters.Count == 1 + && v.Parameters [0].ParameterType.Is ("System", "Object") + && !v.HasGenericParameters); + } + } + public MethodReference System_Object__ctor { get { return GetMethodReference (CorlibAssembly, System_Object, ".ctor", (v) => v.IsDefaultConstructor ()); @@ -559,6 +579,12 @@ public MethodReference Nullable_Value { } } + public MethodReference Nullable_ctor { + get { + return GetMethodReference (CorlibAssembly, System_Nullable_1, ".ctor", isStatic: false, System_Nullable_1.GenericParameters [0]); + } + } + public MethodReference Type_GetTypeFromHandle { get { return GetMethodReference (CorlibAssembly, System_Type, "GetTypeFromHandle", isStatic: true, System_RuntimeTypeHandle); @@ -1403,7 +1429,6 @@ public MethodReference Unsafe_AsRef { } } -#if NET public bool TryGet_NSObject_RegisterToggleRef ([NotNullWhen (true)] out MethodDefinition? md) { // the NSObject.RegisterToggleRef method isn't present on all platforms (for example on Mac) @@ -1415,7 +1440,6 @@ public bool TryGet_NSObject_RegisterToggleRef ([NotNullWhen (true)] out MethodDe return false; } } -#endif public void SetCurrentAssembly (AssemblyDefinition value) { @@ -1434,6 +1458,7 @@ void SaveAssembly (AssemblyDefinition assembly) var annotations = configuration.Context.Annotations; var action = annotations.GetAction (assembly); if (action == AssemblyAction.Copy) { +#if !ASSEMBLY_PREPARER // Preserve TypeForwardedTo which would the linker sweep otherwise // Note that the linker will sweep type forwarders even if the assembly isn't trimmed: // https://github.com/dotnet/runtime/blob/9dd59af3aee2f403e63887afef50d98022a2e575/src/tools/illink/src/linker/Linker.Steps/SweepStep.cs#L191-L200 @@ -1442,6 +1467,7 @@ void SaveAssembly (AssemblyDefinition assembly) annotations.Mark (type); } } +#endif // !ASSEMBLY_PREPARER annotations.SetAction (assembly, AssemblyAction.Save); } } @@ -1457,9 +1483,11 @@ public void ClearCurrentAssembly () public CustomAttribute CreateAttribute (MethodReference constructor) { +#if !ASSEMBLY_PREPARER // For some reason the trimmer doesn't mark attribute constructors // This is probably only needed when running as a custom linker step. configuration.Context.Annotations.Mark (constructor.Resolve ()); +#endif // !ASSEMBLY_PREPARER return new CustomAttribute (constructor); } @@ -1478,8 +1506,7 @@ public bool AddDynamicDependencyAttribute (MethodDefinition addToMethod, MethodD return false; if (addToMethod.DeclaringType == dependsOn.DeclaringType) { - var attribute = CreateAttribute (DynamicDependencyAttribute_ctor__String); - attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_String, DocumentationComments.GetSignature (dependsOn))); + var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn)); return AddAttributeOnlyOnce (addToMethod, attribute); } else if (addToMethod.DeclaringType.Module == dependsOn.DeclaringType.Module) { var attribute = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (dependsOn), dependsOn.DeclaringType); @@ -1501,11 +1528,23 @@ public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, return attribute; } + public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature) + { + var attribute = CreateAttribute (DynamicDependencyAttribute_ctor__String); + attribute.ConstructorArguments.Add (new CustomAttributeArgument (System_String, memberSignature)); + return attribute; + } + public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, TypeDefinition type, AssemblyDefinition assembly) { return CreateDynamicDependencyAttribute (memberSignature, DocumentationComments.GetSignature (type), assembly.Name.Name); } + public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, string typeName, AssemblyDefinition assembly) + { + return CreateDynamicDependencyAttribute (memberSignature, typeName, assembly.Name.Name); + } + public CustomAttribute CreateDynamicDependencyAttribute (string memberSignature, string typeName, string assemblyName) { var attribute = CreateAttribute (DynamicDependencyAttribute_ctor__String_String_String); @@ -1531,7 +1570,16 @@ public CustomAttribute CreateDynamicDependencyAttribute (DynamicallyAccessedMemb /// The method that is the target of the dynamic dependency. public bool AddDynamicDependencyAttributeToStaticConstructor (TypeDefinition onType, MethodDefinition forMethod) { - var attrib = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (forMethod), forMethod.DeclaringType, forMethod.Module.Assembly); + CustomAttribute attrib; + + if (onType == forMethod.DeclaringType) { + attrib = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (forMethod)); + } else if (onType.Module == forMethod.DeclaringType.Module) { + attrib = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (forMethod), forMethod.DeclaringType); + } else { + attrib = CreateDynamicDependencyAttribute (DocumentationComments.GetSignature (forMethod), forMethod.DeclaringType, forMethod.Module.Assembly); + } + return AddAttributeToStaticConstructor (onType, attrib); } @@ -1580,13 +1628,6 @@ public bool AddAttributeToStaticConstructor (TypeDefinition onType, CustomAttrib { var cctor = GetOrCreateStaticConstructor (onType, out var modified); modified |= AddAttributeOnlyOnce (cctor, attribute); - - // Remove the BeforeFieldInit attribute from the type, otherwise the linker may trim away the static constructor, and taking our attributes with it. - if (onType.Attributes.HasFlag (TypeAttributes.BeforeFieldInit)) { - onType.Attributes &= ~TypeAttributes.BeforeFieldInit; - modified = true; - } - return modified; } @@ -1599,10 +1640,22 @@ public MethodDefinition GetOrCreateStaticConstructor (TypeDefinition type, out b staticCtor = type.AddMethod (".cctor", MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.Static, System_Void); staticCtor.CreateBody (out var il); il.Emit (OpCodes.Ret); + modified = true; + } + // Remove the BeforeFieldInit attribute from the type, otherwise the linker may trim away the static constructor, and taking our attributes with it. + if (type.Attributes.HasFlag (TypeAttributes.BeforeFieldInit)) { + type.Attributes &= ~TypeAttributes.BeforeFieldInit; modified = true; } + if (!staticCtor.Body.Instructions.Any (v => v.OpCode != OpCodes.Ret && v.OpCode != OpCodes.Nop)) { + // FIXME: improve workaround. + var body = staticCtor.Body; + body.Instructions.Insert (0, Instruction.Create (OpCodes.Call, this.System_GC__KeepAlive)); + body.Instructions.Insert (0, Instruction.Create (OpCodes.Ldnull)); + } + return staticCtor; } diff --git a/tools/dotnet-linker/ApplyPreserveAttributeBase.cs b/tools/dotnet-linker/ApplyPreserveAttributeBase.cs index 1d4bc9520977..7135c70ffd90 100644 --- a/tools/dotnet-linker/ApplyPreserveAttributeBase.cs +++ b/tools/dotnet-linker/ApplyPreserveAttributeBase.cs @@ -12,7 +12,7 @@ #nullable enable namespace Xamarin.Linker.Steps { - +#if !ASSEMBLY_PREPARER public partial class ApplyPreserveAttribute : ConfigurationAwareSubStep, IApplyPreserveAttribute { ApplyPreserveAttributeImpl impl; @@ -64,6 +64,7 @@ bool IApplyPreserveAttribute.PreserveConditional (TypeDefinition onType, MethodD return true; } } +#endif public interface IApplyPreserveAttribute { bool PreserveType (TypeDefinition type, bool allMembers); diff --git a/tools/dotnet-linker/ApplyPreserveAttributeStep.cs b/tools/dotnet-linker/ApplyPreserveAttributeStep.cs index 18436ab2de88..f1d4025b7bfa 100644 --- a/tools/dotnet-linker/ApplyPreserveAttributeStep.cs +++ b/tools/dotnet-linker/ApplyPreserveAttributeStep.cs @@ -45,7 +45,11 @@ public bool CreateXmlDescriptionFile { } } +#if ASSEMBLY_PREPARER + public bool UseXmlDescriptionFile { get; set; } +#else public bool UseXmlDescriptionFile { get; set; } = true; +#endif public string XmlDescriptionPath { get; set; } = string.Empty; public ApplyPreserveAttributeStep () @@ -316,6 +320,7 @@ void WriteXmlDescription () Configuration.WriteOutputForMSBuild ("TrimmerRootDescriptor", items); } +#if !ASSEMBLY_PREPARER // The current linker run still needs these roots immediately. Writing the TrimmerRootDescriptor item only // makes the descriptor available to MSBuild after this step has already finished running. var applyXmlStepType = Context.GetType ().Assembly.GetType ("Mono.Linker.Steps.ResolveFromXmlStep"); @@ -326,6 +331,7 @@ void WriteXmlDescription () } else { throw ErrorHelper.CreateError (99, $"Unable to find Mono.Linker.Steps.ResolveFromXmlStep to apply the generated XML description file {xmlPath}"); } +#endif } } } diff --git a/tools/dotnet-linker/Compat.cs b/tools/dotnet-linker/Compat.cs index 491446a4a4b8..dfe1ab3f489d 100644 --- a/tools/dotnet-linker/Compat.cs +++ b/tools/dotnet-linker/Compat.cs @@ -100,7 +100,7 @@ public AnnotationStore Annotations { } } - public AssemblyDefinition GetAssembly (string name) + public AssemblyDefinition? GetAssembly (string name) { return LinkerConfiguration.Context.GetLoadedAssembly (name); } diff --git a/tools/dotnet-linker/DotNetGlobals.cs b/tools/dotnet-linker/DotNetGlobals.cs index 7642a580fb19..46c6a32dbe19 100644 --- a/tools/dotnet-linker/DotNetGlobals.cs +++ b/tools/dotnet-linker/DotNetGlobals.cs @@ -4,6 +4,7 @@ global using System; global using System.Collections.Generic; global using System.Diagnostics.CodeAnalysis; +global using System.Linq; global using System.Runtime.InteropServices; global using Foundation; diff --git a/tools/dotnet-linker/DotNetResolver.cs b/tools/dotnet-linker/DotNetResolver.cs index f71c2ec5b4cb..f989c68c5076 100644 --- a/tools/dotnet-linker/DotNetResolver.cs +++ b/tools/dotnet-linker/DotNetResolver.cs @@ -14,6 +14,12 @@ public DotNetResolver (Application app) public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderParameters parameters) { +#if ASSEMBLY_PREPARER + if (cache.TryGetValue (name.Name, out var assembly)) +#else + if (cache.TryGetValue (name.Name, out var assembly) && assembly.Name.FullName == name.FullName) +#endif + return assembly; throw new NotImplementedException ($"Unable to resolve the assembly reference {name}"); } } diff --git a/tools/dotnet-linker/LinkerConfiguration.cs b/tools/dotnet-linker/LinkerConfiguration.cs index 1ad78a1a8313..8c7e6b148dd7 100644 --- a/tools/dotnet-linker/LinkerConfiguration.cs +++ b/tools/dotnet-linker/LinkerConfiguration.cs @@ -23,6 +23,7 @@ public class LinkerConfiguration { public Abi Abi = Abi.None; public string AOTCompiler = string.Empty; public string AOTOutputDirectory = string.Empty; + public string AssemblyPublishDir = string.Empty; public string DedupAssembly = string.Empty; public string CacheDirectory { get; private set; } = string.Empty; public Version? DeploymentTarget { get; private set; } @@ -43,10 +44,13 @@ public class LinkerConfiguration { public string PartialStaticRegistrarLibrary { get; set; } = string.Empty; public ApplePlatform Platform { get; private set; } public string PlatformAssembly { get; private set; } = string.Empty; + public bool PublishTrimmed { get; private set; } public string RelativeAppBundlePath { get; private set; } = string.Empty; public Version? SdkVersion { get; private set; } public string SdkRootDirectory { get; private set; } = string.Empty; public string TypeMapFilePath { get; set; } = string.Empty; + public string TrimMode { get; private set; } = string.Empty; + public string UnmanagedCallersOnlyMapPath { get; private set; } = string.Empty; public int Verbosity => Application.Verbosity; public string XamarinNativeLibraryDirectory { get; private set; } = string.Empty; @@ -54,17 +58,41 @@ public class LinkerConfiguration { public Application Application { get; private set; } + public IToolLog Logger { get; private set; } + public IList RegistrationMethods { get; set; } = new List (); public List NativeCodeToCompileAndLink { get; private set; } = new List (); +#if !ASSEMBLY_PREPARER public CompilerFlags CompilerFlags; +#endif + +#if ASSEMBLY_PREPARER + List exceptions = new List (); + public List Exceptions { + get { + return exceptions; + } + } + public DotNetResolver AssemblyResolver { get; private set; } + public IMetadataResolver MetadataResolver { get; private set; } +#endif +#if ASSEMBLY_PREPARER + public LinkContext Context { get => DerivedLinkContext; } +#else LinkContext? context; public LinkContext Context { get => context!; private set { context = value; } } +#endif public DerivedLinkContext DerivedLinkContext { get => Application.LinkContext; } public Profile Profile { get; private set; } +#if ASSEMBLY_PREPARER + public List Assemblies => Application.LinkContext.Assemblies; + public List<(string Path, AssemblyDefinition Assembly, string? OriginatingAssembly)> AddedAssemblies = new (); +#else // The list of assemblies is populated in CollectAssembliesStep. public List Assemblies = new List (); +#endif string? user_optimize_flags; @@ -93,23 +121,29 @@ public AssemblyDefinition EntryAssembly { // This dictionary contains information about the trampolines created for each assembly. public AssemblyTrampolineInfos AssemblyTrampolineInfos = new (); + // ASSEMBLY_PREPARER TODO move pinvoke wrapper generation out of ListExportedFields step (and remove the #pragma warning) +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null internal PInvokeWrapperGenerator? PInvokeWrapperGenerationState; +#pragma warning restore CS0649 public static bool TryGetInstance (LinkContext context, [NotNullWhen (true)] out LinkerConfiguration? configuration) { return configurations.TryGetValue (context, out configuration); } - public static LinkerConfiguration GetInstance (LinkContext context) { if (!TryGetInstance (context, out var instance)) { +#if ASSEMBLY_PREPARER + throw new InvalidOperationException ($"No LinkerConfiguration instance found for the given LinkContext."); +#else if (!context.TryGetCustomData ("LinkerOptionsFile", out var linker_options_file)) throw new Exception ($"No custom linker options file was passed to the linker (using --custom-data LinkerOptionsFile=..."); - instance = new LinkerConfiguration (linker_options_file) { + instance = new LinkerConfiguration (ConsoleLog.Instance, linker_options_file) { Context = context, }; configurations.Add (context, instance); +#endif } return instance; @@ -147,6 +181,7 @@ Configurator GetConfigurator (string linker_file) if (!TryParseOptionalBoolean (value, out result)) throw new InvalidOperationException ($"Unable to parse the {key} value: {value} in {linker_file}"); }); + var loadWarningLevel = new Action ((key, value, level) => { try { ErrorHelper.ParseWarningLevel (Application, level, value); @@ -177,6 +212,11 @@ Configurator GetConfigurator (string linker_file) new LoadValue ((key, value) => Application.RootAssemblies.Add (value)), new SaveValue ((key, storage) => storage.AddRange (Application.RootAssemblies.Select (v => $"{key}={v}"))) )}, + { "AssemblyPublishDir", ( + // This is the AssemblyPublishDir MSBuild property for the main project + new LoadValue ((key, value) => AssemblyPublishDir = value), + new SaveValue ((key, storage) => saveNonEmpty (key, AssemblyPublishDir, storage)) + )}, { "AOTArgument", ( new LoadValue ((key, value) => { @@ -511,6 +551,10 @@ Configurator GetConfigurator (string linker_file) }), new SaveValue ((key, storage) => saveNonEmpty (key, Application.TargetFramework.ToString (), storage)) )}, + { "TrimMode", ( + new LoadValue ((key, value) => TrimMode = value), + new SaveValue ((key, storage) => saveNonEmpty (key, TrimMode, storage)) + )}, { "TypeMapAssemblyName", ( new LoadValue ((key, value) => Application.TypeMapAssemblyName = value), new SaveValue ((key, storage) => saveNonEmpty (key, Application.TypeMapAssemblyName, storage)) @@ -523,6 +567,10 @@ Configurator GetConfigurator (string linker_file) new LoadValue ((key, value) => Application.TypeMapOutputDirectory = value), new SaveValue ((key, storage) => saveNonEmpty (key, Application.TypeMapOutputDirectory, storage)) )}, + { "UnmanagedCallersOnlyMapPath", ( + new LoadValue ((key, value) => UnmanagedCallersOnlyMapPath = value), + new SaveValue ((key, storage) => saveNonEmpty (key, UnmanagedCallersOnlyMapPath, storage)) + )}, { "UseLlvm", ( new LoadValue ((key, value) => { var use_llvm = string.Equals ("true", value, StringComparison.OrdinalIgnoreCase); @@ -580,28 +628,42 @@ Configurator GetConfigurator (string linker_file) return dict; } - LinkerConfiguration (string linker_file) + public LinkerConfiguration (IToolLog log, string linker_file, Configurator? customConfigurator = null) + : this (log, File.ReadAllLines (linker_file).ToList (), linker_file, customConfigurator) { - if (!File.Exists (linker_file)) - throw new FileNotFoundException ($"The custom linker file {linker_file} does not exist."); + } + + public LinkerConfiguration (IToolLog log, List lines, string linker_file, Configurator? customConfigurator = null) + { + this.Logger = log; LinkerFile = linker_file; Profile = new BaseProfile (this); Application = new Application (this); + +#if ASSEMBLY_PREPARER + AssemblyResolver = new DotNetResolver (Application); + MetadataResolver = new MetadataResolver (AssemblyResolver); + + configurations.Add (this.Context, this); +#endif + +#if !ASSEMBLY_PREPARER CompilerFlags = new CompilerFlags (Application); +#endif var configurator = GetConfigurator (linker_file); - var lines = File.ReadAllLines (linker_file); + var significantLines = new List (); // This is the input the cache uses to verify if the cache is still valid - for (var i = 0; i < lines.Length; i++) { + for (var i = 0; i < lines.Count; i++) { var line = lines [i].TrimStart (); if (line.Length == 0 || line [0] == '#') continue; // Allow comments var eq = line.IndexOf ('='); if (eq == -1) - throw new InvalidOperationException ($"Invalid syntax for line {i + 1} in {linker_file}: No equals sign."); + throw new InvalidOperationException ($"Invalid syntax for line {i + 1} ('{line}') in {linker_file}:{i + 1} : No equals sign."); significantLines.Add (line); @@ -613,6 +675,8 @@ Configurator GetConfigurator (string linker_file) if (configurator.TryGetValue (key, out var actions)) { actions.Load (key, value); + } else if (customConfigurator?.TryGetValue (key, out var customActions) == true) { + customActions.Load (key, value); } else { throw new InvalidOperationException ($"Unknown configuration key '{key}' in {linker_file} at line {i + 1}."); } @@ -626,7 +690,7 @@ Configurator GetConfigurator (string linker_file) } Application.CreateCache (significantLines.ToArray ()); - if (Application.Cache is not null) + if (Application.Cache is not null && !string.IsNullOrEmpty (CacheDirectory)) Application.Cache.SetLocation (Application, CacheDirectory); if (DeploymentTarget is not null) Application.DeploymentTarget = DeploymentTarget; @@ -665,12 +729,18 @@ Configurator GetConfigurator (string linker_file) Application.Initialize (); } - public void Save (List storage) + public void Save (List storage, Configurator? customConfigurator = null) { var configurator = GetConfigurator (LinkerFile); foreach (var kvp in configurator.OrderBy (v => v.Key)) { kvp.Value.Save (kvp.Key, storage); } + + if (customConfigurator is not null) { + foreach (var kvp in customConfigurator.OrderBy (v => v.Key)) { + kvp.Value.Save (kvp.Key, storage); + } + } } // Splits a string in three based on the split character. @@ -771,6 +841,7 @@ public void Write () Application.Log ($" TypeMapAssemblyName: {Application.TypeMapAssemblyName}"); Application.Log ($" TypeMapFilePath: {TypeMapFilePath}"); Application.Log ($" TypeMapOutputDirectory: {Application.TypeMapOutputDirectory}"); + Application.Log ($" UnmanagedCallersOnlyMapPath: {UnmanagedCallersOnlyMapPath}"); Application.Log ($" UseInterpreter: {Application.UseInterpreter}"); Application.Log ($" UseLlvm: {Application.IsLLVM}"); Application.Log ($" Verbosity: {Verbosity}"); @@ -779,10 +850,12 @@ public void Write () } } +#if !ASSEMBLY_PREPARER public string GetAssemblyFileName (AssemblyDefinition assembly) { return Context.GetAssemblyLocation (assembly); } +#endif public void WriteOutputForMSBuild (string itemName, List items) { @@ -821,12 +894,27 @@ public static void Report (LinkContext Context, params Exception [] exceptions) public static void Report (LinkContext context, IList exceptions) { + // Unwrap aggregate exceptions, and collect all exceptions into a single list. + var list = ErrorHelper.CollectExceptions (exceptions); +#if ASSEMBLY_PREPARER + var log = context.Configuration.Logger; + foreach (var ex in list) { + if (ex is ProductException pe) { + if (pe.IsError (context.Configuration.Application)) { + log.LogError (pe); + } else { + log.LogWarning (pe); + } + } else { + log.LogException (ex); + } + } +#else // We can't really use the linker's reporting facilities and keep our own error codes, because we'll // end up re-using the same error codes the linker already uses for its own purposes. So instead show // a generic error using the linker's Context.LogMessage API, and then print our own errors to stderr. // Since we print using a standard message format, msbuild will parse those error messages and show // them as msbuild errors. - var list = ErrorHelper.CollectExceptions (exceptions); if (!TryGetInstance (context, out var instance)) { // Something went very wrong. Just dump out everything. context.LogMessage (MessageContainer.CreateCustomErrorMessage ("No linker configuration available.", 7000)); @@ -844,6 +932,7 @@ public static void Report (LinkContext context, IList exceptions) } // ErrorHelper.Show will print our errors and warnings to stderr. ErrorHelper.Show (instance.Application, list); +#endif } public IEnumerable GetNonDeletedAssemblies (BaseStep step) @@ -854,6 +943,38 @@ public IEnumerable GetNonDeletedAssemblies (BaseStep step) yield return assembly; } } + + public void Log (string value) + { + Log (0, value); + } + + public void Log (string format, params object? [] args) + { + Log (0, format, args); + } + + public void Log (int min_verbosity, string value) + { + if (min_verbosity > Verbosity) + return; + + if (Logger is not null) { + Logger.Log (value); + return; + } + + Console.WriteLine (value); + } + + public void Log (int min_verbosity, string format, params object? [] args) + { + if (min_verbosity > Verbosity) + return; + + var value = string.Format (format, args); + Log (min_verbosity, value); + } } } diff --git a/tools/dotnet-linker/MarkIProtocolHandler.cs b/tools/dotnet-linker/MarkIProtocolHandler.cs index b295ae6d385c..772bca9a3086 100644 --- a/tools/dotnet-linker/MarkIProtocolHandler.cs +++ b/tools/dotnet-linker/MarkIProtocolHandler.cs @@ -16,7 +16,7 @@ public override void Initialize (LinkContext context, MarkContext markContext) { base.Initialize (context); - if (LinkContext.App.Registrar == Bundler.RegistrarMode.Dynamic) { + if (LinkContext.Registrar == Bundler.RegistrarMode.Dynamic) { markContext.RegisterMarkTypeAction (ProcessType); } } @@ -26,19 +26,27 @@ protected override void Process (TypeDefinition type) if (!type.HasInterfaces) return; + var anyAdded = false; foreach (var iface in type.Interfaces) { var resolvedInterfaceType = iface.InterfaceType.Resolve (); // If we're using the dynamic registrar, we need to mark interfaces that represent protocols // even if it doesn't look like the interfaces are used, since we need them at runtime. var isProtocol = type.IsNSObject (LinkContext) && resolvedInterfaceType.HasCustomAttribute (LinkContext, Namespaces.Foundation, "ProtocolAttribute"); if (isProtocol) { - // Mark only if not already marked. - // otherwise we might enqueue something everytime and never get an empty queue - if (!LinkContext.Annotations.IsMarked (resolvedInterfaceType)) { - LinkContext.Annotations.Mark (resolvedInterfaceType); - } + // Preserve the method and field on the static constructor of the type. + abr.AddDynamicDependencyAttributeToStaticConstructor (type, resolvedInterfaceType); + anyAdded = true; } } + +#if ASSEMBLY_PREPARER + if (anyAdded) { + abr.SetCurrentAssembly (type.Module.Assembly); + abr.SaveCurrentAssembly (); + } +#else + _ = anyAdded; +#endif } } } diff --git a/tools/dotnet-linker/Steps/ExceptionalMarkHandler.cs b/tools/dotnet-linker/Steps/ExceptionalMarkHandler.cs index 25153aca76fa..298a3d74a896 100644 --- a/tools/dotnet-linker/Steps/ExceptionalMarkHandler.cs +++ b/tools/dotnet-linker/Steps/ExceptionalMarkHandler.cs @@ -32,7 +32,11 @@ public virtual void Initialize (LinkContext context) protected AnnotationStore Annotations => Context.Annotations; protected LinkerConfiguration Configuration => LinkerConfiguration.GetInstance (Context); + private protected AppBundleRewriter abr { get { return Configuration.AppBundleRewriter; } } + +#if !ASSEMBLY_PREPARER protected Profile Profile => Configuration.Profile; +#endif protected Application App => Configuration.Application; diff --git a/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs b/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs index 95913f88ead9..5393008d2c41 100644 --- a/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs +++ b/tools/dotnet-linker/Steps/InlineDlfcnMethodsStep.cs @@ -101,7 +101,7 @@ attrib.ConstructorArguments [1].Value is string libraryName && TypeDefinition GetDlfcnType (ModuleDefinition module, string @namespace, string? fieldLibraryName = null) { var frameworkOverride = !string.IsNullOrEmpty (fieldLibraryName) ? fieldLibraryName : current_framework; - var ns = string.IsNullOrEmpty (frameworkOverride) ? @namespace : frameworkOverride; + var ns = frameworkOverride ?? @namespace; var rv = abr.GetOrCreateType (module, ns, "Dlfcn", out var created); if (created) { if (!string.IsNullOrEmpty (frameworkOverride)) { diff --git a/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs b/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs index 4396c7319478..499ad0a43357 100644 --- a/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs +++ b/tools/dotnet-linker/Steps/ManagedRegistrarLookupTablesStep.cs @@ -57,11 +57,28 @@ protected override void TryProcessAssembly (AssemblyDefinition assembly) return; abr.SetCurrentAssembly (assembly); + if (App.IsPostProcessingAssemblies) { + // We need to load what the PrepareAssemblies task did/produced + CollectRegistrarType (info, assembly); + } else { + CreateRegistrarType (info); + abr.SaveCurrentAssembly (); + } + abr.ClearCurrentAssembly (); + } - CreateRegistrarType (info); + void CollectRegistrarType (AssemblyTrampolineInfo info, AssemblyDefinition currentAssembly) + { + var registrarType = currentAssembly.MainModule.Types.SingleOrDefault (v => v.Is ("ObjCRuntime", "__Registrar__")); + if (registrarType is null) + throw ErrorHelper.CreateError (99, $"No __Registrar__ was found in the assembly {currentAssembly.Name.Name} after the PrepareAssemblies step, but none was found. This might be a sign that the PrepareAssemblies step didn't run, or didn't run correctly."); - abr.SaveCurrentAssembly (); - abr.ClearCurrentAssembly (); + info.RegistrarType = registrarType; + + // We don't care about getting the types, but we need the mapping to happen. + // None of the types in the generated table should be trimmed away by the trimmer, so sorting + // them when the table is generated, and then again after trimming (aka here), should result in the same order and thus the same mapping. + GetAndMapTypesToRegister (registrarType, info); } void CreateRegistrarType (AssemblyTrampolineInfo info) @@ -119,7 +136,7 @@ void CreateRegistrarType (AssemblyTrampolineInfo info) AddLoadTypeToModuleConstructor (registrarType); // Compute the list of types that we need to register - var types = GetTypesToRegister (registrarType, info); + var types = GetAndMapTypesToRegister (registrarType, info); GenerateLookupUnmanagedFunction (registrarType, sorted); GenerateLookupType (info, registrarType, types); @@ -164,7 +181,7 @@ void AddLoadTypeToModuleConstructor (TypeDefinition registrarType) Annotations.Mark (moduleConstructor); } - List GetTypesToRegister (TypeDefinition registrarType, AssemblyTrampolineInfo info) + List GetAndMapTypesToRegister (TypeDefinition registrarType, AssemblyTrampolineInfo info) { // Compute the list of types that we need to register var types = new List (); @@ -201,6 +218,9 @@ List GetTypesToRegister (TypeDefinition registrarType, AssemblyTrampol types.Add (new (wrapperType, wrapperType.Resolve ())); } + // Sort the types by their full name to make sure the generated code is deterministic. + types.Sort ((x, y) => string.Compare (x.Definition.FullName, y.Definition.FullName, StringComparison.Ordinal)); + // Now create a mapping from type to index for (var i = 0; i < types.Count; i++) info.RegisterType (types [i].Definition, (uint) i); @@ -228,7 +248,11 @@ IEnumerable GetRelevantTypes (Func isRelev bool IsTrimmed (MemberReference type) { +#if ASSEMBLY_PREPARER + return false; +#else return StaticRegistrar.IsTrimmed (type, Annotations); +#endif } void GenerateLookupTypeId (AssemblyTrampolineInfo infos, TypeDefinition registrarType, List types) diff --git a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs index 28d906d981db..9db0c5a5fb7a 100644 --- a/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs +++ b/tools/dotnet-linker/Steps/ManagedRegistrarStep.cs @@ -84,6 +84,8 @@ public class ManagedRegistrarStep : ConfigurationAwareStep { AppBundleRewriter abr { get { return Configuration.AppBundleRewriter; } } List exceptions = new List (); + Dictionary unmanagedCallersOnlyMap = new (); + void AddException (Exception exception) { if (exceptions is null) @@ -98,6 +100,23 @@ protected override void TryProcess () if (App.Registrar != RegistrarMode.ManagedStatic && App.Registrar != RegistrarMode.TrimmableStatic) return; + if (App.IsPostProcessingAssemblies) { + var ucoMapPath = Configuration.UnmanagedCallersOnlyMapPath; + if (File.Exists (ucoMapPath)) { + foreach (var line in File.ReadAllLines (ucoMapPath)) { + var parts = line.Split ('|'); + if (parts.Length != 2) { + Console.WriteLine ($"Warning: Invalid line in unmanaged_callers_only_map.txt: {line}"); + continue; + } + var methodFullName = parts [0]; + var ucoEntryPoint = parts [1]; + unmanagedCallersOnlyMap.Add (methodFullName, ucoEntryPoint); + } + File.Delete (ucoMapPath); + } + } + Configuration.Application.StaticRegistrar.Register (Configuration.GetNonDeletedAssemblies (this)); } @@ -113,10 +132,23 @@ protected override void TryEndProcess (out List? exceptions) // Report back any exceptions that occurred during the processing. exceptions = this.exceptions; + if (App.PrepareAssemblies && !App.InCustomTrimmerStep) { + var ucoMapPath = Configuration.UnmanagedCallersOnlyMapPath; + using (var writer = new StreamWriter (ucoMapPath, false)) { + foreach (var entry in unmanagedCallersOnlyMap.Select (kvp => $"{kvp.Key}|{kvp.Value}").OrderBy (v => v)) { + writer.WriteLine (entry); + } + } + } + +#if !ASSEMBLY_PREPARER // Mark some stuff we use later on. - abr.SetCurrentAssembly (abr.PlatformAssembly); - Annotations.Mark (abr.RegistrarHelper_Register.Resolve ()); - abr.ClearCurrentAssembly (); + if (App.InCustomTrimmerStep && App.PrepareAssemblies == false) { + abr.SetCurrentAssembly (abr.PlatformAssembly); + Annotations.Mark (abr.RegistrarHelper_Register.Resolve ()); + abr.ClearCurrentAssembly (); + } +#endif } protected override void TryProcessAssembly (AssemblyDefinition assembly) @@ -181,7 +213,7 @@ bool ProcessType (TypeDefinition type, AssemblyTrampolineInfo infos, List proxyInterfaces) + { + if (!unmanagedCallersOnlyMap.TryGetValue (method.FullName, out var ucoName)) { + AddException (ErrorHelper.CreateWarning (App, 99, method, $"Couldn't find an entry in the unmanaged_callers_only_map for method {method.FullName}.")); + return; + } + + var callbackType = method.DeclaringType.NestedTypes.SingleOrDefault (v => v.Name == "__Registrar_Callbacks__"); + if (callbackType is null) { + AddException (ErrorHelper.CreateWarning (App, 99, method, $"Couldn't find the __Registrar_Callbacks__ nested type for method {method.FullName}.")); + return; + } + + var candidates = callbackType.Methods.Where (v => v.Name == ucoName).ToArray (); + if (candidates.Length != 1) { + AddException (ErrorHelper.CreateWarning (App, 99, method, $"Didn't find exactly one matching callback method in __Registrar_Callbacks__ for method {method.FullName}, found {candidates.Length}")); + return; + } + var callback = candidates [0]; + + var info = new TrampolineInfo (callback, method, ucoName); + if (this.App.Registrar == RegistrarMode.TrimmableStatic) { + // Don't set Id here, it's not used. + } else if (int.TryParse (ucoName.Split ('_') [1], NumberStyles.None, CultureInfo.InvariantCulture, out var id)) { + info.Id = id; + } else { + AddException (ErrorHelper.CreateError (App, 99, method, $"Failed to parse the ID from the DynamicDependencyAttribute for method {method.FullName}, the trampoline won't be registered correctly. The member signature was: {ucoName}")); + } + infos.Add (info); + } + int counter; void CreateUnmanagedCallersMethod (MethodDefinition method, AssemblyTrampolineInfo infos, List proxyInterfaces) { @@ -306,6 +374,8 @@ void CreateUnmanagedCallersMethod (MethodDefinition method, AssemblyTrampolineIn var placeholderType = abr.System_IntPtr; var name = $"callback_{counter++}_{Sanitize (method.DeclaringType.FullName)}_{Sanitize (method.Name)}"; + unmanagedCallersOnlyMap.Add (method.FullName, name); + var callbackType = method.DeclaringType.NestedTypes.SingleOrDefault (v => v.Name == "__Registrar_Callbacks__"); if (callbackType is null) { callbackType = new TypeDefinition (string.Empty, "__Registrar_Callbacks__", TypeAttributes.NestedPrivate | TypeAttributes.Sealed | TypeAttributes.Class); diff --git a/tools/dotnet-linker/Steps/PreserveBlockCodeHandler.cs b/tools/dotnet-linker/Steps/PreserveBlockCodeHandler.cs index 98194cad5928..b406122d0f26 100644 --- a/tools/dotnet-linker/Steps/PreserveBlockCodeHandler.cs +++ b/tools/dotnet-linker/Steps/PreserveBlockCodeHandler.cs @@ -2,7 +2,8 @@ using System.Linq; using Mono.Cecil; - +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; using Mono.Linker; using Mono.Linker.Steps; using Mono.Tuner; diff --git a/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs b/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs index 22f70f051ef0..e431f8e009bc 100644 --- a/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs +++ b/tools/dotnet-linker/Steps/SetBeforeFieldInitStep.cs @@ -1,5 +1,6 @@ using Mono.Linker.Steps; using Xamarin.Linker; +using Xamarin.Linker.Steps; using Mono.Cecil; using Mono.Tuner; @@ -7,10 +8,27 @@ #nullable enable namespace Xamarin.Linker.Steps { +#if ASSEMBLY_PREPARER + public class SetBeforeFieldInitStep : AssemblyModifierStep { +#else public class SetBeforeFieldInitStep : ConfigurationAwareSubStep { +#endif protected override string Name { get; } = "Set BeforeFieldInit"; protected override int ErrorCode { get; } = 2380; +#if ASSEMBLY_PREPARER + protected override bool ModifyAssembly (AssemblyDefinition assembly) + { + if (Configuration.DerivedLinkContext.App.Optimizations.RegisterProtocols != true) + return false; + return base.ModifyAssembly (assembly); + } + + protected override bool ProcessType (TypeDefinition type) + { + return ProcessTypeImpl (type); + } +#else public override SubStepTargets Targets { get { return SubStepTargets.Type; @@ -19,6 +37,13 @@ public override SubStepTargets Targets { protected override void Process (TypeDefinition type) { + ProcessTypeImpl (type); + } +#endif + + bool ProcessTypeImpl (TypeDefinition type) + { + var modified = false; // If we're registering protocols, we want to remove the static // constructor on the protocol interface, because it's not needed // (because we've removing all the DynamicDependency attributes @@ -43,13 +68,17 @@ protected override void Process (TypeDefinition type) // the linker. if (Configuration.DerivedLinkContext.App.Optimizations.RegisterProtocols != true) - return; + return modified; if (!type.IsBeforeFieldInit && type.IsInterface && type.HasMethods) { var cctor = type.GetTypeConstructor (); - if (cctor is not null && cctor.IsBindingImplOptimizableCode (LinkContext)) + if (cctor is not null && cctor.IsBindingImplOptimizableCode (Configuration.DerivedLinkContext) && type.IsBeforeFieldInit == false) { type.IsBeforeFieldInit = true; + modified = true; + } } + + return modified; } } } diff --git a/tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs b/tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs index 3d0f5feb9281..fafe7d80ae18 100644 --- a/tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs +++ b/tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs @@ -22,7 +22,7 @@ public class TrimmableRegistrarStep : ConfigurationAwareStep { protected override int ErrorCode { get; } = 2470; AppBundleRewriter abr { get { return Configuration.AppBundleRewriter; } } - List addedAssemblies = new List (); + List<(string Path, AssemblyDefinition Assembly, string? OriginatingAssembly)> addedAssemblies = new (); List exceptions = new List (); void AddException (Exception exception) @@ -48,6 +48,7 @@ AssemblyDefinition CreateTypeMapRootAssembly (ModuleParameters moduleParameters, // .NET 10 doesn't support a separate root type map assembly, so we have to add these attributes to the entry assembly instead. var useEntryAssemblyAsRootTypeMapAssembly = App.TargetFramework.Version.Major <= 10; + var createdRootTypeMapAssemblyPath = Path.Combine (App.TypeMapOutputDirectory, App.TypeMapAssemblyName + ".dll"); if (useEntryAssemblyAsRootTypeMapAssembly) { rootTypeMapAssembly = Configuration.EntryAssembly; @@ -55,7 +56,7 @@ AssemblyDefinition CreateTypeMapRootAssembly (ModuleParameters moduleParameters, var rootTypeMapAssemblyName = new AssemblyNameDefinition (App.TypeMapAssemblyName, new Version (1, 0, 0, 0)); rootTypeMapAssembly = AssemblyDefinition.CreateAssembly (rootTypeMapAssemblyName, rootTypeMapAssemblyName.Name, moduleParameters); Annotations.SetAction (rootTypeMapAssembly, AssemblyAction.Link); - addedAssemblies.Add (rootTypeMapAssembly); + addedAssemblies.Add ((createdRootTypeMapAssemblyPath, rootTypeMapAssembly, Configuration.PlatformAssembly)); // We're running from inside the linker, but the TypeMapEntryAssembly property can only be set using a command-line // argument, so we need to cheat a bit here and use reflection to set it. This will go away once we're not running @@ -110,7 +111,7 @@ AssemblyDefinition CreateTypeMapRootAssembly (ModuleParameters moduleParameters, // We write the assembly here even if it hasn't changed, because otherwise we'll just end up re-creating // it again during the next incremental build. if (!useEntryAssemblyAsRootTypeMapAssembly) { - rootTypeMapAssembly.Write (Path.Combine (App.TypeMapOutputDirectory, rootTypeMapAssembly.Name.Name + ".dll")); + rootTypeMapAssembly.Write (createdRootTypeMapAssemblyPath); } return rootTypeMapAssembly; } @@ -239,9 +240,10 @@ void addPostAction (AssemblyDefinition assembly, Action acti var typeMapAssemblyName = new AssemblyNameDefinition ("_" + assembly.Name.Name + ".TypeMap", new Version (1, 0, 0, 0)); var typeMapAssembly = AssemblyDefinition.CreateAssembly (typeMapAssemblyName, typeMapAssemblyName.Name, assemblyParameters); + var typeMapAssemblyPath = Path.Combine (App.TypeMapOutputDirectory, typeMapAssembly.Name.Name + ".dll"); var existingAction = Annotations.GetAction (assembly); Annotations.SetAction (typeMapAssembly, existingAction); - addedAssemblies.Add (typeMapAssembly); + addedAssemblies.Add ((typeMapAssemblyPath, typeMapAssembly, assembly.MainModule.FileName)); var accessesAssemblies = new HashSet (); accessesAssemblies.Add (assembly); @@ -591,7 +593,7 @@ void addPostAction (AssemblyDefinition assembly, Action acti // We write the assembly here even if it hasn't changed, because otherwise we'll just end up re-creating // it again during the next incremental build. - typeMapAssembly.Write (Path.Combine (App.TypeMapOutputDirectory, typeMapAssembly.Name.Name + ".dll")); + typeMapAssembly.Write (typeMapAssemblyPath); } foreach (var kvp in postActionsByAssembly) { @@ -604,13 +606,17 @@ void addPostAction (AssemblyDefinition assembly, Action acti abr.ClearCurrentAssembly (); } +#if ASSEMBLY_PREPARER + Configuration.AddedAssemblies.AddRange (addedAssemblies); +#else // Since we're running inside the trimmer, we need to make sure the trimmer knows about the assemblies we've created. // This will go away once we're running outside of the trimmer. var managedAssemblyToLinkItems = new List (); var resolver = abr.PlatformAssembly.MainModule.AssemblyResolver; var getAssembly = resolver.GetType ().GetMethod ("GetAssembly", new Type [] { typeof (string) })!; var cacheAssembly = resolver.GetType ().GetMethod ("CacheAssembly", new Type [] { typeof (AssemblyDefinition) })!; - foreach (var asm in addedAssemblies) { + foreach (var aa in addedAssemblies) { + var asm = aa.Assembly; var fn = Path.Combine (App.TypeMapOutputDirectory, asm.Name.Name + ".dll"); var asmDef = (AssemblyDefinition) getAssembly.Invoke (resolver, [fn])!; cacheAssembly.Invoke (resolver, [asmDef]); @@ -624,6 +630,7 @@ void addPostAction (AssemblyDefinition assembly, Action acti } Configuration.WriteOutputForMSBuild ("ManagedAssemblyToLink", managedAssemblyToLinkItems); +#endif // Report back any exceptions that occurred during the processing. exceptions = this.exceptions; diff --git a/tools/linker/CoreTypeMapStep.cs b/tools/linker/CoreTypeMapStep.cs index c180cf3b43fc..e73b6b9b56b9 100644 --- a/tools/linker/CoreTypeMapStep.cs +++ b/tools/linker/CoreTypeMapStep.cs @@ -168,14 +168,14 @@ bool IsWrapperType (TypeDefinition type) // Cache the results of the IsCIFilter check in a dictionary. It makes this method slightly faster // (total time spent in IsCIFilter when linking monotouch-test went from 11 ms to 3ms). Dictionary ci_filter_types = new Dictionary (); - bool IsCIFilter (TypeReference type) + bool IsCIFilter (TypeReference? type) { if (type is null) return false; bool rv; if (!ci_filter_types.TryGetValue (type, out rv)) { - rv = type.Is (Namespaces.CoreImage, "CIFilter") || IsCIFilter (Context.Resolve (type).BaseType); + rv = type.Is (Namespaces.CoreImage, "CIFilter") || IsCIFilter (Context.Resolve (type)?.BaseType); ci_filter_types [type] = rv; } return rv; @@ -198,7 +198,7 @@ void SetIsDirectBindingValue (TypeDefinition type) var base_type = Context.Resolve (type.BaseType); while (base_type is not null && IsNSObject (base_type)) { isdirectbinding_value [base_type] = null; - base_type = Context.Resolve (base_type.BaseType); + base_type = LinkContext.Resolve (base_type.BaseType); } return; } diff --git a/tools/linker/MarkNSObjects.cs b/tools/linker/MarkNSObjects.cs index 02502154fd10..2dc41e1e7539 100644 --- a/tools/linker/MarkNSObjects.cs +++ b/tools/linker/MarkNSObjects.cs @@ -41,7 +41,7 @@ #nullable enable namespace Xamarin.Linker.Steps { - +#if !ASSEMBLY_PREPARER public class MarkNSObjects : ExceptionalSubStep, IMarkNSObjects { protected override string Name { get; } = "MarkNSObjects"; protected override int ErrorCode { get; } = 2080; @@ -86,6 +86,7 @@ public bool PreserveMethod (TypeDefinition onType, MethodDefinition method) return true; } } +#endif public interface IMarkNSObjects { bool PreserveType (TypeDefinition type, bool allMembers); diff --git a/tools/linker/MobileExtensions.cs b/tools/linker/MobileExtensions.cs index cba0f7f9997e..d1458fdbf60c 100644 --- a/tools/linker/MobileExtensions.cs +++ b/tools/linker/MobileExtensions.cs @@ -44,7 +44,7 @@ public static bool HasCustomAttribute (this ICustomAttributeProvider? provider, if (provider?.HasCustomAttribute (@namespace, name) == true) return true; - return context?.GetCustomAttributes (provider, @namespace, name)?.Count > 0; + return context?.GetCustomAttributes (provider, @namespace, name)?.Any () == true; } public static bool HasCustomAttribute (this ICustomAttributeProvider? provider, string @namespace, string name) diff --git a/tools/linker/MonoTouch.Tuner/Extensions.cs b/tools/linker/MonoTouch.Tuner/Extensions.cs index e1eac2745c0a..d0764f92518d 100644 --- a/tools/linker/MonoTouch.Tuner/Extensions.cs +++ b/tools/linker/MonoTouch.Tuner/Extensions.cs @@ -7,6 +7,10 @@ using Xamarin.Tuner; +#if !LEGACY_TOOLS && !ASSEMBLY_PREPARER +using LinkContext = Xamarin.Bundler.DotNetLinkContext; +#endif + namespace MonoTouch.Tuner { public static class Extensions { diff --git a/tools/linker/ObjCExtensions.cs b/tools/linker/ObjCExtensions.cs index 5482602a5bdf..3d8ab04c91c0 100644 --- a/tools/linker/ObjCExtensions.cs +++ b/tools/linker/ObjCExtensions.cs @@ -117,8 +117,10 @@ public static bool IsNSObject (this TypeDefinition? type, DerivedLinkContext? li return link_context.CachedIsNSObject.Contains (type); return type.Inherits (Namespaces.Foundation, "NSObject" -#if !LEGACY_TOOLS - , link_context.LinkerConfiguration.Context +#if ASSEMBLY_PREPARER + , link_context!.Configuration.MetadataResolver +#elif !LEGACY_TOOLS + , link_context!.LinkerConfiguration.Context #endif ); } diff --git a/tools/linker/OptimizeGeneratedCode.cs b/tools/linker/OptimizeGeneratedCode.cs index 46af059779e8..43bd328b189d 100644 --- a/tools/linker/OptimizeGeneratedCode.cs +++ b/tools/linker/OptimizeGeneratedCode.cs @@ -140,10 +140,13 @@ internal static bool ValidateInstruction (IToolLog log, MethodDefinition caller, case Code.Conv_I1: case Code.Conv_I2: case Code.Conv_I4: + case Code.Conv_I8: case Code.Conv_U: case Code.Sizeof: case Code.Ldfld: case Code.Ldflda: + case Code.Mul: + case Code.And: return null; // just to not hit the CWL below #endif default: @@ -1076,7 +1079,7 @@ static bool ProcessRuntimeArch (OptimizeGeneratedCodeData data, MethodDefinition static MethodReference GetBlockSetupImpl (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) { if (data.SetupBlockImplDefinition is null) { - var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); + var type = data.LinkContext.GetProductAssembly ().MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); foreach (var method in type.Methods) { if (method.Name != "SetupBlockImpl") continue; @@ -1093,7 +1096,7 @@ static MethodReference GetBlockSetupImpl (OptimizeGeneratedCodeData data, Method static MethodReference GetBlockLiteralConstructor (OptimizeGeneratedCodeData data, MethodDefinition caller, Instruction ins) { if (data.BlockCtorDefinition is null) { - var type = data.LinkContext.GetAssembly (Driver.GetProductAssembly (data.LinkContext.App)).MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); + var type = data.LinkContext.GetProductAssembly ().MainModule.GetType (Namespaces.ObjCRuntime, "BlockLiteral"); foreach (var method in type.Methods) { if (!method.IsConstructor) continue; diff --git a/tools/mtouch/Errors.designer.cs b/tools/mtouch/Errors.designer.cs index 7c3f1861b4ab..750512fbca59 100644 --- a/tools/mtouch/Errors.designer.cs +++ b/tools/mtouch/Errors.designer.cs @@ -3353,6 +3353,15 @@ public static string MX1503 { } } + /// + /// Looks up a localized string similar to Can not find the product assembly '{0}' in the list of loaded assemblies.. + /// + public static string MX1504 { + get { + return ResourceManager.GetString("MX1504", resourceCulture); + } + } + /// /// Looks up a localized string similar to Not a Mach-O dynamic library (unknown header '0x{0}'): {1}.. /// diff --git a/tools/mtouch/Errors.resx b/tools/mtouch/Errors.resx index cc89125d1ff1..99beb38ac960 100644 --- a/tools/mtouch/Errors.resx +++ b/tools/mtouch/Errors.resx @@ -851,6 +851,10 @@ One or more reference(s) to type '{0}' still exists inside '{1}' after linking + + Can not find the product assembly '{0}' in the list of loaded assemblies. + + Not a Mach-O dynamic library (unknown header '0x{0}'): {1}. diff --git a/tools/mtouch/mtouch.cs b/tools/mtouch/mtouch.cs index 42ae6901f1a0..2e61df769f2f 100644 --- a/tools/mtouch/mtouch.cs +++ b/tools/mtouch/mtouch.cs @@ -11,6 +11,7 @@ using Mono.Options; using Xamarin.Linker; +using Xamarin.Utils; namespace Xamarin.Bundler { public partial class Driver { @@ -18,7 +19,7 @@ public partial class Driver { static int Main2 (string [] args) { - var app = new Application (new LinkerConfiguration ()); + var app = new Application (); var os = new OptionSet (); ParseOptions (app, os, args); diff --git a/tools/tools.slnx b/tools/tools.slnx index 140d0af4708c..d4fd9bbf97a3 100644 --- a/tools/tools.slnx +++ b/tools/tools.slnx @@ -1,4 +1,5 @@ +