merge CIL finders & rewriters into one interface (#254)

This commit is contained in:
Jesse Plamondon-Willard 2017-03-26 19:01:35 -04:00
parent 23443721cd
commit 85ed488090
15 changed files with 178 additions and 99 deletions

View File

@ -3,8 +3,8 @@ using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters.Finders
{
/// <summary>Finds CIL instructions that reference a given event.</summary>
public sealed class EventFinder : IInstructionFinder
/// <summary>Finds incompatible CIL instructions that reference a given event and throws an <see cref="IncompatibleInstructionException"/>.</summary>
public class EventFinder : IInstructionRewriter
{
/*********
** Properties
@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{eventName} event";
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
throw new IncompatibleInstructionException(this.NounPhrase);
}
/*********
** Protected methods
@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public bool IsMatch(Instruction instruction, bool platformChanged)
protected bool IsMatch(Instruction instruction, bool platformChanged)
{
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
return

View File

@ -3,8 +3,8 @@ using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters.Finders
{
/// <summary>Finds CIL instructions that reference a given field.</summary>
public class FieldFinder : IInstructionFinder
/// <summary>Finds incompatible CIL instructions that reference a given field and throws an <see cref="IncompatibleInstructionException"/>.</summary>
public class FieldFinder : IInstructionRewriter
{
/*********
** Properties
@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{fieldName} field";
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
throw new IncompatibleInstructionException(this.NounPhrase);
}
/*********
** Protected methods
@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public bool IsMatch(Instruction instruction, bool platformChanged)
protected bool IsMatch(Instruction instruction, bool platformChanged)
{
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
return

View File

@ -3,8 +3,8 @@ using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters.Finders
{
/// <summary>Finds CIL instructions that reference a given method.</summary>
public class MethodFinder : IInstructionFinder
/// <summary>Finds incompatible CIL instructions that reference a given method and throws an <see cref="IncompatibleInstructionException"/>.</summary>
public class MethodFinder : IInstructionRewriter
{
/*********
** Properties
@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{methodName} method";
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
throw new IncompatibleInstructionException(this.NounPhrase);
}
/*********
** Protected methods
@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public bool IsMatch(Instruction instruction, bool platformChanged)
protected bool IsMatch(Instruction instruction, bool platformChanged)
{
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
return

View File

@ -4,8 +4,8 @@ using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters.Finders
{
/// <summary>Finds CIL instructions that reference a given type.</summary>
public class TypeFinder : IInstructionFinder
/// <summary>Finds incompatible CIL instructions that reference a given type and throws an <see cref="IncompatibleInstructionException"/>.</summary>
public class TypeFinder : IInstructionRewriter
{
/*********
** Accessors
@ -33,10 +33,30 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders
this.NounPhrase = nounPhrase ?? $"{fullTypeName} type";
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
throw new IncompatibleInstructionException(this.NounPhrase);
}
/*********
** Protected methods
*********/
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public bool IsMatch(Instruction instruction, bool platformChanged)
protected bool IsMatch(Instruction instruction, bool platformChanged)
{
string fullName = this.FullTypeName;

View File

@ -1,23 +0,0 @@
using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters
{
/// <summary>Finds CIL instructions considered incompatible.</summary>
public interface IInstructionFinder
{
/*********
** Accessors
*********/
/// <summary>A brief noun phrase indicating what the instruction finder matches.</summary>
string NounPhrase { get; }
/*********
** Methods
*********/
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
bool IsMatch(Instruction instruction, bool platformChanged);
}
}

View File

@ -3,9 +3,16 @@ using Mono.Cecil.Cil;
namespace StardewModdingAPI.AssemblyRewriters
{
/// <summary>Rewrites a CIL instruction for compatibility.</summary>
public interface IInstructionRewriter : IInstructionFinder
/// <summary>Rewrites CIL instructions for compatibility.</summary>
public interface IInstructionRewriter
{
/*********
** Accessors
*********/
/// <summary>A brief noun phrase indicating what the rewriter matches.</summary>
string NounPhrase { get; }
/*********
** Methods
*********/
@ -14,6 +21,9 @@ namespace StardewModdingAPI.AssemblyRewriters
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap);
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged);
}
}

View File

@ -1,9 +1,9 @@
using System;
namespace StardewModdingAPI.Framework
namespace StardewModdingAPI.AssemblyRewriters
{
/// <summary>An exception raised when an incompatible instruction is found while loading a mod assembly.</summary>
internal class IncompatibleInstructionException : Exception
public class IncompatibleInstructionException : Exception
{
/*********
** Accessors
@ -15,6 +15,14 @@ namespace StardewModdingAPI.Framework
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="nounPhrase">A brief noun phrase which describes the incompatible instruction that was found.</param>
public IncompatibleInstructionException(string nounPhrase)
: base($"Found an incompatible CIL instruction ({nounPhrase}).")
{
this.NounPhrase = nounPhrase;
}
/// <summary>Construct an instance.</summary>
/// <param name="nounPhrase">A brief noun phrase which describes the incompatible instruction that was found.</param>
/// <param name="message">A message which describes the error.</param>

View File

@ -7,16 +7,13 @@ using StardewModdingAPI.AssemblyRewriters.Finders;
namespace StardewModdingAPI.AssemblyRewriters.Rewriters
{
/// <summary>Rewrites references to one field with another.</summary>
public class FieldReplaceRewriter : FieldFinder, IInstructionRewriter
public class FieldReplaceRewriter : FieldFinder
{
/*********
** Properties
*********/
/// <summary>The type whose field to which references should be rewritten.</summary>
private readonly Type Type;
/// <summary>The new field name to reference.</summary>
private readonly string ToFieldName;
/// <summary>The new field to reference.</summary>
private readonly FieldInfo ToField;
/*********
@ -30,8 +27,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName, string nounPhrase = null)
: base(type.FullName, fromFieldName, nounPhrase)
{
this.Type = type;
this.ToFieldName = toFieldName;
this.ToField = type.GetField(toFieldName);
if (this.ToField == null)
throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field.");
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
@ -39,13 +37,17 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap)
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
FieldInfo field = this.Type.GetField(this.ToFieldName);
if (field == null)
throw new InvalidOperationException($"The {this.Type.FullName} class doesn't have a {this.ToFieldName} field.");
FieldReference newRef = module.Import(field);
if (!this.IsMatch(instruction, platformChanged))
return false;
FieldReference newRef = module.Import(this.ToField);
cil.Replace(instruction, cil.Create(instruction.OpCode, newRef));
return true;
}
}
}

View File

@ -6,7 +6,7 @@ using StardewModdingAPI.AssemblyRewriters.Finders;
namespace StardewModdingAPI.AssemblyRewriters.Rewriters
{
/// <summary>Rewrites field references into property references.</summary>
public class FieldToPropertyRewriter : FieldFinder, IInstructionRewriter
public class FieldToPropertyRewriter : FieldFinder
{
/*********
** Properties
@ -37,11 +37,18 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap)
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set";
MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}"));
cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef));
return true;
}
}
}

View File

@ -43,10 +43,32 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
this.OnlyIfPlatformChanged = onlyIfPlatformChanged;
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
/// <returns>Returns whether the instruction was rewritten.</returns>
/// <exception cref="IncompatibleInstructionException">The CIL instruction is not compatible, and can't be rewritten.</exception>
public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged)
{
if (!this.IsMatch(instruction, platformChanged))
return false;
MethodReference methodRef = (MethodReference)instruction.Operand;
methodRef.DeclaringType = module.Import(this.ToType);
return true;
}
/*********
** Protected methods
*********/
/// <summary>Get whether a CIL instruction matches.</summary>
/// <param name="instruction">The IL instruction.</param>
/// <param name="platformChanged">Whether the mod was compiled on a different platform.</param>
public bool IsMatch(Instruction instruction, bool platformChanged)
protected bool IsMatch(Instruction instruction, bool platformChanged)
{
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
return
@ -55,16 +77,5 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters
&& methodRef.DeclaringType.FullName == this.FromType.FullName
&& RewriteHelper.HasMatchingSignature(this.ToType, methodRef);
}
/// <summary>Rewrite a CIL instruction for compatibility.</summary>
/// <param name="module">The module being rewritten.</param>
/// <param name="cil">The CIL rewriter.</param>
/// <param name="instruction">The instruction to rewrite.</param>
/// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap)
{
MethodReference methodRef = (MethodReference)instruction.Operand;
methodRef.DeclaringType = module.Import(this.ToType);
}
}
}

View File

@ -70,8 +70,8 @@
<Compile Include="Finders\FieldFinder.cs" />
<Compile Include="Finders\MethodFinder.cs" />
<Compile Include="Finders\TypeFinder.cs" />
<Compile Include="IncompatibleInstructionException.cs" />
<Compile Include="RewriteHelper.cs" />
<Compile Include="IInstructionFinder.cs" />
<Compile Include="IInstructionRewriter.cs" />
<Compile Include="Platform.cs" />
<Compile Include="PlatformAssemblyMap.cs" />

View File

@ -137,12 +137,15 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
}
/// <summary>Get finders which match incompatible CIL instructions in mod assemblies.</summary>
internal static IEnumerable<IInstructionFinder> GetIncompatibilityFinders()
/// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary>
internal static IEnumerable<IInstructionRewriter> GetRewriters()
{
return new IInstructionFinder[]
return new IInstructionRewriter[]
{
// changes in Stardew Valley 1.2 (that don't have rewriters)
/****
** Finders throw an exception when incompatible code is found.
****/
// changes in Stardew Valley 1.2 (with no rewriters)
new FieldFinder("StardewValley.Item", "set_Name"),
// APIs removed in SMAPI 1.9
@ -161,15 +164,11 @@ namespace StardewModdingAPI
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck")
};
}
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"),
/// <summary>Get rewriters which fix incompatible CIL instructions in mod assemblies.</summary>
internal static IEnumerable<IInstructionRewriter> GetRewriters()
{
return new IInstructionRewriter[]
{
/****
** Rewriters change CIL as needed to fix incompatible code
****/
// crossplatform
new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true),

View File

@ -193,33 +193,30 @@ namespace StardewModdingAPI.Framework
this.ChangeTypeScope(type);
}
// find incompatible instructions
// find (and optionally rewrite) incompatible instructions
bool anyRewritten = false;
IInstructionFinder[] finders = Constants.GetIncompatibilityFinders().ToArray();
IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray();
foreach (MethodDefinition method in this.GetMethods(module))
{
ILProcessor cil = method.Body.GetILProcessor();
foreach (Instruction instruction in cil.Body.Instructions.ToArray())
{
// throw exception if instruction is incompatible but can't be rewritten
IInstructionFinder finder = finders.FirstOrDefault(p => p.IsMatch(instruction, platformChanged));
if (finder != null)
{
if (!assumeCompatible)
throw new IncompatibleInstructionException(finder.NounPhrase, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}.");
this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn);
}
// rewrite instruction if needed
foreach (IInstructionRewriter rewriter in rewriters)
{
if (!rewriter.IsMatch(instruction, platformChanged))
continue;
this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} to fix {rewriter.NounPhrase}...");
rewriter.Rewrite(module, cil, instruction, this.AssemblyMap);
anyRewritten = true;
try
{
if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged))
{
this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}...");
anyRewritten = true;
}
}
catch (IncompatibleInstructionException)
{
if (!assumeCompatible)
throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}.");
this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn);
}
}
}
}

View File

@ -12,6 +12,7 @@ using System.Windows.Forms;
#endif
using Microsoft.Xna.Framework.Graphics;
using Newtonsoft.Json;
using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.Logging;

View File

@ -150,7 +150,6 @@
<Compile Include="Framework\Content\ContentEventHelper.cs" />
<Compile Include="Framework\Content\ContentEventHelperForDictionary.cs" />
<Compile Include="Framework\Content\ContentEventHelperForImage.cs" />
<Compile Include="Framework\IncompatibleInstructionException.cs" />
<Compile Include="Framework\Logging\ConsoleInterceptionManager.cs" />
<Compile Include="Framework\Logging\InterceptingTextWriter.cs" />
<Compile Include="Framework\CommandHelper.cs" />