/* * 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 { /// /// A utility class with transpiler tools. /// internal static class PTranspilerTools { /// /// The opcodes that branch control conditionally. /// private static readonly ISet BRANCH_CODES; /// /// Opcodes to load an integer onto the stack. /// 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 { 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, }; } /// /// Compares the method parameters and throws ArgumentException if they do not match. /// /// The victim method. /// The method's parameter types. /// The replacement method. 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)); } /// /// 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. /// /// The IL generator where the opcodes will be emitted. /// The type of the value to generate. /// The value to load. /// true if instructions were pushed (all basic types and reference types), /// or false otherwise (by ref type or compound value type). 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; } /// /// 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. /// /// The IL generator where the opcodes will be emitted. /// The type to load and initialize. /// The default value to load. 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); } } } /// /// Gets the method's parameter types. /// /// The method to query. /// The type of each parameter of the method. 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; } /// /// Checks to see if an instruction opcode is a branch instruction. /// /// The opcode to check. /// true if it is a branch, or false otherwise. internal static bool IsConditionalBranchInstruction(OpCode opcode) { return BRANCH_CODES.Contains(opcode); } /// /// Adds a logger to all unhandled exceptions. /// /// Not for production use. /// internal static void LogAllExceptions() { AppDomain.CurrentDomain.UnhandledException += OnThrown; } /// /// 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. /// 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); } } /// /// Modifies a load instruction to load the specified constant, using short forms if /// possible. /// /// The instruction to modify. /// The new i4 constant to load. 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; } } /// /// Logs a failed assertion that is about to occur. /// internal static void OnAssertFailed(bool condition) { if (!condition) { Debug.LogError("Assert is about to fail:"); Debug.LogError(new System.Diagnostics.StackTrace().ToString()); } } /// /// An optional handler for all unhandled exceptions. /// 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); } } /// /// Inserts the declaring instance type to the front of the specified array. /// /// The parameter types. /// The type which declared this method. /// The types with declaringType inserted at the beginning. 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; } } }