oni-priority-ux/mod/PLibCore/PTranspilerTools.cs

360 lines
13 KiB
C#

/*
* Copyright 2022 Peter Han
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace PeterHan.PLib.Core {
/// <summary>
/// A utility class with transpiler tools.
/// </summary>
internal static class PTranspilerTools {
/// <summary>
/// The opcodes that branch control conditionally.
/// </summary>
private static readonly ISet<OpCode> BRANCH_CODES;
/// <summary>
/// Opcodes to load an integer onto the stack.
/// </summary>
internal static readonly OpCode[] LOAD_INT = {
OpCodes.Ldc_I4_M1, OpCodes.Ldc_I4_0, OpCodes.Ldc_I4_1, OpCodes.Ldc_I4_2,
OpCodes.Ldc_I4_3, OpCodes.Ldc_I4_4, OpCodes.Ldc_I4_5, OpCodes.Ldc_I4_6,
OpCodes.Ldc_I4_7, OpCodes.Ldc_I4_8
};
static PTranspilerTools() {
// OpCode has a GetHashCode method!
BRANCH_CODES = new HashSet<OpCode> {
OpCodes.Beq,
OpCodes.Beq_S,
OpCodes.Bge,
OpCodes.Bge_S,
OpCodes.Bge_Un,
OpCodes.Bge_Un_S,
OpCodes.Bgt,
OpCodes.Bgt_S,
OpCodes.Bgt_Un,
OpCodes.Bgt_Un_S,
OpCodes.Ble,
OpCodes.Ble_S,
OpCodes.Ble_Un,
OpCodes.Ble_Un_S,
OpCodes.Blt,
OpCodes.Blt_S,
OpCodes.Blt_Un,
OpCodes.Blt_Un_S,
OpCodes.Bne_Un,
OpCodes.Bne_Un_S,
OpCodes.Brfalse,
OpCodes.Brfalse_S,
OpCodes.Brtrue,
OpCodes.Brtrue_S,
};
}
/// <summary>
/// Compares the method parameters and throws ArgumentException if they do not match.
/// </summary>
/// <param name="victim">The victim method.</param>
/// <param name="paramTypes">The method's parameter types.</param>
/// <param name="newMethod">The replacement method.</param>
internal static void CompareMethodParams(MethodInfo victim, Type[] paramTypes,
MethodInfo newMethod) {
var newTypes = GetParameterTypes(newMethod);
if (!newMethod.IsStatic)
newTypes = PushDeclaringType(newTypes, newMethod.DeclaringType);
if (!victim.IsStatic)
paramTypes = PushDeclaringType(paramTypes, victim.DeclaringType);
int n = paramTypes.Length;
// Argument count check
if (newTypes.Length != n)
throw new ArgumentException(("New method {0} ({1:D} arguments) does not " +
"match method {2} ({3:D} arguments)").F(newMethod.Name, newTypes.Length,
victim.Name, n));
// Argument type check
for (int i = 0; i < n; i++)
if (!newTypes[i].IsAssignableFrom(paramTypes[i]))
throw new ArgumentException(("Argument {0:D}: New method type {1} does " +
"not match old method type {2}").F(i, paramTypes[i].FullName,
newTypes[i].FullName));
if (!victim.ReturnType.IsAssignableFrom(newMethod.ReturnType))
throw new ArgumentException(("New method {0} (returns {1}) does not match " +
"method {2} (returns {3})").F(newMethod.Name, newMethod.
ReturnType, victim.Name, victim.ReturnType));
}
/// <summary>
/// Pushes the specified value onto the evaluation stack. This method does not work on
/// compound value types or by-ref types, as those need a local variable. If the value
/// is DBNull.Value, then default(value) will be used instead.
/// </summary>
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
/// <param name="type">The type of the value to generate.</param>
/// <param name="value">The value to load.</param>
/// <returns>true if instructions were pushed (all basic types and reference types),
/// or false otherwise (by ref type or compound value type).</returns>
private static bool GenerateBasicLoad(ILGenerator generator, Type type, object value) {
bool ok = !type.IsByRef;
if (ok) {
if (type == typeof(int)) {
// int
if (value is int iVal)
generator.Emit(OpCodes.Ldc_I4, iVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(char)) {
// char
if (value is char cVal)
generator.Emit(OpCodes.Ldc_I4, cVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(short)) {
// short
if (value is short sVal)
generator.Emit(OpCodes.Ldc_I4, sVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(uint)) {
// uint
if (value is uint uiVal)
generator.Emit(OpCodes.Ldc_I4, (int)uiVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(ushort)) {
// ushort
if (value is ushort usVal)
generator.Emit(OpCodes.Ldc_I4, usVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(byte)) {
// byte (unsigned)
if (value is byte bVal)
generator.Emit(OpCodes.Ldc_I4_S, bVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(sbyte)) {
// byte (signed)
if (value is sbyte sbVal)
generator.Emit(OpCodes.Ldc_I4, sbVal);
else
generator.Emit(OpCodes.Ldc_I4_0);
} else if (type == typeof(bool))
// bool
generator.Emit((value is bool kVal && kVal) ? OpCodes.Ldc_I4_1 : OpCodes.
Ldc_I4_0);
else if (type == typeof(long))
// long
generator.Emit(OpCodes.Ldc_I8, (value is long lVal) ? lVal : 0L);
else if (type == typeof(ulong))
// ulong
generator.Emit(OpCodes.Ldc_I8, (value is ulong ulVal) ? (long)ulVal : 0L);
else if (type == typeof(float))
// float
generator.Emit(OpCodes.Ldc_R4, (value is float fVal) ? fVal : 0.0f);
else if (type == typeof(double))
// double
generator.Emit(OpCodes.Ldc_R8, (value is double dVal) ? dVal : 0.0);
else if (type == typeof(string))
// string
generator.Emit(OpCodes.Ldstr, (value is string sVal) ? sVal : "");
else if (type.IsPointer)
// All pointers
generator.Emit(OpCodes.Ldc_I4_0);
else if (!type.IsValueType)
// All reference types (including Nullable)
generator.Emit(OpCodes.Ldnull);
else
ok = false;
}
return ok;
}
/// <summary>
/// Creates a local if necessary, and generates initialization code for the default
/// value of the specified type. The resulting value ends up on the stack in a form
/// that it would be used for the method argument.
/// </summary>
/// <param name="generator">The IL generator where the opcodes will be emitted.</param>
/// <param name="type">The type to load and initialize.</param>
/// <param name="defaultValue">The default value to load.</param>
internal static void GenerateDefaultLoad(ILGenerator generator, Type type,
object defaultValue) {
if (type == null)
throw new ArgumentNullException(nameof(type));
if (!GenerateBasicLoad(generator, type, defaultValue)) {
// This method will fail if there are more than 255 local variables, oh no!
if (type.IsByRef) {
var baseType = type.GetElementType();
var localVariable = generator.DeclareLocal(baseType);
int index = localVariable.LocalIndex;
if (GenerateBasicLoad(generator, baseType, defaultValue))
// Reference type or basic type
generator.Emit(OpCodes.Stloc_S, index);
else {
// Value types not handled by it, ref vars cannot have a default value
generator.Emit(OpCodes.Ldloca_S, index);
generator.Emit(OpCodes.Initobj, type);
}
generator.Emit(OpCodes.Ldloca_S, index);
} else {
var localVariable = generator.DeclareLocal(type);
int index = localVariable.LocalIndex;
// Is a value type, those cannot have default values other than default()
// as it must be constant
generator.Emit(OpCodes.Ldloca_S, index);
generator.Emit(OpCodes.Initobj, type);
generator.Emit(OpCodes.Ldloc_S, index);
}
}
}
/// <summary>
/// Gets the method's parameter types.
/// </summary>
/// <param name="method">The method to query.</param>
/// <returns>The type of each parameter of the method.</returns>
internal static Type[] GetParameterTypes(this MethodInfo method) {
if (method == null)
throw new ArgumentNullException(nameof(method));
var pm = method.GetParameters();
int n = pm.Length;
var types = new Type[n];
for (int i = 0; i < n; i++)
types[i] = pm[i].ParameterType;
return types;
}
/// <summary>
/// Checks to see if an instruction opcode is a branch instruction.
/// </summary>
/// <param name="opcode">The opcode to check.</param>
/// <returns>true if it is a branch, or false otherwise.</returns>
internal static bool IsConditionalBranchInstruction(OpCode opcode) {
return BRANCH_CODES.Contains(opcode);
}
/// <summary>
/// Adds a logger to all unhandled exceptions.
///
/// Not for production use.
/// </summary>
internal static void LogAllExceptions() {
AppDomain.CurrentDomain.UnhandledException += OnThrown;
}
/// <summary>
/// Adds a logger to all failed assertions. The assertions will still fail, but a stack
/// trace will be printed for each failed assertion.
///
/// Not for production use.
/// </summary>
internal static void LogAllFailedAsserts() {
var inst = new Harmony("PeterHan.PLib.LogFailedAsserts");
MethodBase assert;
var handler = new HarmonyMethod(typeof(PTranspilerTools), nameof(OnAssertFailed));
try {
// Assert(bool)
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool));
if (assert != null)
inst.Patch(assert, handler);
// Assert(bool, object)
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
object));
if (assert != null)
inst.Patch(assert, handler);
// Assert(bool, object, UnityEngine.Object)
assert = typeof(Debug).GetMethodSafe("Assert", true, typeof(bool), typeof(
object), typeof(UnityEngine.Object));
if (assert != null)
inst.Patch(assert, handler);
} catch (Exception e) {
PUtil.LogException(e);
}
}
/// <summary>
/// Modifies a load instruction to load the specified constant, using short forms if
/// possible.
/// </summary>
/// <param name="instruction">The instruction to modify.</param>
/// <param name="newValue">The new i4 constant to load.</param>
internal static void ModifyLoadI4(CodeInstruction instruction, int newValue) {
if (newValue >= -1 && newValue <= 8) {
// Short form: constant
instruction.opcode = LOAD_INT[newValue + 1];
instruction.operand = null;
} else if (newValue >= sbyte.MinValue && newValue <= sbyte.MaxValue) {
// Short form: -128 to 127 -- looks like Harmony has issues with emitting
// the operand as a Byte
instruction.opcode = OpCodes.Ldc_I4_S;
instruction.operand = newValue;
} else {
// Long form
instruction.opcode = OpCodes.Ldc_I4;
instruction.operand = newValue;
}
}
/// <summary>
/// Logs a failed assertion that is about to occur.
/// </summary>
internal static void OnAssertFailed(bool condition) {
if (!condition) {
Debug.LogError("Assert is about to fail:");
Debug.LogError(new System.Diagnostics.StackTrace().ToString());
}
}
/// <summary>
/// An optional handler for all unhandled exceptions.
/// </summary>
internal static void OnThrown(object sender, UnhandledExceptionEventArgs e) {
if (!e.IsTerminating) {
Debug.LogError("Unhandled exception on Thread " + System.Threading.Thread.
CurrentThread.Name);
if (e.ExceptionObject is Exception ex)
Debug.LogException(ex);
else
Debug.LogError(e.ExceptionObject);
}
}
/// <summary>
/// Inserts the declaring instance type to the front of the specified array.
/// </summary>
/// <param name="types">The parameter types.</param>
/// <param name="declaringType">The type which declared this method.</param>
/// <returns>The types with declaringType inserted at the beginning.</returns>
internal static Type[] PushDeclaringType(Type[] types, Type declaringType) {
int n = types.Length;
// Allow special case of passing "this" as first static arg
var newParamTypes = new Type[n + 1];
if (declaringType.IsValueType)
declaringType = declaringType.MakeByRefType();
newParamTypes[0] = declaringType;
for (int i = 0; i < n; i++)
newParamTypes[i + 1] = types[i];
return newParamTypes;
}
}
}