IL2CPPDetourMethodPatcher.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. using System;
  2. using System.Linq;
  3. using System.Reflection;
  4. using System.Reflection.Emit;
  5. using System.Runtime.InteropServices;
  6. using BepInEx.Logging;
  7. using HarmonyLib;
  8. using HarmonyLib.Public.Patching;
  9. using MonoMod.Cil;
  10. using MonoMod.RuntimeDetour;
  11. using MonoMod.Utils;
  12. using UnhollowerBaseLib;
  13. using UnhollowerBaseLib.Runtime;
  14. using Logger = BepInEx.Logging.Logger;
  15. namespace BepInEx.IL2CPP.Hook
  16. {
  17. public unsafe class IL2CPPDetourMethodPatcher : MethodPatcher
  18. {
  19. private static readonly MethodInfo IL2CPPToManagedStringMethodInfo
  20. = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppStringToManaged));
  21. private static readonly MethodInfo ManagedToIL2CPPStringMethodInfo
  22. = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp));
  23. private static readonly MethodInfo ObjectBaseToPtrMethodInfo
  24. = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr));
  25. private static readonly MethodInfo ReportExceptionMethodInfo
  26. = AccessTools.Method(typeof(IL2CPPDetourMethodPatcher), nameof(ReportException));
  27. private static readonly ManualLogSource DetourLogger = Logger.CreateLogSource("Detour");
  28. private FastNativeDetour nativeDetour;
  29. private Il2CppMethodInfo* originalNativeMethodInfo;
  30. private Il2CppMethodInfo* modifiedNativeMethodInfo;
  31. private bool isValid;
  32. /// <summary>
  33. /// Constructs a new instance of <see cref="NativeDetour"/> method patcher.
  34. /// </summary>
  35. /// <param name="original"></param>
  36. public IL2CPPDetourMethodPatcher(MethodBase original) : base(original)
  37. {
  38. Init();
  39. }
  40. private void Init()
  41. {
  42. try
  43. {
  44. // Get the native MethodInfo struct for the target method
  45. originalNativeMethodInfo = (Il2CppMethodInfo*)(IntPtr)UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original).GetValue(null);
  46. // Create a trampoline from the original target method
  47. var trampolinePtr = DetourGenerator.CreateTrampolineFromFunction(originalNativeMethodInfo->methodPointer, out _, out _);
  48. // Create a modified native MethodInfo struct to point towards the trampoline
  49. modifiedNativeMethodInfo = (Il2CppMethodInfo*)Marshal.AllocHGlobal(Marshal.SizeOf<Il2CppMethodInfo>());
  50. Marshal.StructureToPtr(*originalNativeMethodInfo, (IntPtr)modifiedNativeMethodInfo, false);
  51. modifiedNativeMethodInfo->methodPointer = trampolinePtr;
  52. isValid = true;
  53. }
  54. catch (Exception e)
  55. {
  56. DetourLogger.LogWarning($"Failed to init IL2CPP patch backend for {Original.FullDescription()}, using normal patch handlers: {e.Message}");
  57. }
  58. }
  59. /// <inheritdoc />
  60. public override DynamicMethodDefinition PrepareOriginal()
  61. {
  62. return null;
  63. }
  64. /// <inheritdoc />
  65. public override MethodBase DetourTo(MethodBase replacement)
  66. {
  67. // Unpatch an existing detour if it exists
  68. nativeDetour?.Dispose();
  69. // Generate a new DMD of the modified unhollowed method, and apply harmony patches to it
  70. var copiedDmd = CopyOriginal();
  71. HarmonyManipulator.Manipulate(copiedDmd.OriginalMethod, copiedDmd.OriginalMethod.GetPatchInfo(), new ILContext(copiedDmd.Definition));
  72. // Generate the MethodInfo instances
  73. var managedHookedMethod = copiedDmd.Generate();
  74. var unmanagedTrampolineMethod = GenerateNativeToManagedTrampoline(managedHookedMethod).Generate();
  75. // Apply a detour from the unmanaged implementation to the patched harmony method
  76. var unmanagedDelegateType = DelegateTypeFactory.instance.CreateDelegateType(unmanagedTrampolineMethod,
  77. CallingConvention.Cdecl);
  78. var detourPtr = Marshal.GetFunctionPointerForDelegate(unmanagedTrampolineMethod.CreateDelegate(unmanagedDelegateType));
  79. nativeDetour = new FastNativeDetour(originalNativeMethodInfo->methodPointer, detourPtr);
  80. nativeDetour.Apply();
  81. // TODO: Add an ILHook for the original unhollowed method to go directly to managedHookedMethod
  82. // Right now it goes through three times as much interop conversion as it needs to, when being called from managed side
  83. return managedHookedMethod;
  84. }
  85. /// <inheritdoc />
  86. public override DynamicMethodDefinition CopyOriginal()
  87. {
  88. var dmd = new DynamicMethodDefinition(Original);
  89. dmd.Definition.Name = "UnhollowedWrapper_" + dmd.Definition.Name;
  90. var cursor = new ILCursor(new ILContext(dmd.Definition));
  91. // Remove il2cpp_object_get_virtual_method
  92. if (cursor.TryGotoNext(x => x.MatchLdarg(0),
  93. x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)),
  94. x => x.MatchLdsfld(out _),
  95. x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), nameof(UnhollowerBaseLib.IL2CPP.il2cpp_object_get_virtual_method))))
  96. {
  97. cursor.RemoveRange(4);
  98. }
  99. else
  100. {
  101. cursor.Goto(0)
  102. .GotoNext(x => x.MatchLdsfld(UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original)))
  103. .Remove();
  104. }
  105. // Replace original IL2CPPMethodInfo pointer with a modified one that points to the trampoline
  106. cursor
  107. .Emit(Mono.Cecil.Cil.OpCodes.Ldc_I8, ((IntPtr)modifiedNativeMethodInfo).ToInt64())
  108. .Emit(Mono.Cecil.Cil.OpCodes.Conv_I);
  109. return dmd;
  110. }
  111. /// <summary>
  112. /// A handler for <see cref="PatchManager.ResolvePatcher"/> that checks if a method doesn't have a body
  113. /// (e.g. it's icall or marked with <see cref="DynDllImportAttribute"/>) and thus can be patched with
  114. /// <see cref="NativeDetour"/>.
  115. /// </summary>
  116. /// <param name="sender">Not used</param>
  117. /// <param name="args">Patch resolver arguments</param>
  118. ///
  119. public static void TryResolve(object sender, PatchManager.PatcherResolverEventArgs args)
  120. {
  121. if (args.Original.DeclaringType?.IsSubclassOf(typeof(Il2CppObjectBase)) == true)
  122. {
  123. var backend = new IL2CPPDetourMethodPatcher(args.Original);
  124. if (backend.isValid)
  125. args.MethodPatcher = backend;
  126. }
  127. }
  128. private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo targetManagedMethodInfo)
  129. {
  130. // managedParams are the unhollower types used on the managed side
  131. // unmanagedParams are IntPtr references that are used by IL2CPP compiled assembly
  132. var paramStartIndex = Original.IsStatic ? 0 : 1;
  133. var managedParams = Original.GetParameters().Select(x => x.ParameterType).ToArray();
  134. var unmanagedParams = new Type[managedParams.Length + paramStartIndex + 1]; // +1 for thisptr if needed, +1 for methodInfo at the end
  135. if (!Original.IsStatic)
  136. unmanagedParams[0] = typeof(IntPtr);
  137. unmanagedParams[unmanagedParams.Length - 1] = typeof(Il2CppMethodInfo*);
  138. Array.Copy(managedParams.Select(ConvertManagedTypeToIL2CPPType).ToArray(), 0,
  139. unmanagedParams, paramStartIndex, managedParams.Length);
  140. var managedReturnType = AccessTools.GetReturnedType(Original);
  141. var unmanagedReturnType = ConvertManagedTypeToIL2CPPType(managedReturnType);
  142. var dmd = new DynamicMethodDefinition("(il2cpp -> managed) " + Original.Name,
  143. unmanagedReturnType,
  144. unmanagedParams
  145. );
  146. var il = dmd.GetILGenerator();
  147. il.BeginExceptionBlock();
  148. // Declare a list of variables to dereference back to the original pointers.
  149. // This is required due to the needed unhollower type conversions, so we can't directly pass some addresses as byref types
  150. LocalBuilder[] indirectVariables = new LocalBuilder[managedParams.Length];
  151. if (!Original.IsStatic)
  152. {
  153. // Load thisptr as arg0
  154. il.Emit(OpCodes.Ldarg_0);
  155. EmitConvertArgumentToManaged(il, Original.DeclaringType, out _);
  156. }
  157. for (int i = 0; i < managedParams.Length; ++i)
  158. {
  159. il.Emit(OpCodes.Ldarg_S, i + paramStartIndex);
  160. EmitConvertArgumentToManaged(il, managedParams[i], out indirectVariables[i]);
  161. }
  162. // Run the managed method
  163. il.Emit(OpCodes.Call, targetManagedMethodInfo);
  164. // Store the managed return type temporarily (if there was one)
  165. LocalBuilder managedReturnVariable = null;
  166. if (managedReturnType != typeof(void))
  167. {
  168. managedReturnVariable = il.DeclareLocal(managedReturnType);
  169. il.Emit(OpCodes.Stloc, managedReturnVariable);
  170. }
  171. // Convert any managed byref values into their relevant IL2CPP types, and then store the values into their relevant dereferenced pointers
  172. for (int i = 0; i < managedParams.Length; ++i)
  173. {
  174. if (indirectVariables[i] == null)
  175. continue;
  176. il.Emit(OpCodes.Ldarg_S, i + paramStartIndex);
  177. il.Emit(OpCodes.Ldloc, indirectVariables[i]);
  178. EmitConvertManagedTypeToIL2CPP(il, managedParams[i].GetElementType());
  179. il.Emit(OpCodes.Stind_I);
  180. }
  181. // Handle any lingering exceptions
  182. il.BeginCatchBlock(typeof(Exception));
  183. il.Emit(OpCodes.Call, ReportExceptionMethodInfo);
  184. il.EndExceptionBlock();
  185. // Convert the return value back to an IL2CPP friendly type (if there was a return value), and then return
  186. if (managedReturnVariable != null)
  187. {
  188. il.Emit(OpCodes.Ldloc, managedReturnVariable);
  189. EmitConvertManagedTypeToIL2CPP(il, managedReturnType);
  190. }
  191. il.Emit(OpCodes.Ret);
  192. return dmd;
  193. }
  194. private static void ReportException(Exception ex)
  195. {
  196. DetourLogger.LogError(ex.ToString());
  197. }
  198. private static Type ConvertManagedTypeToIL2CPPType(Type managedType)
  199. {
  200. if (managedType.IsByRef)
  201. {
  202. Type directType = managedType.GetElementType();
  203. if (directType == typeof(string) || directType.IsSubclassOf(typeof(Il2CppObjectBase)))
  204. {
  205. return typeof(IntPtr*);
  206. }
  207. }
  208. else if (managedType == typeof(string) || managedType.IsSubclassOf(typeof(Il2CppObjectBase)))
  209. {
  210. return typeof(IntPtr);
  211. }
  212. return managedType;
  213. }
  214. private static void EmitConvertManagedTypeToIL2CPP(ILGenerator il, Type returnType)
  215. {
  216. if (returnType == typeof(string))
  217. {
  218. il.Emit(OpCodes.Call, ManagedToIL2CPPStringMethodInfo);
  219. }
  220. else if (!returnType.IsValueType && returnType.IsSubclassOf(typeof(Il2CppObjectBase)))
  221. {
  222. il.Emit(OpCodes.Call, ObjectBaseToPtrMethodInfo);
  223. }
  224. }
  225. private static void EmitConvertArgumentToManaged(ILGenerator il, Type managedParamType, out LocalBuilder variable)
  226. {
  227. variable = null;
  228. if (managedParamType.IsValueType) // don't need to convert blittable types
  229. return;
  230. void EmitCreateIl2CppObject()
  231. {
  232. Label endLabel = il.DefineLabel();
  233. Label notNullLabel = il.DefineLabel();
  234. il.Emit(OpCodes.Dup);
  235. il.Emit(OpCodes.Brtrue_S, notNullLabel);
  236. il.Emit(OpCodes.Pop);
  237. il.Emit(OpCodes.Ldnull);
  238. il.Emit(OpCodes.Br_S, endLabel);
  239. il.MarkLabel(notNullLabel);
  240. il.Emit(OpCodes.Newobj, AccessTools.DeclaredConstructor(managedParamType, new[] { typeof(IntPtr) }));
  241. il.MarkLabel(endLabel);
  242. }
  243. void HandleTypeConversion(Type originalType)
  244. {
  245. if (originalType == typeof(string))
  246. {
  247. il.Emit(OpCodes.Call, IL2CPPToManagedStringMethodInfo);
  248. }
  249. else if (originalType.IsSubclassOf(typeof(Il2CppObjectBase)))
  250. {
  251. EmitCreateIl2CppObject();
  252. }
  253. }
  254. if (managedParamType.IsByRef)
  255. {
  256. Type directType = managedParamType.GetElementType();
  257. variable = il.DeclareLocal(directType);
  258. il.Emit(OpCodes.Ldind_I);
  259. HandleTypeConversion(directType);
  260. il.Emit(OpCodes.Stloc, variable);
  261. il.Emit(OpCodes.Ldloca, variable);
  262. }
  263. else
  264. {
  265. HandleTypeConversion(managedParamType);
  266. }
  267. }
  268. }
  269. }