I want to inject sql logging into a few methods. Basically I want to transform
public static object IDbCommandTest_ExecuteScalar(IDbCommand command)
{
// .. do stuff
command.CommandText = "SELECT ...";
var obj = command.ExecuteScalar();
Console.WriteLine("SQL returned " + obj);
// do other stuff
return obj;
}
into
public static object IDbCommandTest_ExecuteScalar_Transformed(IDbCommand command)
{
// .. do stuff
command.CommandText = "SELECT ...";
object obj;
using (SqlLogger.Enter(command, "IDbCommand", "ExecuteScalar"))
{
obj = command.ExecuteScalar();
}
Console.WriteLine("SQL returned " + obj);
// do other stuff
return obj;
}
I got the easy cases working, the issue I am currently facing is that the stack must be empty when entering a .try.
My primitive assumption was that the IDbCommand itself was loaded directly before the call to ExecuteScalar, so I search for callvirt and then use Instruction.Previous as the start of the .try.
But if the return value of ExecuteScalar is used afterwards, the compiler generates the following IL:
IL_004d: ldarg.0
IL_004e: ldloc.1
IL_004f: callvirt instance object [System.Data]System.Data.IDbCommand::ExecuteScalar()
IL_0054: call string Class_which_uses_obj::DoStuff(object)
With my primitivy algorithm, this is transformed into
IL_0050: ldarg.0
.try
{
IL_0051: ldloc.1
IL_0052: dup
IL_0053: ldstr "IDbCommand"
IL_0058: ldstr "get_FileName"
IL_005d: call class [mscorlib]System.IDisposable SqlLogger::Enter(class [System.Data]System.Data.IDbCommand, string, string)
IL_0062: stloc.3
IL_0063: callvirt instance object [System.Data]System.Data.IDbCommand::ExecuteScalar()
IL_0068: stloc.s 4
IL_006a: leave.s IL_0077
} // end .try
finally
{
IL_006c: nop
IL_006d: ldloc.3
IL_006e: brfalse.s IL_0076
IL_0070: ldloc.3
IL_0071: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0076: endfinally
} // end handler
IL_0077: nop
IL_0078: ldloc.s 4
IL_007a: call string IL_0050: ldarg.0
.try
{
IL_0051: ldloc.1
IL_0052: dup
IL_0053: ldstr "IDbCommand"
IL_0058: ldstr "ExecuteScalar"
IL_005d: call class [mscorlib]System.IDisposable Nemetschek.Allready.SqlLogger::Enter(class [System.Data]System.Data.IDbCommand, string, string)
IL_0062: stloc.3
IL_0063: callvirt instance object [System.Data]System.Data.IDbCommand::ExecuteScalar()
IL_0068: stloc.s 4
IL_006a: leave.s IL_0077
} // end .try
finally
{
IL_006c: nop
IL_006d: ldloc.3
IL_006e: brfalse.s IL_0076
IL_0070: ldloc.3
IL_0071: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0076: endfinally
} // end handler
IL_0077: nop
IL_0078: ldloc.s 4
IL_007a: call string Nemetschek.Allready.Logistics.DbTools.CDbTools::GetSafeStringEmpty(object)(object)
Then PEVERIFY complains that I enter a .try with a non-empty stack.
Is there an easy way to inject the try-finally over just ExecuteScalar or do I need to do a complete flow analysis over the whole method, calculating the stack depth at any point and then loading/restoring the values before/after the try/finally?
EDIT:
I have been able to get it working by scanning up/down until I find two points where the stack-depth is 0. In my limited tests, this seems to work, but I would still be interested in a 'clean' implementation instead of blindly scanning the IL.
I would rewrite a call to
ExecuteScalarinto a call to a helper method:ExecuteScalarWrapperwould be a static helper method that you can write in C# and reference.Then, you do not need to inject any try-blocks. You just need to replace the pattern
with
which should be easier because the stack layout and modifications are locally defined and do not require any complicated reasoning.
Now the JIT does all the heavy lifting for you.