From 5d99dcda281f89f59f1b82734346c00379d49c00 Mon Sep 17 00:00:00 2001 From: Andreas Pardeike Date: Thu, 30 Dec 2021 14:10:00 +0100 Subject: [PATCH] fixes #25 by implementing __args; updates docs; bump to v2.2 --- .../articles/patching-auxilary.md | 6 +- .../articles/patching-injections.md | 49 +++-- .../examples/annotations_basic.cs | 2 +- .../examples/annotations_multiple.cs | 8 +- Harmony/Documentation/examples/basics.cs | 25 +++ .../examples/intro_annotations.cs | 2 +- .../Documentation/examples/intro_manual.cs | 2 +- .../examples/patching-auxilary.cs | 2 + .../examples/patching-postfix.cs | 8 +- .../Documentation/examples/patching-prefix.cs | 8 +- .../examples/patching-transpiler.cs | 8 +- Harmony/Harmony.csproj | 6 +- Harmony/Internal/MethodPatcher.cs | 179 +++++++++++++++++- Harmony/Tools/Extensions.cs | 2 +- Harmony/Tools/FileLog.cs | 2 +- HarmonyTests/IL/DynamicArgumentPatches.cs | 2 +- HarmonyTests/Patching/Arguments.cs | 78 ++++++++ HarmonyTests/Patching/Assets/ArgumentCases.cs | 69 +++++++ HarmonyTests/Patching/Assets/PatchClasses.cs | 2 +- 19 files changed, 406 insertions(+), 54 deletions(-) diff --git a/Harmony/Documentation/articles/patching-auxilary.md b/Harmony/Documentation/articles/patching-auxilary.md index 68cdf160..021f8951 100644 --- a/Harmony/Documentation/articles/patching-auxilary.md +++ b/Harmony/Documentation/articles/patching-auxilary.md @@ -10,6 +10,10 @@ Each of those methods can take up to three optional arguments that are injected - `Harmony harmony` - the current Harmony instance - `Exception ex` - only valid in `Cleanup` and receives a possible exception +Here is a simple example that patches a method inside a private type: + +[!code-csharp[example](../examples/basics.cs?name=target_method)] + ### Prepare Before the patching, Harmony gives you a chance to prepare your state. For this, Harmony searches for a method called @@ -72,4 +76,4 @@ static Exception MyCleanup(MethodBase original, ...) Similar to `Prepare()` this method is called with `original` set to the method that just has been patched and then finally one more time before ending the overall patching (original will be `null`). -Additionally, you can intercept exceptions that are thrown while patching. Use the injection of `exception` to learn what happened and check if you can cast it to `HarmonyException` to get more information. Finally, you can return `Exception` to replace the exception or `null` to suppress it. \ No newline at end of file +Additionally, you can intercept exceptions that are thrown while patching. Use the injection of `exception` to learn what happened and check if you can cast it to `HarmonyException` to get more information. Finally, you can return `Exception` to replace the exception or `null` to suppress it. diff --git a/Harmony/Documentation/articles/patching-injections.md b/Harmony/Documentation/articles/patching-injections.md index b460a3b6..db860873 100644 --- a/Harmony/Documentation/articles/patching-injections.md +++ b/Harmony/Documentation/articles/patching-injections.md @@ -2,48 +2,55 @@ ## Common injected values -Each patch method (except a transpiler) can get all the arguments of the original method as well as the instance if the original method is not static and the return value. Patches only need to define the parameters they want to access. +Each patch method (except a transpiler) can get all the arguments of the original method as well as the instance if the original method is not static and the return value. -### Instance +You only need to define the parameters you want to access. -Patches can use an argument named `__instance` to access the instance value if original method is not static. This is similar to the C# keyword `this` when used in the original method. +### __instance -### Result +Patches can use an argument called **`__instance`** to access the instance value if original method is not static. This is similar to the C# keyword `this` when used in the original method. -Patches can use an argument named `__result` to access the returned value. The type `T` of argument must match the return type of the original or be assignable from it. For prefixes, as the original method hasn't run yet, the value of `__result` is default(T). For most reference types, that would be `null`. If you wish to alter the `__result`, you need to define it by reference like `ref string name`. +### __result -### State +Patches can use an argument called **`__result`** to access the returned value. The type must match the return type of the original or be assignable from it. For prefixes, as the original method hasn't run yet, the value of `__result` is the default for that type. For most reference types, that would be `null`. If you wish to **alter** the `__result`, you need to define it **by reference** like `ref string name`. -Patches can use an argument named `__state` to store information in the prefix method that can be accessed again in the postfix method. Think of it as a local variable. It can be any type and you are responsible to initialize its value in the prefix. It only works if both patches are defined in the same class. +### __state -### (Private) Fields +Patches can use an argument called **`__state`** to store information in the prefix method that can be accessed again in the postfix method. Think of it as a local variable. It can be any type and you are responsible to initialize its value in the prefix. **Note:** It only works if both patches are defined in the same class. -Argument names starting with three underscores, for example `___someField`, can be used to read and (with `ref`) write private fields on the instance that has the corresponding name (minus the underscores). +### ___fields -### Original Method Argument Matching +Argument names starting with **three** underscores like **`___someField`** can be used to read/write private fields that have that name minus the underscores. To write to field you need to use the **`ref`** keyword like `ref string ___name`. -In order for the original method arguments to be properly matched to the patched method, some restrictions are placed on the types and names of arguments in the patched method: +### __args -#### - Argument Types +To access all arguments at once, you can let Harmony inject **`object[] __args`** that will contain all arguments in the order they appear. Editing the contents of that array (no ref needed) will automatically update the values of the corresponding arguments. -The type of a given argument (that is to be matched to the argument of the original method) must either be the same type or be `object`. +**Note:** This way of manipulation comes with some small overhead so if possible use normal argument injection -#### - Argument Names +### method arguments -The name of a given argument (that is to be matched to the argument of the original method) must either be the same name or of the form `__n`, where `n` is the zero-based index of the argument in the orignal method (you can use argument annotations to map to custom names). +To access or change one or several of the original methods arguments, simply repeat them with the same name in your patch. Some restrictions are placed on the types and names of arguments in the patched method: -### - The original +- The type of an injected argument must be assignable from the original argument (or just use `object`) +- The name of a given argument (that is to be matched to the argument of the original method) must either be the same name or of the form **`__n`**, where `n` is the zero-based index of the argument in the orignal method (you can also use argument annotations to map to custom names). -To allow patches to identify on which method they are attachted you can inject the original methods MethodBase by using an argument named `__originalMethod`. +### __originalMethod + +To allow patches to identify on which method they are attachted you can inject the original methods MethodBase by using an argument called **`__originalMethod`**. ![note] **You cannot call the original method with that**. The value is only for conditional code in your patch that can selectively run if the patch is applied to multiple methods. The original does not exist after patching and this will point to the patched version. -### - Special arguments +### __runOriginal + +To learn if the original is/was skipped you can inject **`bool __runOriginal`**. This is a readonly injection to understand if the original will be run (in a Prefix) or was run (in a Postfix). + +### Transpilers In transpilers, arguments are only matched by their type so you can choose any argument name you like. -An argument of type `IEnumerable` is required and will be used to pass the IL codes to the transpiler -An argument of type `ILGenerator` will be set to the current IL code generator -An argument of type `MethodBase` will be set to the current original method being patched +An argument of type **`IEnumerable`** is required and will be used to pass the IL codes to the transpiler +An argument of type **`ILGenerator`** will be set to the current IL code generator +An argument of type **`MethodBase`** will be set to the current original method being patched [note]: https://raw.githubusercontent.com/pardeike/Harmony/master/Harmony/Documentation/images/note.png diff --git a/Harmony/Documentation/examples/annotations_basic.cs b/Harmony/Documentation/examples/annotations_basic.cs index 7e1b37d9..4fa27247 100644 --- a/Harmony/Documentation/examples/annotations_basic.cs +++ b/Harmony/Documentation/examples/annotations_basic.cs @@ -4,7 +4,7 @@ namespace Annotations_Basics using HarmonyLib; [HarmonyPatch(typeof(SomeTypeHere))] - [HarmonyPatch("SomeMethodName")] + [HarmonyPatch("SomeMethodName")] // if possible use nameof() here class MyPatches { static void Postfix(/*...*/) diff --git a/Harmony/Documentation/examples/annotations_multiple.cs b/Harmony/Documentation/examples/annotations_multiple.cs index 08982ed6..4525a256 100644 --- a/Harmony/Documentation/examples/annotations_multiple.cs +++ b/Harmony/Documentation/examples/annotations_multiple.cs @@ -20,9 +20,13 @@ static IEnumerable TargetMethods() } // prefix all methods in someAssembly with a non-void return type and beginning with "Player" - static void Prefix(MethodBase __originalMethod) + static void Prefix(object[] __args, MethodBase __originalMethod) { - // use __originalMethod to decide what to do + // use dynamic code to handle all method calls + var parameters = __originalMethod.GetParameters(); + FileLog.Log($"Method {__originalMethod.FullDescription()}:"); + for (var i = 0; i < __args.Length; i++) + FileLog.Log($"{parameters[i].Name} of type {parameters[i].ParameterType} is {__args[i]}"); } } // diff --git a/Harmony/Documentation/examples/basics.cs b/Harmony/Documentation/examples/basics.cs index 0341b73b..41e0eb1f 100644 --- a/Harmony/Documentation/examples/basics.cs +++ b/Harmony/Documentation/examples/basics.cs @@ -38,6 +38,7 @@ void PatchManual() { // // add null checks to the following lines, they are omitted for clarity + // when possible, don't use string and instead use nameof(...) var original = typeof(TheClass).GetMethod("TheMethod"); var prefix = typeof(MyPatchClass1).GetMethod("SomeMethod"); var postfix = typeof(MyPatchClass2).GetMethod("SomeMethod"); @@ -141,6 +142,30 @@ void Unpatch() // } + // + [HarmonyPatch] // at least one Harmony annotation makes Harmony not skip this patch class when calling PatchAll() + class MyPatch + { + // here, inside the patch class, you can place the auxilary patch methods + // for example TargetMethod: + + public MethodBase TargetMethod() + { + // use normal reflection or helper methods in to find the method/constructor + // you want to patch and return its MethodInfo/ConstructorInfo + // + var type = AccessTools.FirstInner(typeof(TheClass), t => t.Name.Contains("Stuff")); + return AccessTools.FirstMethod(type, method => method.Name.Contains("SomeMethod")); + } + + // your patches + public void Prefix() + { + // ... + } + } + // + class TheClass { } class MyPatchClass1 { } class MyPatchClass2 { } diff --git a/Harmony/Documentation/examples/intro_annotations.cs b/Harmony/Documentation/examples/intro_annotations.cs index fea4aec8..c1ca43ad 100644 --- a/Harmony/Documentation/examples/intro_annotations.cs +++ b/Harmony/Documentation/examples/intro_annotations.cs @@ -21,7 +21,7 @@ public static void DoPatching() } [HarmonyPatch(typeof(SomeGameClass))] - [HarmonyPatch("DoSomething")] + [HarmonyPatch("DoSomething")] // if possible use nameof() here class Patch01 { static AccessTools.FieldRef isRunningRef = diff --git a/Harmony/Documentation/examples/intro_manual.cs b/Harmony/Documentation/examples/intro_manual.cs index 3f81bcf1..6b6cd821 100644 --- a/Harmony/Documentation/examples/intro_manual.cs +++ b/Harmony/Documentation/examples/intro_manual.cs @@ -17,7 +17,7 @@ public static void DoPatching() { var harmony = new Harmony("com.example.patch"); - var mOriginal = AccessTools.Method(typeof(SomeGameClass), "DoSomething"); + var mOriginal = AccessTools.Method(typeof(SomeGameClass), "DoSomething"); // if possible use nameof() here var mPrefix = SymbolExtensions.GetMethodInfo(() => MyPrefix()); var mPostfix = SymbolExtensions.GetMethodInfo(() => MyPostfix()); // in general, add null checks here (new HarmonyMethod() does it for you too) diff --git a/Harmony/Documentation/examples/patching-auxilary.cs b/Harmony/Documentation/examples/patching-auxilary.cs index 21361fed..82c3e634 100644 --- a/Harmony/Documentation/examples/patching-auxilary.cs +++ b/Harmony/Documentation/examples/patching-auxilary.cs @@ -12,8 +12,10 @@ class Example // static IEnumerable TargetMethods() { + // if possible use nameof() or SymbolExtensions.GetMethodInfo() here yield return AccessTools.Method(typeof(Foo), "Method1"); yield return AccessTools.Method(typeof(Bar), "Method2"); + // you could also iterate using reflections over many methods } // diff --git a/Harmony/Documentation/examples/patching-postfix.cs b/Harmony/Documentation/examples/patching-postfix.cs index 6eb111c9..d2faafdc 100644 --- a/Harmony/Documentation/examples/patching-postfix.cs +++ b/Harmony/Documentation/examples/patching-postfix.cs @@ -14,7 +14,7 @@ public string GetName() } } - [HarmonyPatch(typeof(OriginalCode), "GetName")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.GetName))] class Patch { static void Postfix(ref string __result) @@ -46,7 +46,7 @@ public IEnumerable GetNumbers() } } - [HarmonyPatch(typeof(OriginalCode), "GetName")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.GetName))] class Patch1 { static string Postfix(string name) @@ -55,7 +55,7 @@ static string Postfix(string name) } } - [HarmonyPatch(typeof(OriginalCode), "GetNumbers")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.GetNumbers))] class Patch2 { static IEnumerable Postfix(IEnumerable values) @@ -83,7 +83,7 @@ public void Test(int counter) } } - [HarmonyPatch(typeof(OriginalCode), "Test")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.Test))] class Patch { static void Prefix(int counter) diff --git a/Harmony/Documentation/examples/patching-prefix.cs b/Harmony/Documentation/examples/patching-prefix.cs index 888f8dd0..2c284c48 100644 --- a/Harmony/Documentation/examples/patching-prefix.cs +++ b/Harmony/Documentation/examples/patching-prefix.cs @@ -14,7 +14,7 @@ public void Test(int counter, string name) } } - [HarmonyPatch(typeof(OriginalCode), "Test")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.Test))] class Patch { static void Prefix(int counter, ref string name) @@ -37,7 +37,7 @@ public string GetName() } } - [HarmonyPatch(typeof(OriginalCode), "GetName")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.GetName))] class Patch { static bool Prefix(ref string __result) @@ -62,7 +62,7 @@ public bool IsFullAfterTakingIn(int i) } } - [HarmonyPatch(typeof(OriginalCode), "IsFullAfterTakingIn")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.IsFullAfterTakingIn))] class Patch { static bool Prefix(ref bool __result, int i) @@ -91,7 +91,7 @@ public void Test(int counter, string name) } } - [HarmonyPatch(typeof(OriginalCode), "Test")] + [HarmonyPatch(typeof(OriginalCode), nameof(OriginalCode.Test))] class Patch { // this example uses a Stopwatch type to measure diff --git a/Harmony/Documentation/examples/patching-transpiler.cs b/Harmony/Documentation/examples/patching-transpiler.cs index 80b69d93..cd25aabc 100644 --- a/Harmony/Documentation/examples/patching-transpiler.cs +++ b/Harmony/Documentation/examples/patching-transpiler.cs @@ -9,7 +9,7 @@ namespace Patching_Transpiler class TypicalExample { // - static FieldInfo f_someField = AccessTools.Field(typeof(SomeType), "someField"); + static FieldInfo f_someField = AccessTools.Field(typeof(SomeType), nameof(SomeType.someField)); static MethodInfo m_MyExtraMethod = SymbolExtensions.GetMethodInfo(() => Tools.MyExtraMethod()); // looks for STDFLD someField and inserts CALL MyExtraMethod before it @@ -30,7 +30,7 @@ static IEnumerable Transpiler(IEnumerable inst } // - class SomeType { } + class SomeType { public string someField; } class Tools { @@ -44,7 +44,7 @@ class CaravanExample { // [HarmonyPatch(typeof(Dialog_FormCaravan))] - [HarmonyPatch("CheckForErrors")] + [HarmonyPatch(nameof(Dialog_FormCaravan.CheckForErrors))] public static class Dialog_FormCaravan_CheckForErrors_Patch { static IEnumerable Transpiler(IEnumerable instructions) @@ -98,7 +98,7 @@ static IEnumerable Transpiler(IEnumerable inst } // - class Dialog_FormCaravan { } + class Dialog_FormCaravan { public void CheckForErrors() { } } class Log { diff --git a/Harmony/Harmony.csproj b/Harmony/Harmony.csproj index 1193b3af..3c0cd1e8 100644 --- a/Harmony/Harmony.csproj +++ b/Harmony/Harmony.csproj @@ -11,13 +11,13 @@ Andreas Pardeike 0Harmony true - 2.1.2.0 + 2.2.0.0 LICENSE https://github.com/pardeike/Harmony false Harmony,Mono,Patch,Patching,Runtime,Detour,Detours,Aspect,Aspects - 2.1.2.0 - 2.1.2.0 + 2.2.0.0 + 2.2.0.0 HarmonyLogo.png https://raw.githubusercontent.com/pardeike/Harmony/master/HarmonyLogo.png true diff --git a/Harmony/Internal/MethodPatcher.cs b/Harmony/Internal/MethodPatcher.cs index 362019fa..efaea6e2 100644 --- a/Harmony/Internal/MethodPatcher.cs +++ b/Harmony/Internal/MethodPatcher.cs @@ -14,10 +14,11 @@ internal class MethodPatcher const string INSTANCE_PARAM = "__instance"; const string ORIGINAL_METHOD_PARAM = "__originalMethod"; + const string ARGS_ARRAY_VAR = "__args"; const string RESULT_VAR = "__result"; const string STATE_VAR = "__state"; const string EXCEPTION_VAR = "__exception"; - const string RUN_ORIGINA_VAR = "__runOriginal"; + const string RUN_ORIGINAL_VAR = "__runOriginal"; const string PARAM_INDEX_PREFIX = "__"; const string INSTANCE_FIELD_PREFIX = "___"; @@ -72,6 +73,7 @@ internal MethodInfo CreateReplacement(out Dictionary final { var originalVariables = DeclareLocalVariables(il, source ?? original); var privateVars = new Dictionary(); + var fixes = prefixes.Union(postfixes).Union(finalizers).ToList(); LocalBuilder resultVariable = null; if (idx > 0) @@ -80,6 +82,15 @@ internal MethodInfo CreateReplacement(out Dictionary final privateVars[RESULT_VAR] = resultVariable; } + LocalBuilder argsArrayVariable = null; + if (fixes.Any(fix => fix.GetParameters().Any(p => p.Name == ARGS_ARRAY_VAR))) + { + PrepareArgumentArray(); + argsArrayVariable = il.DeclareLocal(typeof(object[])); + emitter.Emit(OpCodes.Stloc, argsArrayVariable); + privateVars[ARGS_ARRAY_VAR] = argsArrayVariable; + } + Label? skipOriginalLabel = null; LocalBuilder runOriginalVariable = null; if (prefixes.Any(fix => PrefixAffectsOriginal(fix))) @@ -91,7 +102,7 @@ internal MethodInfo CreateReplacement(out Dictionary final skipOriginalLabel = il.DefineLabel(); } - prefixes.Union(postfixes).Union(finalizers).ToList().ForEach(fix => + fixes.ForEach(fix => { if (fix.DeclaringType is object && privateVars.ContainsKey(fix.DeclaringType.AssemblyQualifiedName) is false) { @@ -288,7 +299,7 @@ internal static LocalBuilder[] DeclareLocalVariables(ILGenerator il, MethodBase LocalBuilder DeclareLocalVariable(Type type, bool isReturnValue = false) { - if (type.IsByRef && isReturnValue == false) type = type.GetElementType(); + if (type.IsByRef && isReturnValue is false) type = type.GetElementType(); if (type.IsEnum) type = Enum.GetUnderlyingType(type); if (AccessTools.IsClass(type)) @@ -343,6 +354,71 @@ static OpCode LoadIndOpCodeFor(Type type) return OpCodes.Ldind_Ref; } + static OpCode StoreIndOpCodeFor(Type type) + { + if (type.IsEnum) + return OpCodes.Stind_I4; + + if (type == typeof(float)) return OpCodes.Stind_R4; + if (type == typeof(double)) return OpCodes.Stind_R8; + + if (type == typeof(byte)) return OpCodes.Stind_I1; + if (type == typeof(ushort)) return OpCodes.Stind_I2; + if (type == typeof(uint)) return OpCodes.Stind_I4; + if (type == typeof(ulong)) return OpCodes.Stind_I8; + + if (type == typeof(sbyte)) return OpCodes.Stind_I1; + if (type == typeof(short)) return OpCodes.Stind_I2; + if (type == typeof(int)) return OpCodes.Stind_I4; + if (type == typeof(long)) return OpCodes.Stind_I8; + + return OpCodes.Stind_Ref; + } + + void InitializeOutParameter(int argIndex, Type type) + { + if (type.IsByRef) type = type.GetElementType(); + emitter.Emit(OpCodes.Ldarg, argIndex); + + if (AccessTools.IsStruct(type)) + { + emitter.Emit(OpCodes.Initobj, type); + return; + } + + if (AccessTools.IsValue(type)) + { + if (type == typeof(float)) + { + emitter.Emit(OpCodes.Ldc_R4, (float)0); + emitter.Emit(OpCodes.Stind_R4); + return; + } + else if (type == typeof(double)) + { + emitter.Emit(OpCodes.Ldc_R8, (double)0); + emitter.Emit(OpCodes.Stind_R8); + return; + } + else if (type == typeof(long)) + { + emitter.Emit(OpCodes.Ldc_I8, (long)0); + emitter.Emit(OpCodes.Stind_I8); + return; + } + else + { + emitter.Emit(OpCodes.Ldc_I4, 0); + emitter.Emit(OpCodes.Stind_I4); + return; + } + } + + // class or default + emitter.Emit(OpCodes.Ldnull); + emitter.Emit(OpCodes.Stind_Ref); + } + static readonly MethodInfo m_GetMethodFromHandle1 = typeof(MethodBase).GetMethod("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle) }); static readonly MethodInfo m_GetMethodFromHandle2 = typeof(MethodBase).GetMethod("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle), typeof(RuntimeTypeHandle) }); bool EmitOriginalBaseMethod() @@ -382,7 +458,7 @@ void EmitCallParameter(MethodInfo patch, Dictionary variab continue; } - if (patchParam.Name == RUN_ORIGINA_VAR) + if (patchParam.Name == RUN_ORIGINAL_VAR) { if (runOriginalVariable != null) emitter.Emit(OpCodes.Ldloc, runOriginalVariable); @@ -416,6 +492,15 @@ void EmitCallParameter(MethodInfo patch, Dictionary variab continue; } + if (patchParam.Name == ARGS_ARRAY_VAR) + { + if (variables.TryGetValue(ARGS_ARRAY_VAR, out var argsArrayVar)) + emitter.Emit(OpCodes.Ldloc, argsArrayVar); + else + emitter.Emit(OpCodes.Ldnull); + continue; + } + if (patchParam.Name.StartsWith(INSTANCE_FIELD_PREFIX, StringComparison.Ordinal)) { var fieldName = patchParam.Name.Substring(INSTANCE_FIELD_PREFIX.Length); @@ -462,11 +547,11 @@ void EmitCallParameter(MethodInfo patch, Dictionary variab if (returnType == typeof(void)) throw new Exception($"Cannot get result from void method {original.FullDescription()}"); var resultType = patchParam.ParameterType; - if (resultType.IsByRef && returnType.IsByRef == false) + if (resultType.IsByRef && returnType.IsByRef is false) resultType = resultType.GetElementType(); if (resultType.IsAssignableFrom(returnType) is false) throw new Exception($"Cannot assign method return type {returnType.FullName} to {RESULT_VAR} type {resultType.FullName} for method {original.FullDescription()}"); - var ldlocCode = patchParam.ParameterType.IsByRef && returnType.IsByRef == false ? OpCodes.Ldloca : OpCodes.Ldloc; + var ldlocCode = patchParam.ParameterType.IsByRef && returnType.IsByRef is false ? OpCodes.Ldloca : OpCodes.Ldloc; if (returnType.IsValueType && patchParam.ParameterType == typeof(object).MakeByRefType()) ldlocCode = OpCodes.Ldloc; emitter.Emit(ldlocCode, variables[RESULT_VAR]); if (returnType.IsValueType) @@ -557,7 +642,7 @@ void EmitCallParameter(MethodInfo patch, Dictionary variab var patchParamElementType = patchParamType.IsByRef ? patchParamType.GetElementType() : patchParamType; var originalIsNormal = originalParameters[idx].IsOut is false && originalParamType.IsByRef is false; var patchIsNormal = patchParam.IsOut is false && patchParamType.IsByRef is false; - var needsBoxing = originalParamElementType.IsValueType && patchParamElementType.IsValueType == false; + var needsBoxing = originalParamElementType.IsValueType && patchParamElementType.IsValueType is false; var patchArgIndex = idx + (isInstance ? 1 : 0) + (useStructReturnBuffer ? 1 : 0); // Case 1 + 4 @@ -628,7 +713,7 @@ static bool PrefixAffectsOriginal(MethodInfo fix) if (name == ORIGINAL_METHOD_PARAM) return false; if (name == STATE_VAR) return false; - if (p.IsOut) return true; + if (p.IsOut || p.IsRetval) return true; if (type.IsByRef) return true; if (AccessTools.IsValue(type) is false && AccessTools.IsStruct(type) is false) return true; @@ -654,6 +739,8 @@ void AddPrefixes(Dictionary variables, LocalBuilder runOri var tmpBoxVars = new List>(); EmitCallParameter(fix, variables, runOriginalVariable, false, out var tmpObjectVar, tmpBoxVars); emitter.Emit(OpCodes.Call, fix); + if (fix.GetParameters().Any(p => p.Name == ARGS_ARRAY_VAR)) + RestoreArgumentArray(variables); if (tmpObjectVar != null) { emitter.Emit(OpCodes.Ldloc, tmpObjectVar); @@ -697,6 +784,8 @@ bool AddPostfixes(Dictionary variables, bool passthroughPa var tmpBoxVars = new List>(); EmitCallParameter(fix, variables, null, true, out var tmpObjectVar, tmpBoxVars); emitter.Emit(OpCodes.Call, fix); + if (fix.GetParameters().Any(p => p.Name == ARGS_ARRAY_VAR)) + RestoreArgumentArray(variables); if (tmpObjectVar != null) { emitter.Emit(OpCodes.Ldloc, tmpObjectVar); @@ -744,6 +833,8 @@ bool AddFinalizers(Dictionary variables, bool catchExcepti var tmpBoxVars = new List>(); EmitCallParameter(fix, variables, null, false, out var tmpObjectVar, tmpBoxVars); emitter.Emit(OpCodes.Call, fix); + if (fix.GetParameters().Any(p => p.Name == ARGS_ARRAY_VAR)) + RestoreArgumentArray(variables); if (tmpObjectVar != null) { emitter.Emit(OpCodes.Ldloc, tmpObjectVar); @@ -774,5 +865,77 @@ bool AddFinalizers(Dictionary variables, bool catchExcepti return rethrowPossible; } + + void PrepareArgumentArray() + { + var parameters = original.GetParameters(); + var i = 0; + foreach (var pInfo in parameters) + { + var argIndex = i++ + (original.IsStatic ? 0 : 1); + if (pInfo.IsOut || pInfo.IsRetval) + InitializeOutParameter(argIndex, pInfo.ParameterType); + } + emitter.Emit(OpCodes.Ldc_I4, parameters.Length); + emitter.Emit(OpCodes.Newarr, typeof(object)); + i = 0; + var arrayIdx = 0; + foreach (var pInfo in parameters) + { + var argIndex = i++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + var paramByRef = pType.IsByRef; + if (paramByRef) pType = pType.GetElementType(); + emitter.Emit(OpCodes.Dup); + emitter.Emit(OpCodes.Ldc_I4, arrayIdx++); + emitter.Emit(OpCodes.Ldarg, argIndex); + if (paramByRef) + { + if (AccessTools.IsStruct(pType)) + emitter.Emit(OpCodes.Ldobj, pType); + else + emitter.Emit(LoadIndOpCodeFor(pType)); + } + if (pType.IsValueType) + emitter.Emit(OpCodes.Box, pType); + emitter.Emit(OpCodes.Stelem_Ref); + } + } + + void RestoreArgumentArray(Dictionary variables) + { + var parameters = original.GetParameters(); + var i = 0; + var arrayIdx = 0; + foreach (var pInfo in parameters) + { + var argIndex = i++ + (original.IsStatic ? 0 : 1); + var pType = pInfo.ParameterType; + if (pType.IsByRef) + { + pType = pType.GetElementType(); + + emitter.Emit(OpCodes.Ldarg, argIndex); + emitter.Emit(OpCodes.Ldloc, variables[ARGS_ARRAY_VAR]); + emitter.Emit(OpCodes.Ldc_I4, arrayIdx); + emitter.Emit(OpCodes.Ldelem_Ref); + + if (pType.IsValueType) + { + emitter.Emit(OpCodes.Unbox_Any, pType); + if (AccessTools.IsStruct(pType)) + emitter.Emit(OpCodes.Stobj, pType); + else + emitter.Emit(StoreIndOpCodeFor(pType)); + } + else + { + emitter.Emit(OpCodes.Castclass, pType); + emitter.Emit(OpCodes.Stind_Ref); + } + } + arrayIdx++; + } + } } } diff --git a/Harmony/Tools/Extensions.cs b/Harmony/Tools/Extensions.cs index 2865f588..4790d523 100644 --- a/Harmony/Tools/Extensions.cs +++ b/Harmony/Tools/Extensions.cs @@ -400,7 +400,7 @@ public static bool LoadsField(this CodeInstruction code, FieldInfo field, bool b var ldfldCode = field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld; if (byAddress is false && code.opcode == ldfldCode && Equals(code.operand, field)) return true; var ldfldaCode = field.IsStatic ? OpCodes.Ldsflda : OpCodes.Ldflda; - if (byAddress == true && code.opcode == ldfldaCode && Equals(code.operand, field)) return true; + if (byAddress is true && code.opcode == ldfldaCode && Equals(code.operand, field)) return true; return false; } diff --git a/Harmony/Tools/FileLog.cs b/Harmony/Tools/FileLog.cs index ea11343c..d84622ba 100644 --- a/Harmony/Tools/FileLog.cs +++ b/Harmony/Tools/FileLog.cs @@ -16,7 +16,7 @@ public static class FileLog static FileLog() { var customPath = Environment.GetEnvironmentVariable("HARMONY_LOG_FILE"); - if (string.IsNullOrEmpty(customPath) == false) + if (string.IsNullOrEmpty(customPath) is false) { logPath = customPath; return; diff --git a/HarmonyTests/IL/DynamicArgumentPatches.cs b/HarmonyTests/IL/DynamicArgumentPatches.cs index 9eb1b6aa..88a0b8ec 100644 --- a/HarmonyTests/IL/DynamicArgumentPatches.cs +++ b/HarmonyTests/IL/DynamicArgumentPatches.cs @@ -143,7 +143,7 @@ static IEnumerable Transpiler(MethodBase original, IEnumerable< yield return new CodeInstruction(OpCodes.Ldarg, argIndex); if (pInfo.IsOut || pInfo.IsRetval) { - if (pType.IsValueType) + if (pType.IsValueType && AccessTools.IsStruct(pType) is false) yield return new CodeInstruction(OpCodes.Ldobj, pType); else yield return new CodeInstruction(OpCodes.Ldind_Ref); diff --git a/HarmonyTests/Patching/Arguments.cs b/HarmonyTests/Patching/Arguments.cs index e8c145a8..efbfda5c 100644 --- a/HarmonyTests/Patching/Arguments.cs +++ b/HarmonyTests/Patching/Arguments.cs @@ -273,5 +273,83 @@ public void Test_ArgumentCases() Assert.AreEqual("OOOOVVVVVVVV", ArgumentPatchMethods.result); } + + [Test] + public void Test_ArrayArguments() + { + var harmony = new Harmony("test"); + var processor = new PatchClassProcessor(harmony, typeof(ArgumentArrayPatches)); + var patches = processor.Patch(); + Assert.NotNull(patches, "patches"); + Assert.AreEqual(1, patches.Count); + + ArgumentArrayPatches.prefixInput = null; + ArgumentArrayPatches.postfixInput = null; + + var instance = new ArgumentArrayMethods(); + var n1 = 8; + var n2 = 9; + var s1 = "A"; + var s2 = "B"; + var st1 = new ArgumentArrayMethods.SomeStruct() { n = 8 }; + var st2 = new ArgumentArrayMethods.SomeStruct() { n = 9 }; + var f1 = new float[] { 8f }; + var f2 = new float[] { 9f }; + + instance.Method( + n1, ref n2, out var n3, + s1, ref s2, out var s3, + st1, ref st2, out var st3, + f1, ref f2, out var f3 + ); + + // prefix input + var r = ArgumentArrayPatches.prefixInput; + var i = 0; + Assert.AreEqual(8, r[i], $"prefix[{i++}]"); + Assert.AreEqual(9, r[i], $"prefix[{i++}]"); + Assert.AreEqual(0, r[i], $"prefix[{i++}]"); + + Assert.AreEqual("A", r[i], $"prefix[{i++}]"); + Assert.AreEqual("B", r[i], $"prefix[{i++}]"); + Assert.AreEqual(null, r[i], $"prefix[{i++}]"); + + Assert.AreEqual(8, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"prefix[{i++}]"); + Assert.AreEqual(9, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"prefix[{i++}]"); + Assert.AreEqual(0, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"prefix[{i++}]"); + + Assert.AreEqual(8f, ((float[])r[i])[0], $"prefix[{i++}]"); + Assert.AreEqual(9f, ((float[])r[i])[0], $"prefix[{i++}]"); + Assert.AreEqual(null, (float[])r[i], $"prefix[{i++}]"); + + // postfix input + r = ArgumentArrayPatches.postfixInput; + i = 0; + Assert.AreEqual(8, r[i], $"postfix[{i++}]"); + Assert.AreEqual(123, r[i], $"postfix[{i++}]"); + Assert.AreEqual(456, r[i], $"postfix[{i++}]"); + + Assert.AreEqual("A", r[i], $"postfix[{i++}]"); + Assert.AreEqual("abc", r[i], $"postfix[{i++}]"); + Assert.AreEqual("def", r[i], $"postfix[{i++}]"); + + Assert.AreEqual(8, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"postfix[{i++}]"); + Assert.AreEqual(123, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"postfix[{i++}]"); + Assert.AreEqual(456, ((ArgumentArrayMethods.SomeStruct)r[i]).n, $"postfix[{i++}]"); + + Assert.AreEqual(8f, ((float[])r[i])[0], $"postfix[{i++}]"); + Assert.AreEqual(5.6f, ((float[])r[i])[2], $"postfix[{i++}]"); + Assert.AreEqual(6.5f, ((float[])r[i])[2], $"postfix[{i++}]"); + + // method output values + Assert.AreEqual(123, n2, "n2"); + Assert.AreEqual(456, n3, "n3"); + Assert.AreEqual("abc", s2, "s2"); + Assert.AreEqual("def", s3, "s3"); + Assert.AreEqual(123, st2.n, "st2"); + Assert.AreEqual(456, st3.n, "st3"); + Assert.AreEqual(5.6f, f2[2], "f2"); + Assert.AreEqual(6.5f, f3[2], "f3"); + } } } diff --git a/HarmonyTests/Patching/Assets/ArgumentCases.cs b/HarmonyTests/Patching/Assets/ArgumentCases.cs index f4fa8215..387603ec 100644 --- a/HarmonyTests/Patching/Assets/ArgumentCases.cs +++ b/HarmonyTests/Patching/Assets/ArgumentCases.cs @@ -126,4 +126,73 @@ public static void To_BoxingRef(ref object p) _ = Traverse.Create(p).Field("n").SetValue(102); } } + + public class ArgumentArrayMethods + { + public struct SomeStruct + { + public int n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public void Method( + int n1, ref int n2, out int n3, + string s1, ref string s2, out string s3, + SomeStruct st1, ref SomeStruct st2, out SomeStruct st3, + float[] f1, ref float[] f2, out float[] f3 + ) + { + n2 = 12; + n3 = 45; + s2 = "ab"; + s3 = "de"; + st2 = new SomeStruct() { n = 12 }; + st3 = new SomeStruct() { n = 45 }; + f2 = new float[] { 1f, 3f, 5f }; + f3 = new float[] { 2f, 4f, 6f }; + } + } + + [HarmonyPatch(typeof(ArgumentArrayMethods), nameof(ArgumentArrayMethods.Method))] + public static class ArgumentArrayPatches + { + public static object[] prefixInput; + public static object[] postfixInput; + + public static bool Prefix(object[] __args) + { + prefixInput = (object[])Array.CreateInstance(typeof(object), __args.Length); + Array.Copy(__args, prefixInput, __args.Length); + + __args[1] = 123; + __args[2] = 456; + + __args[4] = "abc"; + __args[5] = "def"; + + __args[7] = new ArgumentArrayMethods.SomeStruct() { n = 123 }; + __args[8] = new ArgumentArrayMethods.SomeStruct() { n = 456 }; + + __args[10] = new float[] { 1.2f, 3.4f, 5.6f }; + __args[11] = new float[] { 2.1f, 4.3f, 6.5f }; + + return false; + } + + public static void Postfix( + int n1, int n2, int n3, + string s1, string s2, string s3, + ArgumentArrayMethods.SomeStruct st1, ArgumentArrayMethods.SomeStruct st2, ArgumentArrayMethods.SomeStruct st3, + float[] f1, float[] f2, float[] f3 + ) + { + postfixInput = new object[] + { + n1, n2, n3, + s1, s2, s3, + st1, st2, st3, + f1, f2, f3 + }; + } + } } diff --git a/HarmonyTests/Patching/Assets/PatchClasses.cs b/HarmonyTests/Patching/Assets/PatchClasses.cs index 68d6cd19..ae2a7a74 100644 --- a/HarmonyTests/Patching/Assets/PatchClasses.cs +++ b/HarmonyTests/Patching/Assets/PatchClasses.cs @@ -1281,7 +1281,7 @@ static IEnumerable Transpiler(IEnumerable inst { transpileCount++; var list = instructions.ToList(); - if (original.IsConstructor == false) + if (original.IsConstructor is false) list.Insert(list.Count - 1, CodeInstruction.Call(() => Fix(""))); return list.AsEnumerable(); }