// Copyright 2011-2013 Xamarin Inc., All rights reserved. // adapted from MonoTouchMarkStep.cs, itself // adapted from xtouch/tools/mtouch/Touch.Tuner/ManualMarkStep.cs using System; using System.Collections.Generic; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Linker; using Mono.Tuner; using Registrar; namespace Xamarin.Linker.Steps { // Generated backend fields inside .dll are also removed if only used (i.e. set to null) // inside the Dispose method public abstract class CoreMarkStep : MobileMarkStep { readonly List dispose_methods = new List (); protected string ProductAssembly { get; private set; } bool RegisterProtocols { get { return LinkContext.App.Optimizations.RegisterProtocols == true; } } public override void Process (LinkContext context) { ProductAssembly = (Profile.Current as BaseProfile).ProductAssembly; base.Process (context); // adjust IL inside generated Dispose methods (overriding NSObject) // to remove references to fields that were not marked TypeDefinition nsobject = GetType (ProductAssembly, Namespaces.Foundation + ".NSObject"); if (nsobject != null) { foreach (MethodDefinition d in nsobject.Methods) { if (d.Name == "Dispose" && d.HasParameters) { ProcessDispose (d, d); break; } } } dispose_methods.Clear (); } void ProcessDispose (MethodDefinition bd, MethodDefinition cd) { bool skip = false; // did we detect some fields that could be removed ? if (dispose_methods.Contains (cd)) { // removed unmarked fields skip = FilterDispose (cd); // return value tells us if the Dispose method is now "empty" and could be skipped to the next // (non-empty) base.Dispose if (skip) { // if it does nothing then it should not be part of the final binary //cd.DeclaringType.Methods.Remove (cd); } } var overrides = Annotations.GetOverrides (cd); if (overrides == null) return; // every subclass-Dispose should be calling base-Dispose foreach (var overrideInfo in overrides) { var od = overrideInfo.Override; // we do not need to process unmarked code (it won't be part of the final binary) if (!Annotations.IsMarked (od)) continue; // we do NOT process non-generated code - we could break user code if (!od.IsOptimizableCode (LinkContext)) continue; ProcessDispose (skip ? bd : cd, od); // check if we need to replace the base.Dipose call if (bd == cd) continue; // replace "base.Dispose". In C# this always looks fine - but in IL it's the base type that's // used (and needs to change to remove the "empty" Dispose methods) foreach (Instruction ins in od.Body.Instructions) { if (ins.OpCode.Code != Code.Call) continue; // we can cross the assembly borders and the Dispose method might not be // part of the existing member references so we import it in such cases if (od.Module != bd.Module) ins.Operand = od.Module.ImportReference (bd); else ins.Operand = bd; break; } } } bool FilterDispose (MethodDefinition m) { #if DEBUG var sp = m.DebugInformation.GetSequencePoint (m.Body.Instructions [0]); if (sp != null) { string source = sp.Document.Url; if (!source.EndsWith (".g.cs", StringComparison.Ordinal)) throw new InvalidProgramException (String.Format ("Attempt at modifying non-generated code for {0} : {1}", m, source)); } #endif bool remove_all_fields = true; var il = m.Body.Instructions; for (int i = il.Count - 1; i >= 0; i--) { Instruction ins = il [i]; if (ins.OpCode.Code != Code.Stfld) continue; // if the field is not marked elsewhere (since we skipped Dispose) if (!Annotations.IsMarked (ins.Operand as FieldReference)) { // remove stfld, the previous ldnull and ldarg.0 instructions ins.OpCode = OpCodes.Nop; ins.Operand = null; ins = ins.Previous; #if DEBUG if (ins.OpCode.Code != Code.Ldnull) throw new InvalidProgramException (String.Format ("Attempt at modifying wrong code pattern for {0}", m)); #endif ins.OpCode = OpCodes.Nop; ins.Operand = null; ins = ins.Previous; #if DEBUG if (ins.OpCode.Code != Code.Ldarg_0) throw new InvalidProgramException (String.Format ("Attempt at modifying wrong code pattern for {0}", m)); #endif ins.OpCode = OpCodes.Nop; ins.Operand = null; i -= 2; } else remove_all_fields = false; } return remove_all_fields; } bool processing_generated_dispose; int skipped_fields; // looking for 'protected override void Dispose (bool disposing)' with generated code bool IsGeneratedDispose (MethodDefinition method) { if (!method.IsFamily || !method.IsVirtual || method.IsNewSlot || !method.HasParameters || !method.HasBody) return false; return ((method.Name == "Dispose") && method.IsOptimizableCode (LinkContext)); } protected override TypeDefinition MarkType (TypeReference reference) { try { var td = base.MarkType (reference); if (td == null) return null; // We're removing the Protocol attribute, which points to its wrapper type. // But we need the wrapper type if the protocol interface is marked, so manually mark it. if (td.IsInterface) { var proto = LinkContext.StaticRegistrar.GetProtocolAttribute (td); if (proto?.WrapperType != null) MarkType (proto.WrapperType); } // older generated bindings did not preserve the `Handler` field and // newer (mono 2019-02) linker can optimize them (enabled by default) // so we make sure our old bindings remains linker-safe if (td.IsAbstract && td.IsSealed && td.IsNested && td.HasFields) { var dt = td.DeclaringType; if (dt.Is ("ObjCRuntime", "Trampolines")) { var f = td.Fields [0]; if (f.IsInitOnly && td.Fields.Count == 1 && f.Name == "Handler") MarkField (f); } } return td; } catch (Exception e) { // we need a way to know where (not just what) went wrong (e.g. debugging symbols being incorrect) e.Data ["TypeReference"] = reference.ToString (); e.Data ["AssemblyDefinition"] = reference.Module.Assembly.ToString (); throw; } } protected override void ProcessMethod (MethodDefinition method) { // check for generated Dispose methods inside monotouch.dll processing_generated_dispose = IsGeneratedDispose (method); int skip = skipped_fields; try { base.ProcessMethod (method); } catch (Exception e) { // we need a way to know where (not just what) went wrong (e.g. debugging symbols being incorrect) e.Data ["MethodDefinition"] = method.ToString (); e.Data ["AssemblyDefinition"] = method.DeclaringType.Module.Assembly.ToString (); throw; } if (processing_generated_dispose) { // if some fields were skipped (i.e. only used inside Dispose) if (skip < skipped_fields) dispose_methods.Add (method); processing_generated_dispose = false; } } bool SkipField (FieldDefinition f) { if (f == null) return false; if (f.Name.StartsWith ("__mt_", StringComparison.Ordinal)) { skipped_fields++; #if DEBUG Console.WriteLine ("SkipField {0}::{1} ({2})", f.DeclaringType.FullName, f.Name, skipped_fields); #endif return true; } return false; } // we don't want to mark fields are only used in generated Dispose protected override void MarkInstruction (Instruction instruction) { if (processing_generated_dispose) { switch (instruction.OpCode.OperandType) { case OperandType.InlineField: case OperandType.InlineTok: if (SkipField (instruction.Operand as FieldDefinition)) return; break; } } base.MarkInstruction (instruction); } protected override MethodDefinition MarkMethod (MethodReference reference) { var method = base.MarkMethod (reference); if (method == null) return null; var t = method.DeclaringType; // We have special processing that prevents protocol interfaces from being marked if they're // only used by being implemented by a class, but the linker will not mark interfaces if a method implemented by an interface // is marked: this means we need special processing to preserve a protocol interface whose methods have been implemented. if (RegisterProtocols && t.HasInterfaces && method.IsVirtual) { foreach (var r in t.Interfaces) { var i = r.InterfaceType.Resolve (); if (i == null) continue; if (Annotations.IsMarked (i)) continue; if (!LinkContext.StaticRegistrar.HasAttribute (i, Namespaces.Foundation, "ProtocolAttribute")) continue; var isProtocolImplementation = false; // Are there any explicit overrides? foreach (var @override in method.Overrides) { if (!i.Methods.Contains (@override.Resolve ())) continue; isProtocolImplementation = true; break; } if (!isProtocolImplementation) { // Are there any implicit overrides (identical name and signature)? foreach (var imethod in i.Methods) { if (!StaticRegistrar.MethodMatch (imethod, method)) continue; isProtocolImplementation = true; break; } } if (isProtocolImplementation) { MarkType (r.InterfaceType); Bundler.Driver.Log (9, "Marking {0} because the method {1} implements one of its methods.", r.InterfaceType, method.FullName); } } } // special processing to find [BlockProxy] attributes in _Extensions types // ref: https://bugzilla.xamarin.com/show_bug.cgi?id=23540 if (LinkContext.Target.StaticRegistrar.MapProtocolMember (method, out var extensionMethod)) { // one cannot simply mark the `ca.ConstructorArguments [0].Value` type, // e.g. Trampolines.NIDActionArity1V26 // as the relation to *_Extensions will be reflected at runtime MarkMethod (extensionMethod); } return method; } protected override bool ShouldMarkInterfaceImplementation (TypeDefinition type, InterfaceImplementation iface, TypeDefinition resolvedInterfaceType) { if (RegisterProtocols) { // If we're registering protocols, we can remove interfaces that represent protocols. // The linker will automatically mark interfaces a class implements, but we have to // override the linker behavior for interfaces that represent protocols for those // interfaces to be removed. var isProtocol = type.IsNSObject (LinkContext) && resolvedInterfaceType.HasCustomAttribute (LinkContext, Namespaces.Foundation, "ProtocolAttribute"); // We're not linking the current assembly, which means the interface should be marked. if (isProtocol && !IgnoreScope (type.Scope)) { LinkContext.StoreProtocolMethods (resolvedInterfaceType); } } return base.ShouldMarkInterfaceImplementation (type, iface, resolvedInterfaceType); } } }