2

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.

usr
  • 168,620
  • 35
  • 240
  • 369
Lukas Rieger
  • 676
  • 10
  • 31

1 Answers1

4

I would rewrite a call to ExecuteScalar into a call to a helper method:

static object ExecuteScalarWrapper(SqlCommand command, string logString) {
        using (SqlLogger.Enter(command, logString))
        {
            return command.ExecuteScalar();
        }
}

ExecuteScalarWrapper would 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

ld... command
call ExecuteScalar

with

ld... command
ld... logString
call ExecuteScalarWrapper

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.

usr
  • 168,620
  • 35
  • 240
  • 369