Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MethodDelegate: allow for swapping value type with object, and allow ref for arg 0 on value type #633

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions Harmony/Tools/AccessTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,13 @@ public static FieldRef<F> StaticFieldRefAccess<F>(FieldInfo fieldInfo)
/// else, invocation of the delegate calls the exact specified <paramref name="method"/> (this is useful for calling base class methods)
/// Note: if <c>false</c> and <paramref name="method"/> is an interface method, an ArgumentException is thrown.
/// </param>
/// <param name="delegateArgs">
/// Only applies for instance methods, and if argument <paramref name="instance"/> is null.
/// This argument only matters if the target <paramref name="method"/> signature contains a value type (such as struct or primitive types),
/// and your <typeparamref name="DelegateType"/> argument is replaced by a non-value type
/// (usually <c>object</c>) instead of using said value type.
/// Use this if the generic arguments of <typeparamref name="DelegateType"/> doesn't represent the delegate's
/// arguments, and calling this function fails
/// <returns>A delegate of given <typeparamref name="DelegateType"/> to given <paramref name="method"/></returns>
/// <remarks>
/// <para>
Expand All @@ -1536,7 +1543,7 @@ public static FieldRef<F> StaticFieldRefAccess<F>(FieldInfo fieldInfo)
/// </para>
/// </remarks>
///
public static DelegateType MethodDelegate<DelegateType>(MethodInfo method, object instance = null, bool virtualCall = true) where DelegateType : Delegate
public static DelegateType MethodDelegate<DelegateType>(MethodInfo method, object instance = null, bool virtualCall = true, Type[] delegateArgs = null) where DelegateType : Delegate
{
if (method is null)
throw new ArgumentNullException(nameof(method));
Expand Down Expand Up @@ -1612,20 +1619,36 @@ public static DelegateType MethodDelegate<DelegateType>(MethodInfo method, objec
parameterTypes[0] = declaringType;
for (var i = 0; i < numParameters; i++)
parameterTypes[i + 1] = parameters[i].ParameterType;
var delegateArgsResolved = delegateArgs ?? delegateType.GetGenericArguments();
var dynMethodReturn = delegateArgsResolved.Length < parameterTypes.Length
? parameterTypes
: delegateArgsResolved;
var dmd = new DynamicMethodDefinition(
"OpenInstanceDelegate_" + method.Name,
method.ReturnType,
parameterTypes)
dynMethodReturn)
{
// OwnerType = declaringType
};
var ilGen = dmd.GetILGenerator();
if (declaringType != null && declaringType.IsValueType)
if (declaringType != null && declaringType.IsValueType && delegateArgsResolved.Length > 0 &&
!delegateArgsResolved[0].IsByRef)
{
ilGen.Emit(OpCodes.Ldarga_S, 0);
}
else
ilGen.Emit(OpCodes.Ldarg_0);
for (var i = 1; i < parameterTypes.Length; i++)
{
ilGen.Emit(OpCodes.Ldarg, i);
// unbox to make il code valid
if (parameterTypes[i].IsValueType && i < delegateArgsResolved.Length &&
!delegateArgsResolved[i].IsValueType)
{
ilGen.Emit(OpCodes.Unbox_Any, parameterTypes[i]);
}
}

ilGen.Emit(OpCodes.Call, method);
ilGen.Emit(OpCodes.Ret);
return (DelegateType)dmd.Generate().CreateDelegate(delegateType);
Expand Down
44 changes: 44 additions & 0 deletions HarmonyTests/Tools/TestAccessTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,48 @@ public void Test_AccessTools_MethodDelegate_OpenInstanceDelegates_InterfaceMetho
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDel<Struct>>(interfaceTest, virtualCall: false)(structInstance, 456, ref f));
}

