From 311033964924b69961c3a9f3e21e6e3ea880910e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 00:51:56 -0400 Subject: add attribute type rewriting (#711) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 46 +++++------ .../ModLoading/Framework/BaseInstructionHandler.cs | 10 +++ .../ModLoading/Framework/BaseTypeFinder.cs | 29 +++++++ .../Framework/BaseTypeReferenceRewriter.cs | 88 +++++++++++++++++++++- .../Framework/ModLoading/IInstructionHandler.cs | 7 ++ src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 67 ++++++++++++++++ 6 files changed, 222 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b5533335..5b5a621b 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,31 +285,44 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) + foreach (TypeDefinition type in module.GetTypes()) { - // check method definition + // check type definition foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, type, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - var instructions = cil.Body.Instructions; - // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers - for (int offset = 0; offset < instructions.Count; offset++) + // check methods + foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) { + // check method definition foreach (IInstructionHandler handler in handlers) { - Instruction instruction = instructions[offset]; - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + var instructions = cil.Body.Instructions; + // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers + for (int offset = 0; offset < instructions.Count; offset++) + { + foreach (IInstructionHandler handler in handlers) + { + Instruction instruction = instructions[offset]; + InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + } } } @@ -395,18 +408,5 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; type.Scope = assemblyRef; } - - /// Get all methods in a module. - /// The module to search. - private IEnumerable GetMethods(ModuleDefinition module) - { - return ( - from type in module.GetTypes() - where type.HasMethods - from method in type.Methods - where method.HasBody - select method - ); - } } } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index 10780d07..353de464 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -16,6 +16,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Public methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index cfd87922..48165c4c 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -50,9 +50,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method definition. public bool IsMatch(MethodDefinition method) { + // return type if (this.IsMatch(method.ReturnType)) return true; + // parameters + foreach (ParameterDefinition parameter in method.Parameters) + { + if (this.IsMatch(parameter.ParameterType)) + return true; + } + + // generic parameters + foreach (GenericParameter parameter in method.GenericParameters) + { + if (this.IsMatch(parameter)) + return true; + } + + // custom attributes + foreach (CustomAttribute attribute in method.CustomAttributes) + { + if (this.IsMatch(attribute.AttributeType)) + return true; + + foreach (var arg in attribute.ConstructorArguments) + { + if (this.IsMatch(arg.Type)) + return true; + } + } + + // local variables foreach (VariableDefinition variable in method.Body.Variables) { if (this.IsMatch(variable.VariableType)) diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs index 5f38a30b..445ac2cb 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; namespace StardewModdingAPI.Framework.ModLoading.Framework { @@ -17,6 +19,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Public methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + bool rewritten = this.RewriteCustomAttributesIfNeeded(module, type.CustomAttributes); + + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; + } + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. @@ -28,9 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // return type if (this.Finder.IsMatch(method.ReturnType)) - { rewritten |= this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - } // parameters foreach (ParameterDefinition parameter in method.Parameters) @@ -47,6 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework rewritten |= this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); } + // custom attributes + rewritten |= this.RewriteCustomAttributesIfNeeded(module, method.CustomAttributes); + // local variables foreach (VariableDefinition variable in method.Body.Variables) { @@ -116,5 +133,72 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The type to replace if it matches. /// Assign the new type reference. protected abstract bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set); + + /// Rewrite custom attributes if needed. + /// The assembly module containing the attributes. + /// The custom attributes to handle. + private bool RewriteCustomAttributesIfNeeded(ModuleDefinition module, Collection attributes) + { + bool rewritten = false; + + for (int attrIndex = 0; attrIndex < attributes.Count; attrIndex++) + { + CustomAttribute attribute = attributes[attrIndex]; + bool curChanged = false; + + // attribute type + TypeReference newAttrType = null; + if (this.Finder.IsMatch(attribute.AttributeType)) + { + rewritten |= this.RewriteIfNeeded(module, attribute.AttributeType, newType => + { + newAttrType = newType; + curChanged = true; + }); + } + + // constructor arguments + TypeReference[] argTypes = new TypeReference[attribute.ConstructorArguments.Count]; + for (int i = 0; i < argTypes.Length; i++) + { + var arg = attribute.ConstructorArguments[i]; + + argTypes[i] = arg.Type; + rewritten |= this.RewriteIfNeeded(module, arg.Type, newType => + { + argTypes[i] = newType; + curChanged = true; + }); + } + + // swap attribute + if (curChanged) + { + // get constructor + MethodDefinition constructor = (newAttrType ?? attribute.AttributeType) + .Resolve() + .Methods + .Where(method => method.IsConstructor) + .FirstOrDefault(ctor => RewriteHelper.HasMatchingSignature(ctor, attribute.Constructor)); + if (constructor == null) + throw new InvalidOperationException($"Can't rewrite attribute type '{attribute.AttributeType.FullName}' to '{newAttrType?.FullName}', no equivalent constructor found."); + + // create new attribute + var newAttr = new CustomAttribute(module.ImportReference(constructor)); + for (int i = 0; i < argTypes.Length; i++) + newAttr.ConstructorArguments.Add(new CustomAttributeArgument(argTypes[i], attribute.ConstructorArguments[i].Value)); + foreach (var prop in attribute.Properties) + newAttr.Properties.Add(new CustomAttributeNamedArgument(prop.Name, prop.Argument)); + foreach (var field in attribute.Fields) + newAttr.Fields.Add(new CustomAttributeNamedArgument(field.Name, field.Argument)); + + // swap attribute + attributes[attrIndex] = newAttr; + rewritten = true; + } + } + + return rewritten; + } } } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 65b45b08..f9d320a6 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -16,6 +16,13 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index d9a49cfa..553679f9 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -42,6 +42,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The type reference. public static bool IsSameType(Type type, TypeReference reference) { + // + // duplicated by IsSameType(TypeReference, TypeReference) below + // + // same namespace & name if (type.Namespace != reference.Namespace || type.Name != reference.Name) return false; @@ -66,6 +70,39 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + public static bool IsSameType(TypeReference type, TypeReference reference) + { + // + // duplicated by IsSameType(Type, TypeReference) above + // + + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericInstance) + { + if (!reference.IsGenericInstance) + return false; + + TypeReference[] defGenerics = ((GenericInstanceType)type).GenericArguments.ToArray(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. /// The type ID to compare. /// The other type ID to compare. @@ -80,6 +117,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The method reference. public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) { + // + // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below + // + // same name if (definition.Name != reference.Name) return false; @@ -97,6 +138,32 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodInfo, MethodReference) above + // + + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterDefinition[] definitionParameters = definition.Parameters.ToArray(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + /// Get whether a type has a method whose signature matches the one expected by a method reference. /// The type to check. /// The method reference. -- cgit