using System; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.InteropServices; using BepInEx.Logging; using HarmonyLib; using HarmonyLib.Public.Patching; using MonoMod.Cil; using MonoMod.RuntimeDetour; using MonoMod.Utils; using UnhollowerBaseLib; using UnhollowerBaseLib.Runtime; using Logger = BepInEx.Logging.Logger; namespace BepInEx.IL2CPP.Hook { public unsafe class IL2CPPDetourMethodPatcher : MethodPatcher { private static readonly MethodInfo IL2CPPToManagedStringMethodInfo = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppStringToManaged)); private static readonly MethodInfo ManagedToIL2CPPStringMethodInfo = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp)); private static readonly MethodInfo ObjectBaseToPtrMethodInfo = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)); private static readonly MethodInfo ReportExceptionMethodInfo = AccessTools.Method(typeof(IL2CPPDetourMethodPatcher), nameof(ReportException)); private static readonly ManualLogSource DetourLogger = Logger.CreateLogSource("Detour"); private FastNativeDetour nativeDetour; private Il2CppMethodInfo* originalNativeMethodInfo; private Il2CppMethodInfo* modifiedNativeMethodInfo; private bool isValid; /// /// Constructs a new instance of method patcher. /// /// public IL2CPPDetourMethodPatcher(MethodBase original) : base(original) { Init(); } private void Init() { try { // Get the native MethodInfo struct for the target method originalNativeMethodInfo = (Il2CppMethodInfo*)(IntPtr)UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original).GetValue(null); // Create a trampoline from the original target method var trampolinePtr = DetourGenerator.CreateTrampolineFromFunction(originalNativeMethodInfo->methodPointer, out _, out _); // Create a modified native MethodInfo struct to point towards the trampoline modifiedNativeMethodInfo = (Il2CppMethodInfo*)Marshal.AllocHGlobal(Marshal.SizeOf()); Marshal.StructureToPtr(*originalNativeMethodInfo, (IntPtr)modifiedNativeMethodInfo, false); modifiedNativeMethodInfo->methodPointer = trampolinePtr; isValid = true; } catch (Exception e) { DetourLogger.LogWarning($"Failed to init IL2CPP patch backend for {Original.FullDescription()}, using normal patch handlers: {e.Message}"); } } /// public override DynamicMethodDefinition PrepareOriginal() { return null; } /// public override MethodBase DetourTo(MethodBase replacement) { // Unpatch an existing detour if it exists nativeDetour?.Dispose(); // Generate a new DMD of the modified unhollowed method, and apply harmony patches to it var copiedDmd = CopyOriginal(); HarmonyManipulator.Manipulate(copiedDmd.OriginalMethod, copiedDmd.OriginalMethod.GetPatchInfo(), new ILContext(copiedDmd.Definition)); // Generate the MethodInfo instances var managedHookedMethod = copiedDmd.Generate(); var unmanagedTrampolineMethod = GenerateNativeToManagedTrampoline(managedHookedMethod).Generate(); // Apply a detour from the unmanaged implementation to the patched harmony method var unmanagedDelegateType = DelegateTypeFactory.instance.CreateDelegateType(unmanagedTrampolineMethod, CallingConvention.Cdecl); var detourPtr = Marshal.GetFunctionPointerForDelegate(unmanagedTrampolineMethod.CreateDelegate(unmanagedDelegateType)); nativeDetour = new FastNativeDetour(originalNativeMethodInfo->methodPointer, detourPtr); nativeDetour.Apply(); // TODO: Add an ILHook for the original unhollowed method to go directly to managedHookedMethod // Right now it goes through three times as much interop conversion as it needs to, when being called from managed side return managedHookedMethod; } /// public override DynamicMethodDefinition CopyOriginal() { var dmd = new DynamicMethodDefinition(Original); dmd.Definition.Name = "UnhollowedWrapper_" + dmd.Definition.Name; var cursor = new ILCursor(new ILContext(dmd.Definition)); // Remove il2cpp_object_get_virtual_method if (cursor.TryGotoNext(x => x.MatchLdarg(0), x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)), x => x.MatchLdsfld(out _), x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.il2cpp_object_get_virtual_method)))) { cursor.RemoveRange(4); } else { cursor.Goto(0) .GotoNext(x => x.MatchLdsfld(UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original))) .Remove(); } // Replace original IL2CPPMethodInfo pointer with a modified one that points to the trampoline cursor .Emit(Mono.Cecil.Cil.OpCodes.Ldc_I8, ((IntPtr)modifiedNativeMethodInfo).ToInt64()) .Emit(Mono.Cecil.Cil.OpCodes.Conv_I); return dmd; } /// /// A handler for that checks if a method doesn't have a body /// (e.g. it's icall or marked with ) and thus can be patched with /// . /// /// Not used /// Patch resolver arguments /// public static void TryResolve(object sender, PatchManager.PatcherResolverEventArgs args) { if (args.Original.DeclaringType?.IsSubclassOf(typeof(Il2CppObjectBase)) == true) { var backend = new IL2CPPDetourMethodPatcher(args.Original); if (backend.isValid) args.MethodPatcher = backend; } } private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo targetManagedMethodInfo) { // managedParams are the unhollower types used on the managed side // unmanagedParams are IntPtr references that are used by IL2CPP compiled assembly var paramStartIndex = Original.IsStatic ? 0 : 1; var managedParams = Original.GetParameters().Select(x => x.ParameterType).ToArray(); var unmanagedParams = new Type[managedParams.Length + paramStartIndex + 1]; // +1 for thisptr if needed, +1 for methodInfo at the end if (!Original.IsStatic) unmanagedParams[0] = typeof(IntPtr); unmanagedParams[unmanagedParams.Length - 1] = typeof(Il2CppMethodInfo*); Array.Copy(managedParams.Select(ConvertManagedTypeToIL2CPPType).ToArray(), 0, unmanagedParams, paramStartIndex, managedParams.Length); var managedReturnType = AccessTools.GetReturnedType(Original); var unmanagedReturnType = ConvertManagedTypeToIL2CPPType(managedReturnType); var dmd = new DynamicMethodDefinition("(il2cpp -> managed) " + Original.Name, unmanagedReturnType, unmanagedParams ); var il = dmd.GetILGenerator(); il.BeginExceptionBlock(); // Declare a list of variables to dereference back to the original pointers. // This is required due to the needed unhollower type conversions, so we can't directly pass some addresses as byref types LocalBuilder[] indirectVariables = new LocalBuilder[managedParams.Length]; if (!Original.IsStatic) { // Load thisptr as arg0 il.Emit(OpCodes.Ldarg_0); EmitConvertArgumentToManaged(il, Original.DeclaringType, out _); } for (int i = 0; i < managedParams.Length; ++i) { il.Emit(OpCodes.Ldarg_S, i + paramStartIndex); EmitConvertArgumentToManaged(il, managedParams[i], out indirectVariables[i]); } // Run the managed method il.Emit(OpCodes.Call, targetManagedMethodInfo); // Store the managed return type temporarily (if there was one) LocalBuilder managedReturnVariable = null; if (managedReturnType != typeof(void)) { managedReturnVariable = il.DeclareLocal(managedReturnType); il.Emit(OpCodes.Stloc, managedReturnVariable); } // Convert any managed byref values into their relevant IL2CPP types, and then store the values into their relevant dereferenced pointers for (int i = 0; i < managedParams.Length; ++i) { if (indirectVariables[i] == null) continue; il.Emit(OpCodes.Ldarg_S, i + paramStartIndex); il.Emit(OpCodes.Ldloc, indirectVariables[i]); EmitConvertManagedTypeToIL2CPP(il, managedParams[i].GetElementType()); il.Emit(OpCodes.Stind_I); } // Handle any lingering exceptions il.BeginCatchBlock(typeof(Exception)); il.Emit(OpCodes.Call, ReportExceptionMethodInfo); il.EndExceptionBlock(); // Convert the return value back to an IL2CPP friendly type (if there was a return value), and then return if (managedReturnVariable != null) { il.Emit(OpCodes.Ldloc, managedReturnVariable); EmitConvertManagedTypeToIL2CPP(il, managedReturnType); } il.Emit(OpCodes.Ret); return dmd; } private static void ReportException(Exception ex) { DetourLogger.LogError(ex.ToString()); } private static Type ConvertManagedTypeToIL2CPPType(Type managedType) { if (managedType.IsByRef) { Type directType = managedType.GetElementType(); if (directType == typeof(string) || directType.IsSubclassOf(typeof(Il2CppObjectBase))) { return typeof(IntPtr*); } } else if (managedType == typeof(string) || managedType.IsSubclassOf(typeof(Il2CppObjectBase))) { return typeof(IntPtr); } return managedType; } private static void EmitConvertManagedTypeToIL2CPP(ILGenerator il, Type returnType) { if (returnType == typeof(string)) { il.Emit(OpCodes.Call, ManagedToIL2CPPStringMethodInfo); } else if (!returnType.IsValueType && returnType.IsSubclassOf(typeof(Il2CppObjectBase))) { il.Emit(OpCodes.Call, ObjectBaseToPtrMethodInfo); } } private static void EmitConvertArgumentToManaged(ILGenerator il, Type managedParamType, out LocalBuilder variable) { variable = null; if (managedParamType.IsValueType) // don't need to convert blittable types return; void EmitCreateIl2CppObject() { Label endLabel = il.DefineLabel(); Label notNullLabel = il.DefineLabel(); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Brtrue_S, notNullLabel); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Br_S, endLabel); il.MarkLabel(notNullLabel); il.Emit(OpCodes.Newobj, AccessTools.DeclaredConstructor(managedParamType, new[] { typeof(IntPtr) })); il.MarkLabel(endLabel); } void HandleTypeConversion(Type originalType) { if (originalType == typeof(string)) { il.Emit(OpCodes.Call, IL2CPPToManagedStringMethodInfo); } else if (originalType.IsSubclassOf(typeof(Il2CppObjectBase))) { EmitCreateIl2CppObject(); } } if (managedParamType.IsByRef) { Type directType = managedParamType.GetElementType(); variable = il.DeclareLocal(directType); il.Emit(OpCodes.Ldind_I); HandleTypeConversion(directType); il.Emit(OpCodes.Stloc, variable); il.Emit(OpCodes.Ldloca, variable); } else { HandleTypeConversion(managedParamType); } } } }