Skip to content

Commit c76b09c

Browse files
committed
IL2CPPDetourMethodPatcher: Improve struct handling on x64
* Add correct return buffer when return value is a struct * Use IntPtr for all parameters on x64 instead of a FixedStruct
1 parent b7b8dd2 commit c76b09c

File tree

1 file changed

+48
-18
lines changed

1 file changed

+48
-18
lines changed

BepInEx.IL2CPP/Hook/IL2CPPDetourMethodPatcher.cs

+48-18
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,15 @@
44
using System.Reflection;
55
using System.Reflection.Emit;
66
using System.Runtime.InteropServices;
7-
using System.Text;
87
using BepInEx.Logging;
98
using HarmonyLib;
109
using HarmonyLib.Public.Patching;
11-
using Mono.Cecil.Cil;
1210
using MonoMod.Cil;
1311
using MonoMod.RuntimeDetour;
1412
using MonoMod.Utils;
1513
using UnhollowerBaseLib;
1614
using UnhollowerBaseLib.Runtime;
1715
using UnhollowerBaseLib.Runtime.VersionSpecific.MethodInfo;
18-
using OpCode = System.Reflection.Emit.OpCode;
19-
using OpCodes = System.Reflection.Emit.OpCodes;
2016
using ValueType = Il2CppSystem.ValueType;
2117
using Void = Il2CppSystem.Void;
2218

@@ -39,7 +35,6 @@ private static readonly MethodInfo ObjectBaseToPtrMethodInfo
3935
private static readonly MethodInfo ReportExceptionMethodInfo
4036
= AccessTools.Method(typeof(IL2CPPDetourMethodPatcher), nameof(ReportException));
4137

42-
4338
private static readonly ManualLogSource DetourLogger = Logger.CreateLogSource("Detour");
4439

4540
// Map each value type to correctly sized store opcode to prevent memory overwrite
@@ -100,7 +95,7 @@ private void Init()
10095

10196
// Get the native MethodInfo struct for the target method
10297
originalNativeMethodInfo =
103-
UnityVersionHandler.Wrap((Il2CppMethodInfo*) (IntPtr) methodField.GetValue(null));
98+
UnityVersionHandler.Wrap((Il2CppMethodInfo*)(IntPtr)methodField.GetValue(null));
10499

105100
// Create a trampoline from the original target method
106101
var trampolinePtr =
@@ -206,19 +201,35 @@ private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo tar
206201
{
207202
// managedParams are the unhollower types used on the managed side
208203
// unmanagedParams are IntPtr references that are used by IL2CPP compiled assembly
209-
var paramStartIndex = Original.IsStatic ? 0 : 1;
204+
var paramStartIndex = 0;
205+
206+
var managedReturnType = AccessTools.GetReturnedType(Original);
207+
var hasReturnBuffer = managedReturnType.IsSubclassOf(typeof(ValueType)) &&
208+
PlatformHelper.Is(Platform.Bits64);
209+
if (hasReturnBuffer)
210+
// C compilers seem to return values larger than 64 bits by allocating a return buffer on caller's side and passing it as the first parameter
211+
// TODO: Handle ARM
212+
// TODO: Check if this applies to values other than structs
213+
// TODO: Check if we can use the dummy struct generated by GetFixedSizeStructType() so that mono's marshaller can handle this
214+
paramStartIndex++;
215+
216+
if (!Original.IsStatic)
217+
paramStartIndex++;
218+
210219
var managedParams = Original.GetParameters().Select(x => x.ParameterType).ToArray();
211220
var unmanagedParams =
212221
new Type[managedParams.Length + paramStartIndex +
213-
1]; // +1 for thisptr if needed, +1 for methodInfo at the end
222+
1]; // +1 for methodInfo at the end
214223

215-
if (!Original.IsStatic)
224+
if (hasReturnBuffer)
225+
// With GCC the return buffer seems to be the first param, same is likely with other compilers too
216226
unmanagedParams[0] = typeof(IntPtr);
227+
if (!Original.IsStatic)
228+
unmanagedParams[paramStartIndex - 1] = typeof(IntPtr);
217229
unmanagedParams[^1] = typeof(Il2CppMethodInfo*);
218230
Array.Copy(managedParams.Select(ConvertManagedTypeToIL2CPPType).ToArray(), 0,
219231
unmanagedParams, paramStartIndex, managedParams.Length);
220232