[Test]
public void Test_AccessTools_MethodDelegate_OpenInstanceDelegates_Arg0ByRef()
{
// only applies to struct, since you want to mutate structs via ref
var f = 789f;

var structInstance = new Struct();
// repeat for mutation
Assert.AreEqual("struct result 456 790 1", AccessTools.MethodDelegate<OpenMethodDelRefInstance<Struct>>(structTest, virtualCall: false, delegateArgs: [typeof(Struct).MakeByRefType(), typeof(int), typeof(float).MakeByRefType()])(ref structInstance, 456, ref f));
Assert.AreEqual("struct result 456 791 2", AccessTools.MethodDelegate<OpenMethodDelRefInstance<Struct>>(structTest, virtualCall: false, delegateArgs: [typeof(Struct).MakeByRefType(), typeof(int), typeof(float).MakeByRefType()])(ref structInstance, 456, ref f));
Assert.AreEqual("struct result 456 792 3", AccessTools.MethodDelegate<OpenMethodDelRefInstance<Struct>>(structTest, virtualCall: false, delegateArgs: [typeof(Struct).MakeByRefType(), typeof(int), typeof(float).MakeByRefType()])(ref structInstance, 456, ref f));
}

[Test]
public void Test_AccessTools_MethodDelegate_OpenInstanceDelegates_BoxedArgs()
{
var f = 789f;
var baseInstance = new Base();
var derivedInstance = new Derived();
var structInstance = new Struct();
var delegateArgs_IInterface = new Type[] { typeof(IInterface), typeof(object), typeof(float).MakeByRefType() };
var delegateArgs_Base = new Type[] { typeof(Base), typeof(object), typeof(float).MakeByRefType() };
var delegateArgs_Derived = new Type[] { typeof(Derived), typeof(object), typeof(float).MakeByRefType() };
var delegateArgs_Struct = new Type[] { typeof(Struct), typeof(object), typeof(float).MakeByRefType() };
// Assert.AreEqual("base test 456 790 1", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_IInterface)(baseInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_IInterface)(baseInstance, 456, ref f));
// Assert.AreEqual("derived test 456 791 1", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_IInterface)(derivedInstance, 456, ref f));
// _ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_IInterface)(derivedInstance, 456, ref f));
// Assert.AreEqual("struct result 456 792 1", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_IInterface)(structInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<IInterface>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_IInterface)(structInstance, 456, ref f));
// Assert.AreEqual("base test 456 793 2", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Base>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_Base)(baseInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Base>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_Base)(baseInstance, 456, ref f));
// Assert.AreEqual("derived test 456 794 2", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Base>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_Base)(derivedInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Base>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_Base)(derivedInstance, 456, ref f));
// AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Derived>>(interfaceTest, virtualCall: true)(baseInstance, 456, ref f)); // expected compile error
// AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Derived>>(interfaceTest, virtualCall: false)(baseInstance, 456, ref f)); // expected compile error
// Assert.AreEqual("derived test 456 795 3", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Derived>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_Derived)(derivedInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Derived>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_Derived)(derivedInstance, 456, ref f));
// Assert.AreEqual("struct result 456 796 1", AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Struct>>(interfaceTest, virtualCall: true, delegateArgs: delegateArgs_Struct)(structInstance, 456, ref f));
_ = Assert.Throws(typeof(ArgumentException), () => AccessTools.MethodDelegate<OpenMethodDelBoxedArg<Struct>>(interfaceTest, virtualCall: false, delegateArgs: delegateArgs_Struct)(structInstance, 456, ref f));
}

[Test]
public void Test_AccessTools_MethodDelegate_StaticDelegates_InterfaceMethod()
{
Expand All @@ -520,6 +562,8 @@ public void Test_AccessTools_MethodDelegate_InvalidDelegates()

delegate string MethodDel(int n, ref float f);
delegate string OpenMethodDel<T>(T instance, int n, ref float f);
delegate string OpenMethodDelRefInstance<T>(ref T instance, int n, ref float f);
delegate string OpenMethodDelBoxedArg<T>(T instance, object n, ref float f);

[Test]
public void Test_AccessTools_HarmonyDelegate()
Expand Down