I have a bit of code that takes two Methods and injects one (injectionMethod) in place of the other (replacedMethod) in the IL. This is being used to manipulate the result of the replacedMethod to give the result provided by the injectionMethod in a unit test.
public static void Inject<T>( Func<T> replacedMethod, Func<T> injectionMethod )
{
string replacedMethodName = replacedMethod.Method.Name;
Type replacedMethodType = replacedMethod.Method.ReflectedType;
string injectionMethodName = injectionMethod.Method.Name;
Type injectionMethodType = injectionMethod.Method.ReflectedType;
MethodInfo methodToReplace = replacedMethodType.GetMethod( replacedMethodName );
MethodInfo methodToInject = injectionMethodType.GetMethod( injectionMethodName );
RuntimeHelpers.PrepareMethod( methodToReplace.MethodHandle );
RuntimeHelpers.PrepareMethod( methodToInject.MethodHandle );
ReplacedMethod = methodToReplace;
unsafe {
if( IntPtr.Size == 4 ) {
int* inj = (int*) methodToInject.MethodHandle.Value.ToPointer() + 2;
int* tar = (int*) methodToReplace.MethodHandle.Value.ToPointer() + 2;
if( System.Diagnostics.Debugger.IsAttached ) {
//Version x86 Debug
byte* injInst = (byte*) *inj;
byte* tarInst = (byte*) *tar;
int* injSrc = (int*) (injInst + 1);
int* tarSrc = (int*) (tarInst + 1);
*tarSrc = (((int) injInst + 5) + *injSrc) - ((int) tarInst + 5);
}
else {
//Version x86 Release
*tar = *inj;
}
}
else {
long* inj = (long*) methodToInject.MethodHandle.Value.ToPointer() + 1;
long* tar = (long*) methodToReplace.MethodHandle.Value.ToPointer() + 1;
if( System.Diagnostics.Debugger.IsAttached ) {
//Version x64 Debug
byte* injInst = (byte*) *inj;
byte* tarInst = (byte*) *tar;
int* injSrc = (int*) (injInst + 1);
int* tarSrc = (int*) (tarInst + 1);
*tarSrc = (((int) injInst + 5) + *injSrc) - ((int) tarInst + 5);
}
else {
//Version x64 Release
*tar = *inj;
}
}
}
}
The code works, but I do not fully understand how it works under the hood. Once the IL is jitted the injected method seems to be cached permanently and any other code referencing the original method fails because it is referencing the injected method.
I am not looking to refactor the original code under test and would rather not run one test at a time which uses the Inject method. Is there a way to "undo" the injection done here?
I had tried to save off the target source, however once the target method had been replaced I couldn't simply reassign the previous source without the code going into break mode. This turned out to be a fairly simple answer. All I had to do was run the same command a second time after I was done running the methods as swapped(injected). Running an NUnit test on an object with the ToLongString() method this will pass:
public string InterceptorToString()
{
return "Injected Text";
}
[Test]
[Category( "Interceptor" )]
public void InjectionTest()
{
MyObject obj = new MyObject();
string objString1 = obj.ToLongString();
string intString1 = InterceptorToString(); //returns "Injected Text"
Interceptor.Inject( obj.ToLongString, InterceptorToString );
string objString2 = obj.ToLongString(); //returns "Injected Text"
string intString2 = InterceptorToString();
Interceptor.Inject( obj.ToLongString, InterceptorToString );
string objString3 = obj.ToLongString();
string intString3 = InterceptorToString(); //returns "Injected Text"
Assert.That( objString2, Is.Not.EqualTo( intString2 ) );
Assert.That( objString3, Is.Not.EqualTo( intString3 ) );
Assert.That( objString2, Is.EqualTo( "Injected Text" ) );
}
Changes that had to be made to the Interceptor class:
unsafe {
if( IntPtr.Size == 4 ) {
int* inj = (int*) methodToInject.MethodHandle.Value.ToPointer() + 2;
int* tar = (int*) methodToReplace.MethodHandle.Value.ToPointer() + 2;
if( System.Diagnostics.Debugger.IsAttached ) {
//Version x86 Debug
byte* injInst = (byte*) *inj;
byte* tarInst = (byte*) *tar;
int* injSrc = (int*) (injInst + 1);
int* tarSrc = (int*) (tarInst + 1);
Replaced = (((int) tarInst + 5) + *tarSrc) - ((int) injInst + 5);
*tarSrc = (((int) injInst + 5) + *injSrc) - ((int) tarInst + 5);
*injSrc = Replaced;
}
else {
//Version x86 Release
Replaced = *tar;
*tar = *inj;
*inj = Replaced;
}
}