221-
var managedReturnType = AccessTools.GetReturnedType(Original);
222233
var unmanagedReturnType = ConvertManagedTypeToIL2CPPType(managedReturnType);
223234

224235
var dmd = new DynamicMethodDefinition("(il2cpp -> managed) " + Original.Name,
@@ -234,7 +245,7 @@ private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo tar
234245
var indirectVariables = new LocalBuilder[managedParams.Length];
235246

236247
if (!Original.IsStatic)
237-
EmitConvertArgumentToManaged(il, 0, Original.DeclaringType, out _);
248+
EmitConvertArgumentToManaged(il, paramStartIndex - 1, Original.DeclaringType, out _);
238249
for (var i = 0; i < managedParams.Length; ++i)
239250
EmitConvertArgumentToManaged(il, i + paramStartIndex, managedParams[i], out indirectVariables[i]);
240251

@@ -270,8 +281,26 @@ private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo tar
270281
// Convert the return value back to an IL2CPP friendly type (if there was a return value), and then return
271282
if (managedReturnVariable != null)
272283
{
273-
il.Emit(OpCodes.Ldloc, managedReturnVariable);
274-
EmitConvertManagedTypeToIL2CPP(il, managedReturnType);
284+
if (hasReturnBuffer)
285+
{
286+
uint align = 0;
287+
var size = UnhollowerBaseLib.IL2CPP.il2cpp_class_value_size(Il2CppTypeToClassPointer(managedReturnType),
288+
ref align);
289+
290+
il.Emit(OpCodes.Ldarg_0);
291+
il.Emit(OpCodes.Ldloc, managedReturnVariable);
292+
il.Emit(OpCodes.Call, ObjectBaseToPtrMethodInfo);
293+
il.Emit(OpCodes.Ldc_I4, size);
294+
il.Emit(OpCodes.Cpblk);
295+
296+
// Return the same pointer to the return buffer
297+
il.Emit(OpCodes.Ldarg_0);
298+
}
299+
else
300+
{
301+
il.Emit(OpCodes.Ldloc, managedReturnVariable);
302+
EmitConvertManagedTypeToIL2CPP(il, managedReturnType);
303+
}
275304
}
276305

277306
il.Emit(OpCodes.Ret);
@@ -322,8 +351,8 @@ private static IntPtr Il2CppTypeToClassPointer(Type type)
322351
{
323352
if (type == typeof(void))
324353
return Il2CppClassPointerStore<Void>.NativeClassPtr;
325-
return (IntPtr) typeof(Il2CppClassPointerStore<>).MakeGenericType(type).GetField("NativeClassPtr")
326-
.GetValue(null);
354+
return (IntPtr)typeof(Il2CppClassPointerStore<>).MakeGenericType(type).GetField("NativeClassPtr")
355+
.GetValue(null);
327356
}
328357

329358
private static void EmitConvertArgumentToManaged(ILGenerator il,
@@ -333,13 +362,14 @@ private static void EmitConvertArgumentToManaged(ILGenerator il,
333362
{
334363
variable = null;
335364

336-
if (managedParamType.IsSubclassOf(typeof(ValueType)) && !PlatformHelper.Is(Platform.Bits64))
365+
if (managedParamType.IsSubclassOf(typeof(ValueType)))
337366
{
338367
// Box struct into object first before conversion
339-
// This will likely incur struct copying down the line, but it shouldn't be a massive loss
340368
il.Emit(OpCodes.Ldc_I8, Il2CppTypeToClassPointer(managedParamType).ToInt64());
341369
il.Emit(OpCodes.Conv_I);
342-
il.Emit(OpCodes.Ldarga_S, argIndex);
370+
// On x64, struct is always a pointer but it is a non-pointer on x86
371+
// We don't handle byref structs on x86 yet but we're yet to encounter those
372+
il.Emit(PlatformHelper.Is(Platform.Bits64) ? OpCodes.Ldarg : OpCodes.Ldarga_S, argIndex);
343373
il.Emit(OpCodes.Call,
344374
AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP),
345375
nameof(UnhollowerBaseLib.IL2CPP.il2cpp_value_box)));

0 commit comments

Comments
 (0)