From c33b39fa06b186277457014b65d6d8fc270a0f65 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 16 Mar 2026 11:10:08 -0700 Subject: [PATCH 1/2] [xabt] Move `AddKeepAlives` trimmer step to standalone MSBuild task Migrate AddKeepAlivesStep out of the ILLink custom step pipeline into a standalone MSBuild task that runs AfterTargets="ILLink", following the same pattern established by StripEmbeddedLibraries in #10894. Core IL-rewriting logic is extracted into AddKeepAlivesHelper, shared by both the new task (trimmed builds) and the existing pipeline step (non-trimmed builds via LinkAssembliesNoShrink). --- .../Microsoft.Android.Sdk.ILLink.csproj | 1 - .../MonoDroid.Tuner/AddKeepAlivesHelper.cs | 124 ++++++++++++++ .../MonoDroid.Tuner/AddKeepAlivesStep.cs | 158 +----------------- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 23 ++- .../Tasks/AddKeepAlives.cs | 91 ++++++++++ .../Xamarin.Android.Build.Tasks.csproj | 1 + 6 files changed, 239 insertions(+), 159 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs diff --git a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj index 3757ed34b68..92c92c0ee94 100644 --- a/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj +++ b/src/Microsoft.Android.Sdk.ILLink/Microsoft.Android.Sdk.ILLink.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs new file mode 100644 index 00000000000..5abe21e19ad --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Xamarin.Android.Tasks; + +namespace MonoDroid.Tuner +{ + static class AddKeepAlivesHelper + { + internal static bool AddKeepAlives (AssemblyDefinition assembly, IMetadataResolver resolver, Func getCorlibAssembly, Action logMessage) + { + if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object")) + return false; + + // Anything that was built against .NET for Android will have + // keep-alives already compiled in. + if (MonoAndroidHelper.IsDotNetAndroidAssembly (assembly)) + return false; + + MethodDefinition? methodKeepAlive = null; + bool changed = false; + foreach (TypeDefinition type in assembly.MainModule.Types) + changed |= ProcessType (type, resolver, ref methodKeepAlive, getCorlibAssembly, logMessage); + + return changed; + } + + static bool ProcessType (TypeDefinition type, IMetadataResolver resolver, ref MethodDefinition? methodKeepAlive, Func getCorlibAssembly, Action logMessage) + { + bool changed = false; + if (MightNeedFix (type, resolver)) + changed |= AddKeepAlives (type, ref methodKeepAlive, getCorlibAssembly, logMessage); + + if (type.HasNestedTypes) { + foreach (var t in type.NestedTypes) { + changed |= ProcessType (t, resolver, ref methodKeepAlive, getCorlibAssembly, logMessage); + } + } + + return changed; + } + + static bool MightNeedFix (TypeDefinition type, IMetadataResolver resolver) + { + return !type.IsAbstract && type.IsSubclassOf ("Java.Lang.Object", resolver); + } + + static bool AddKeepAlives (TypeDefinition type, ref MethodDefinition? methodKeepAlive, Func getCorlibAssembly, Action logMessage) + { + bool changed = false; + foreach (MethodDefinition method in type.Methods) { + if (method.Parameters.Count == 0) + continue; + + if (!method.CustomAttributes.Any (a => a.AttributeType.FullName == "Android.Runtime.RegisterAttribute")) + continue; + + var instructions = method.Body.Instructions; + + var found = false; + for (int off = Math.Max (0, instructions.Count - 6); off < instructions.Count; off++) { + var current = instructions [off]; + if (current.OpCode == OpCodes.Call && current.Operand.ToString ().Contains ("System.GC::KeepAlive")) { + found = true; + break; + } + } + + if (found) + continue; + + var processor = method.Body.GetILProcessor (); + var module = method.DeclaringType.Module; + var end = instructions.Last (); + if (end.Previous.OpCode == OpCodes.Endfinally) + end = end.Previous; + + for (int i = 0; i < method.Parameters.Count; i++) { + if (method.Parameters [i].ParameterType.IsValueType || method.Parameters [i].ParameterType.FullName == "System.String") + continue; + + if (methodKeepAlive == null) + methodKeepAlive = GetKeepAliveMethod (getCorlibAssembly, logMessage); + + if (methodKeepAlive == null) { + logMessage ("Unable to add KeepAlive call, did not find System.GC.KeepAlive method."); + break; + } + + processor.InsertBefore (end, GetLoadArgumentInstruction (method.IsStatic ? i : i + 1, method.Parameters [i])); + processor.InsertBefore (end, Instruction.Create (OpCodes.Call, module.ImportReference (methodKeepAlive))); + changed = true; + } + } + return changed; + } + + static MethodDefinition? GetKeepAliveMethod (Func getCorlibAssembly, Action logMessage) + { + var corlibAssembly = getCorlibAssembly (); + if (corlibAssembly == null) + return null; + + var gcType = Extensions.GetType (corlibAssembly, "System.GC"); + if (gcType == null) + return null; + + return Extensions.GetMethod (gcType, "KeepAlive", new string [] { "System.Object" }); + } + + // Adapted from src/Mono.Android.Export/Mono.CodeGeneration/CodeArgumentReference.cs + static Instruction GetLoadArgumentInstruction (int argNum, ParameterDefinition parameter) + { + switch (argNum) { + case 0: return Instruction.Create (OpCodes.Ldarg_0); + case 1: return Instruction.Create (OpCodes.Ldarg_1); + case 2: return Instruction.Create (OpCodes.Ldarg_2); + case 3: return Instruction.Create (OpCodes.Ldarg_3); + default: return Instruction.Create (OpCodes.Ldarg, parameter); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs index 0ca2fa7f597..32f69e5e70f 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs @@ -1,169 +1,23 @@ -using System; -using System.Linq; -using Java.Interop.Tools.Cecil; using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Linker; using Mono.Linker.Steps; using Xamarin.Android.Tasks; namespace MonoDroid.Tuner { - public class AddKeepAlivesStep : BaseStep -#if !ILLINK - , IAssemblyModifierPipelineStep -#endif // !ILLINK + public class AddKeepAlivesStep : BaseStep, IAssemblyModifierPipelineStep { - protected override void ProcessAssembly (AssemblyDefinition assembly) - { - var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip; - if (action == AssemblyAction.Delete) - return; - - if (AddKeepAlives (assembly)) { - if (action == AssemblyAction.Skip || action == AssemblyAction.Copy) - Annotations.SetAction (assembly, AssemblyAction.Save); - } - } - -#if !ILLINK public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) { // Only run this step on user Android assemblies if (!context.IsAndroidUserAssembly) return; - context.IsAssemblyModified |= AddKeepAlives (assembly); - } -#endif // !ILLINK - - internal bool AddKeepAlives (AssemblyDefinition assembly) - { - if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object")) - return false; - - // Anything that was built against .NET for Android will have - // keep-alives already compiled in. - if (MonoAndroidHelper.IsDotNetAndroidAssembly (assembly)) - return false; - - bool changed = false; - foreach (TypeDefinition type in assembly.MainModule.Types) - changed |= ProcessType (type); - - return changed; - } - - bool ProcessType (TypeDefinition type) - { - bool changed = false; - if (MightNeedFix (type)) - changed |= AddKeepAlives (type); - - if (type.HasNestedTypes) { - foreach (var t in type.NestedTypes) { - changed |= ProcessType (t); - } - } - - return changed; - } - - bool MightNeedFix (TypeDefinition type) - { - return !type.IsAbstract && type.IsSubclassOf ("Java.Lang.Object", Context); - } - - MethodDefinition? methodKeepAlive = null; - - bool AddKeepAlives (TypeDefinition type) - { - bool changed = false; - foreach (MethodDefinition method in type.Methods) { - if (method.Parameters.Count == 0) - continue; - - if (!method.CustomAttributes.Any (a => a.AttributeType.FullName == "Android.Runtime.RegisterAttribute")) - continue; - - var instructions = method.Body.Instructions; - - var found = false; - for (int off = Math.Max (0, instructions.Count - 6); off < instructions.Count; off++) { - var current = instructions [off]; - if (current.OpCode == OpCodes.Call && current.Operand.ToString ().Contains ("System.GC::KeepAlive")) { - found = true; - break; - } - } - - if (found) - continue; - - var processor = method.Body.GetILProcessor (); - var module = method.DeclaringType.Module; - var end = instructions.Last (); - if (end.Previous.OpCode == OpCodes.Endfinally) - end = end.Previous; - - for (int i = 0; i < method.Parameters.Count; i++) { - if (method.Parameters [i].ParameterType.IsValueType || method.Parameters [i].ParameterType.FullName == "System.String") - continue; - - if (methodKeepAlive == null) - methodKeepAlive = GetKeepAliveMethod (); - - if (methodKeepAlive == null) { - LogMessage ("Unable to add KeepAlive call, did not find System.GC.KeepAlive method."); - break; - } - - processor.InsertBefore (end, GetLoadArgumentInstruction (method.IsStatic ? i : i + 1, method.Parameters [i])); - processor.InsertBefore (end, Instruction.Create (OpCodes.Call, module.ImportReference (methodKeepAlive))); - changed = true; - } - } - return changed; - } - - protected virtual AssemblyDefinition GetCorlibAssembly () - { - return Context.GetAssembly ("System.Private.CoreLib"); - } - - MethodDefinition? GetKeepAliveMethod () - { - var corlibAssembly = GetCorlibAssembly (); - if (corlibAssembly == null) - return null; - - var gcType = Extensions.GetType (corlibAssembly, "System.GC"); - if (gcType == null) - return null; - - return Extensions.GetMethod (gcType, "KeepAlive", new string [] { "System.Object" }); - } - - public -#if !ILLINK - override -#endif - void LogMessage (string message) - { - Context.LogMessage (message); - } - - // Adapted from src/Mono.Android.Export/Mono.CodeGeneration/CodeArgumentReference.cs - static Instruction GetLoadArgumentInstruction (int argNum, ParameterDefinition parameter) - { - switch (argNum) { - case 0: return Instruction.Create (OpCodes.Ldarg_0); - case 1: return Instruction.Create (OpCodes.Ldarg_1); - case 2: return Instruction.Create (OpCodes.Ldarg_2); - case 3: return Instruction.Create (OpCodes.Ldarg_3); - default: return Instruction.Create (OpCodes.Ldarg, parameter); - } + context.IsAssemblyModified |= AddKeepAlivesHelper.AddKeepAlives ( + assembly, + Context, + () => Context.GetAssembly ("System.Private.CoreLib"), + (msg) => LogMessage (msg)); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index 4be55630c91..8a67744e311 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -8,6 +8,7 @@ + <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag @@ -197,12 +198,6 @@ <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveJavaInterfaces" /> <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.FixAbstractMethodsStep" /> - <_TrimmerCustomSteps - Condition=" '$(AndroidAddKeepAlives)' == 'true' " - Include="$(_AndroidLinkerCustomStepAssembly)" - AfterStep="CleanStep" - Type="MonoDroid.Tuner.AddKeepAlivesStep" - /> <_TrimmerCustomSteps Condition=" '$(AndroidLinkResources)' == 'true' " @@ -266,6 +261,22 @@ Deterministic="$(Deterministic)" /> + + + + <_AddKeepAlivesAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs new file mode 100644 index 00000000000..3674cff0892 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AddKeepAlives.cs @@ -0,0 +1,91 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Java.Interop.Tools.Cecil; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Mono.Cecil; +using MonoDroid.Tuner; + +namespace Xamarin.Android.Tasks; + +/// +/// An MSBuild task that injects GC.KeepAlive() calls into binding methods of trimmed assemblies. +/// +/// This runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation, +/// so that R2R images are generated from the already-modified assemblies. +/// +public class AddKeepAlives : AndroidTask +{ + public override string TaskPrefix => "AKA"; + + [Required] + public ITaskItem [] Assemblies { get; set; } = []; + + public bool Deterministic { get; set; } + + public override bool RunTask () + { + var resolver = new DefaultAssemblyResolver (); + var cache = new TypeDefinitionCache (); + var searchDirectories = new HashSet (StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in Assemblies) { + var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? ""); + if (searchDirectories.Add (dir)) { + resolver.AddSearchDirectory (dir); + } + } + + try { + foreach (var assembly in Assemblies) { + if (MonoAndroidHelper.IsFrameworkAssembly (assembly)) { + continue; + } + + ProcessAssembly (assembly.ItemSpec, resolver, cache); + } + } finally { + resolver.Dispose (); + } + + return !Log.HasLoggedErrors; + } + + void ProcessAssembly (string assemblyPath, IAssemblyResolver resolver, IMetadataResolver cache) + { + string pdbPath = Path.ChangeExtension (assemblyPath, ".pdb"); + bool havePdb = File.Exists (pdbPath); + + var readerParams = new ReaderParameters { + ReadSymbols = havePdb, + ReadWrite = true, + AssemblyResolver = resolver, + }; + + using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams)) { + bool modified = AddKeepAlivesHelper.AddKeepAlives ( + assembly, + cache, + () => GetCorlibAssembly (resolver), + (msg) => Log.LogDebugMessage (msg)); + + if (!modified) { + return; + } + + Log.LogDebugMessage ($" Writing modified assembly: {assemblyPath}"); + assembly.Write (new WriterParameters { + WriteSymbols = havePdb, + DeterministicMvid = Deterministic, + }); + } + } + + static AssemblyDefinition GetCorlibAssembly (IAssemblyResolver resolver) + { + return resolver.Resolve (AssemblyNameReference.Parse ("System.Private.CoreLib")); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 79f6e450ae3..e1cedf463b9 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -49,6 +49,7 @@ + From e24ddc37287895f59c8de8d2682106aed4278616 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 16 Mar 2026 13:23:24 -0700 Subject: [PATCH 2/2] Fix missing using directive in AddKeepAlivesHelper.cs --- .../Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs index 5abe21e19ad..c051fe30971 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesHelper.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Java.Interop.Tools.Cecil; using Mono.Cecil; using Mono.Cecil.Cil; using Xamarin.Android.Tasks;