SMAPI/ModLoader/Harmony/ILCopying/MethodCopier.cs

730 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
namespace Harmony.ILCopying
{
public class MethodCopier
{
readonly MethodBodyReader reader;
readonly List<MethodInfo> transpilers = new List<MethodInfo>();
public MethodCopier(MethodBase fromMethod, ILGenerator toILGenerator, LocalBuilder[] existingVariables = null)
{
if (fromMethod == null) throw new ArgumentNullException("Method cannot be null");
reader = new MethodBodyReader(fromMethod, toILGenerator);
reader.DeclareVariables(existingVariables);
reader.ReadInstructions();
}
public void AddTranspiler(MethodInfo transpiler)
{
transpilers.Add(transpiler);
}
public void Finalize(List<Label> endLabels, List<ExceptionBlock> endBlocks)
{
reader.FinalizeILCodes(transpilers, endLabels, endBlocks);
}
}
public class MethodBodyReader
{
readonly ILGenerator generator;
readonly MethodBase method;
readonly Module module;
readonly Type[] typeArguments;
readonly Type[] methodArguments;
readonly ByteBuffer ilBytes;
readonly ParameterInfo this_parameter;
readonly ParameterInfo[] parameters;
readonly IList<LocalVariableInfo> locals;
readonly IList<ExceptionHandlingClause> exceptions;
List<ILInstruction> ilInstructions;
LocalBuilder[] variables;
// NOTE: you cannot simply "copy" ILInstructions from a method. They contain references to
// local variables which must be CREATED on an ILGenerator or else they are invalid when you
// want to use the ILInstruction. If you are really clever, you can supply a dummy generator
// and edit out all labels during the processing but that might be more trickier than you think
//
// In order to copy together a bunch of method parts within a transpiler, you have to pass in
// your current generator that builds your new method
//
// You will end up with the sum of all declared local variables of all methods you run
// GetInstructions on or use a dummy generator but edit out the invalid labels from the codes
// you copy
//
public static List<ILInstruction> GetInstructions(ILGenerator generator, MethodBase method)
{
if (method == null) throw new ArgumentNullException("Method cannot be null");
var reader = new MethodBodyReader(method, generator);
reader.DeclareVariables(null);
reader.ReadInstructions();
return reader.ilInstructions;
}
// constructor
//
public MethodBodyReader(MethodBase method, ILGenerator generator)
{
this.generator = generator;
this.method = method;
module = method.Module;
var body = method.GetMethodBody();
if (body == null)
throw new ArgumentException("Method " + method.FullDescription() + " has no body");
var bytes = body.GetILAsByteArray();
if (bytes == null)
throw new ArgumentException("Can not get IL bytes of method " + method.FullDescription());
ilBytes = new ByteBuffer(bytes);
ilInstructions = new List<ILInstruction>((bytes.Length + 1) / 2);
var type = method.DeclaringType;
if (type.IsGenericType)
{
try { typeArguments = type.GetGenericArguments(); }
catch { typeArguments = null; }
}
if (method.IsGenericMethod)
{
try { methodArguments = method.GetGenericArguments(); }
catch { methodArguments = null; }
}
if (!method.IsStatic)
this_parameter = new ThisParameter(method);
parameters = method.GetParameters();
locals = body.LocalVariables;
exceptions = body.ExceptionHandlingClauses;
}
// read and parse IL codes
//
public void ReadInstructions()
{
while (ilBytes.position < ilBytes.buffer.Length)
{
var loc = ilBytes.position; // get location first (ReadOpCode will advance it)
var instruction = new ILInstruction(ReadOpCode()) { offset = loc };
ReadOperand(instruction);
ilInstructions.Add(instruction);
}
ResolveBranches();
ParseExceptions();
}
// declare local variables
//
public void DeclareVariables(LocalBuilder[] existingVariables)
{
if (generator == null) return;
if (existingVariables != null)
variables = existingVariables;
else
variables = locals.Select(
lvi => generator.DeclareLocal(lvi.LocalType, lvi.IsPinned)
).ToArray();
}
// process all jumps
//
void ResolveBranches()
{
foreach (var ilInstruction in ilInstructions)
{
switch (ilInstruction.opcode.OperandType)
{
case OperandType.ShortInlineBrTarget:
case OperandType.InlineBrTarget:
ilInstruction.operand = GetInstruction((int)ilInstruction.operand, false);
break;
case OperandType.InlineSwitch:
var offsets = (int[])ilInstruction.operand;
var branches = new ILInstruction[offsets.Length];
for (var j = 0; j < offsets.Length; j++)
branches[j] = GetInstruction(offsets[j], false);
ilInstruction.operand = branches;
break;
}
}
}
// process all exception blocks
//
void ParseExceptions()
{
foreach (var exception in exceptions)
{
var try_start = exception.TryOffset;
var try_end = exception.TryOffset + exception.TryLength - 1;
var handler_start = exception.HandlerOffset;
var handler_end = exception.HandlerOffset + exception.HandlerLength - 1;
var instr1 = GetInstruction(try_start, false);
instr1.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginExceptionBlock, null));
var instr2 = GetInstruction(handler_end, true);
instr2.blocks.Add(new ExceptionBlock(ExceptionBlockType.EndExceptionBlock, null));
// The FilterOffset property is meaningful only for Filter clauses.
// The CatchType property is not meaningful for Filter or Finally clauses.
//
switch (exception.Flags)
{
case ExceptionHandlingClauseOptions.Filter:
var instr3 = GetInstruction(exception.FilterOffset, false);
instr3.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginExceptFilterBlock, null));
break;
case ExceptionHandlingClauseOptions.Finally:
var instr4 = GetInstruction(handler_start, false);
instr4.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginFinallyBlock, null));
break;
case ExceptionHandlingClauseOptions.Clause:
var instr5 = GetInstruction(handler_start, false);
instr5.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginCatchBlock, exception.CatchType));
break;
case ExceptionHandlingClauseOptions.Fault:
var instr6 = GetInstruction(handler_start, false);
instr6.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginFaultBlock, null));
break;
}
}
}
// used in FinalizeILCodes to convert short jumps to long ones
static Dictionary<OpCode, OpCode> shortJumps = new Dictionary<OpCode, OpCode>()
{
{ OpCodes.Leave_S, OpCodes.Leave },
{ OpCodes.Brfalse_S, OpCodes.Brfalse },
{ OpCodes.Brtrue_S, OpCodes.Brtrue },
{ OpCodes.Beq_S, OpCodes.Beq },
{ OpCodes.Bge_S, OpCodes.Bge },
{ OpCodes.Bgt_S, OpCodes.Bgt },
{ OpCodes.Ble_S, OpCodes.Ble },
{ OpCodes.Blt_S, OpCodes.Blt },
{ OpCodes.Bne_Un_S, OpCodes.Bne_Un },
{ OpCodes.Bge_Un_S, OpCodes.Bge_Un },
{ OpCodes.Bgt_Un_S, OpCodes.Bgt_Un },
{ OpCodes.Ble_Un_S, OpCodes.Ble_Un },
{ OpCodes.Br_S, OpCodes.Br },
{ OpCodes.Blt_Un_S, OpCodes.Blt_Un }
};
// use parsed IL codes and emit them to a generator
//
public void FinalizeILCodes(List<MethodInfo> transpilers, List<Label> endLabels, List<ExceptionBlock> endBlocks)
{
if (generator == null) return;
// pass1 - define labels and add them to instructions that are target of a jump
//
foreach (var ilInstruction in ilInstructions)
{
switch (ilInstruction.opcode.OperandType)
{
case OperandType.InlineSwitch:
{
var targets = ilInstruction.operand as ILInstruction[];
if (targets != null)
{
var labels = new List<Label>();
foreach (var target in targets)
{
var label = generator.DefineLabel();
target.labels.Add(label);
labels.Add(label);
}
ilInstruction.argument = labels.ToArray();
}
break;
}
case OperandType.ShortInlineBrTarget:
case OperandType.InlineBrTarget:
{
var target = ilInstruction.operand as ILInstruction;
if (target != null)
{
var label = generator.DefineLabel();
target.labels.Add(label);
ilInstruction.argument = label;
}
break;
}
}
}
// pass2 - filter through all processors
//
var codeTranspiler = new CodeTranspiler(ilInstructions);
transpilers.Do(transpiler => codeTranspiler.Add(transpiler));
var codeInstructions = codeTranspiler.GetResult(generator, method);
// pass3 - remove RET if it appears at the end
while (true)
{
var lastInstruction = codeInstructions.LastOrDefault();
if (lastInstruction == null || lastInstruction.opcode != OpCodes.Ret) break;
// remember any existing labels
endLabels.AddRange(lastInstruction.labels);
codeInstructions.RemoveAt(codeInstructions.Count - 1);
}
// pass4 - mark labels and exceptions and emit codes
//
var idx = 0;
codeInstructions.Do(codeInstruction =>
{
// mark all labels
codeInstruction.labels.Do(label => Emitter.MarkLabel(generator, label));
// start all exception blocks
// TODO: we ignore the resulting label because we have no way to use it
//
codeInstruction.blocks.Do(block => { Label? label; Emitter.MarkBlockBefore(generator, block, out label); });
var code = codeInstruction.opcode;
var operand = codeInstruction.operand;
// replace RET with a jump to the end (outside this code)
if (code == OpCodes.Ret)
{
var endLabel = generator.DefineLabel();
code = OpCodes.Br;
operand = endLabel;
endLabels.Add(endLabel);
}
// replace short jumps with long ones (can be optimized but requires byte counting, not instruction counting)
if (shortJumps.TryGetValue(code, out var longJump))
code = longJump;
var emitCode = true;
//if (code == OpCodes.Leave || code == OpCodes.Leave_S)
//{
// // skip LEAVE on EndExceptionBlock
// if (codeInstruction.blocks.Any(block => block.blockType == ExceptionBlockType.EndExceptionBlock))
// emitCode = false;
// // skip LEAVE on next instruction starts a new exception handler and we are already in
// if (idx < instructions.Length - 1)
// if (instructions[idx + 1].blocks.Any(block => block.blockType != ExceptionBlockType.EndExceptionBlock))
// emitCode = false;
//}
if (emitCode)
{
switch (code.OperandType)
{
case OperandType.InlineNone:
Emitter.Emit(generator, code);
break;
case OperandType.InlineSig:
// TODO the following will fail because we do not convert the token (operand)
// All the decompilers can show the arguments correctly, we just need to find out how
//
if (operand == null) throw new Exception("Wrong null argument: " + codeInstruction);
if ((operand is int) == false) throw new Exception("Wrong Emit argument type " + operand.GetType() + " in " + codeInstruction);
Emitter.Emit(generator, code, (int)operand);
/*
// the following will only work if we can convert the original signature token to the required arguments
//
var callingConvention = System.Runtime.InteropServices.CallingConvention.ThisCall;
var returnType = typeof(object);
var parameterTypes = new[] { typeof(object) };
Emitter.EmitCalli(generator, code, callingConvention, returnType, parameterTypes);
var callingConventions = System.Reflection.CallingConventions.Standard;
var optionalParameterTypes = new[] { typeof(object) };
Emitter.EmitCalli(generator, code, callingConventions, returnType, parameterTypes, optionalParameterTypes);
*/
break;
default:
if (operand == null) throw new Exception("Wrong null argument: " + codeInstruction);
var emitMethod = EmitMethodForType(operand.GetType());
if (emitMethod == null) throw new Exception("Unknown Emit argument type " + operand.GetType() + " in " + codeInstruction);
if (HarmonyInstance.DEBUG) FileLog.LogBuffered(Emitter.CodePos(generator) + code + " " + Emitter.FormatArgument(operand));
emitMethod.Invoke(generator, new object[] { code, operand });
break;
}
}
codeInstruction.blocks.Do(block => Emitter.MarkBlockAfter(generator, block));
idx++;
});
}
// interpret member info value
//
static void GetMemberInfoValue(MemberInfo info, out object result)
{
result = null;
switch (info.MemberType)
{
case MemberTypes.Constructor:
result = (ConstructorInfo)info;
break;
case MemberTypes.Event:
result = (EventInfo)info;
break;
case MemberTypes.Field:
result = (FieldInfo)info;
break;
case MemberTypes.Method:
result = (MethodInfo)info;
break;
case MemberTypes.TypeInfo:
case MemberTypes.NestedType:
result = (Type)info;
break;
case MemberTypes.Property:
result = (PropertyInfo)info;
break;
}
}
// interpret instruction operand
//
void ReadOperand(ILInstruction instruction)
{
switch (instruction.opcode.OperandType)
{
case OperandType.InlineNone:
{
instruction.argument = null;
break;
}
case OperandType.InlineSwitch:
{
var length = ilBytes.ReadInt32();
var base_offset = ilBytes.position + (4 * length);
var branches = new int[length];
for (var i = 0; i < length; i++)
branches[i] = ilBytes.ReadInt32() + base_offset;
instruction.operand = branches;
break;
}
case OperandType.ShortInlineBrTarget:
{
var val = (sbyte)ilBytes.ReadByte();
instruction.operand = val + ilBytes.position;
break;
}
case OperandType.InlineBrTarget:
{
var val = ilBytes.ReadInt32();
instruction.operand = val + ilBytes.position;
break;
}
case OperandType.ShortInlineI:
{
if (instruction.opcode == OpCodes.Ldc_I4_S)
{
var sb = (sbyte)ilBytes.ReadByte();
instruction.operand = sb;
instruction.argument = (sbyte)instruction.operand;
}
else
{
var b = ilBytes.ReadByte();
instruction.operand = b;
instruction.argument = (byte)instruction.operand;
}
break;
}
case OperandType.InlineI:
{
var val = ilBytes.ReadInt32();
instruction.operand = val;
instruction.argument = (int)instruction.operand;
break;
}
case OperandType.ShortInlineR:
{
var val = ilBytes.ReadSingle();
instruction.operand = val;
instruction.argument = (float)instruction.operand;
break;
}
case OperandType.InlineR:
{
var val = ilBytes.ReadDouble();
instruction.operand = val;
instruction.argument = (double)instruction.operand;
break;
}
case OperandType.InlineI8:
{
var val = ilBytes.ReadInt64();
instruction.operand = val;
instruction.argument = (long)instruction.operand;
break;
}
case OperandType.InlineSig:
{
var val = ilBytes.ReadInt32();
var bytes = module.ResolveSignature(val);
instruction.operand = bytes;
instruction.argument = bytes;
Debugger.Log(0, "TEST", "METHOD " + method.FullDescription() + "\n");
Debugger.Log(0, "TEST", "Signature = " + bytes.Select(b => string.Format("0x{0:x02}", b)).Aggregate((a, b) => a + " " + b) + "\n");
Debugger.Break();
break;
}
case OperandType.InlineString:
{
var val = ilBytes.ReadInt32();
instruction.operand = module.ResolveString(val);
instruction.argument = (string)instruction.operand;
break;
}
case OperandType.InlineTok:
{
var val = ilBytes.ReadInt32();
instruction.operand = module.ResolveMember(val, typeArguments, methodArguments);
GetMemberInfoValue((MemberInfo)instruction.operand, out instruction.argument);
break;
}
case OperandType.InlineType:
{
var val = ilBytes.ReadInt32();
instruction.operand = module.ResolveType(val, typeArguments, methodArguments);
instruction.argument = (Type)instruction.operand;
break;
}
case OperandType.InlineMethod:
{
var val = ilBytes.ReadInt32();
instruction.operand = module.ResolveMethod(val, typeArguments, methodArguments);
if (instruction.operand is ConstructorInfo)
instruction.argument = (ConstructorInfo)instruction.operand;
else
instruction.argument = (MethodInfo)instruction.operand;
break;
}
case OperandType.InlineField:
{
var val = ilBytes.ReadInt32();
instruction.operand = module.ResolveField(val, typeArguments, methodArguments);
instruction.argument = (FieldInfo)instruction.operand;
break;
}
case OperandType.ShortInlineVar:
{
var idx = ilBytes.ReadByte();
if (TargetsLocalVariable(instruction.opcode))
{
var lvi = GetLocalVariable(idx);
if (lvi == null)
instruction.argument = idx;
else
{
instruction.operand = lvi;
instruction.argument = variables[lvi.LocalIndex];
}
}
else
{
instruction.operand = GetParameter(idx);
instruction.argument = idx;
}
break;
}
case OperandType.InlineVar:
{
var idx = ilBytes.ReadInt16();
if (TargetsLocalVariable(instruction.opcode))
{
var lvi = GetLocalVariable(idx);
if (lvi == null)
instruction.argument = idx;
else
{
instruction.operand = lvi;
instruction.argument = variables[lvi.LocalIndex];
}
}
else
{
instruction.operand = GetParameter(idx);
instruction.argument = idx;
}
break;
}
default:
throw new NotSupportedException();
}
}
ILInstruction GetInstruction(int offset, bool isEndOfInstruction)
{
var lastInstructionIndex = ilInstructions.Count - 1;
if (offset < 0 || offset > ilInstructions[lastInstructionIndex].offset)
throw new Exception("Instruction offset " + offset + " is outside valid range 0 - " + ilInstructions[lastInstructionIndex].offset);
var min = 0;
var max = lastInstructionIndex;
while (min <= max)
{
var mid = min + ((max - min) / 2);
var instruction = ilInstructions[mid];
if (isEndOfInstruction)
{
if (offset == instruction.offset + instruction.GetSize() - 1)
return instruction;
}
else
{
if (offset == instruction.offset)
return instruction;
}
if (offset < instruction.offset)
max = mid - 1;
else
min = mid + 1;
}
throw new Exception("Cannot find instruction for " + offset.ToString("X4"));
}
static bool TargetsLocalVariable(OpCode opcode)
{
return opcode.Name.Contains("loc");
}
LocalVariableInfo GetLocalVariable(int index)
{
return locals?[index];
}
ParameterInfo GetParameter(int index)
{
if (index == 0)
return this_parameter;
return parameters[index - 1];
}
OpCode ReadOpCode()
{
var op = ilBytes.ReadByte();
return op != 0xfe
? one_byte_opcodes[op]
: two_bytes_opcodes[ilBytes.ReadByte()];
}
MethodInfo EmitMethodForType(Type type)
{
foreach (var entry in emitMethods)
if (entry.Key == type) return entry.Value;
foreach (var entry in emitMethods)
if (entry.Key.IsAssignableFrom(type)) return entry.Value;
return null;
}
// static initializer to prep opcodes
static readonly OpCode[] one_byte_opcodes;
static readonly OpCode[] two_bytes_opcodes;
static readonly Dictionary<Type, MethodInfo> emitMethods;
[MethodImpl(MethodImplOptions.Synchronized)]
static MethodBodyReader()
{
one_byte_opcodes = new OpCode[0xe1];
two_bytes_opcodes = new OpCode[0x1f];
var fields = typeof(OpCodes).GetFields(
BindingFlags.Public | BindingFlags.Static);
foreach (var field in fields)
{
var opcode = (OpCode)field.GetValue(null);
if (opcode.OpCodeType == OpCodeType.Nternal)
continue;
if (opcode.Size == 1)
one_byte_opcodes[opcode.Value] = opcode;
else
two_bytes_opcodes[opcode.Value & 0xff] = opcode;
}
emitMethods = new Dictionary<Type, MethodInfo>();
typeof(ILGenerator).GetMethods().ToList()
.Do(method =>
{
if (method.Name != "Emit") return;
var pinfos = method.GetParameters();
if (pinfos.Length != 2) return;
var types = pinfos.Select(p => p.ParameterType).ToArray();
if (types[0] != typeof(OpCode)) return;
emitMethods[types[1]] = method;
});
}
// a custom this parameter
class ThisParameter : ParameterInfo
{
public ThisParameter(MethodBase method)
{
MemberImpl = method;
ClassImpl = method.DeclaringType;
NameImpl = "this";
PositionImpl = -1;
}
}
}
}