Search code examples
c#expression-treesil

Cannot call static methods using expression converted to IL in C#


This is my first attempt to generate IL from expression and I couldn't get it working. I couldn't make a call to a static method using IL generator.

My Class structure is

public class TestClass
{
    public static A Process(IServiceFactory factory, A a)
    {
        a.Value = 40;
        return a;
    }
}

public class A
{
    public int Value { get; set; }
}

I need to generate IL which does this

a.Value = 10;
TestClass.Process(factory,a)

The IL code I managed to generate is

IL_0000: ldarg.1    
IL_0001: castclass  A
IL_0006: stloc.0    
IL_0007: ldloc.0    
IL_0008: ldarg.0    
IL_0009: ldc.i4.0   
IL_000a: ldelem.ref 
IL_000b: castclass  System.Func`1[System.Int32]
IL_0010: callvirt   Int32 Invoke()/System.Func`1[System.Int32]
IL_0015: callvirt   Void set_Value(Int32)/A
IL_001a: ldloc.0    
IL_001b: ldarg.0    
IL_001c: ldc.i4.1   
IL_001d: ldelem.ref 
IL_001e: castclass  IServiceFactory
IL_0023: ldarg.1    
IL_0024: castclass  A
IL_0029: call       A Process(IServiceFactory, A)/TestClass
IL_002e: ret        

Note: its loading the actual factory object from a array.

This IL generates InvalidProgramException. But if I can the static method to instance method and execute the call within the TestClass instance it works fine.

Not sure where I have got it wrong


Solution

  • It looks like you're loading too much stuff on the stack. The first part seems fine (ignoring that it doesn't match your desired effect):

    IL_0000: ldarg.1    
    IL_0001: castclass  A
    IL_0006: stloc.0    
    IL_0007: ldloc.0    
    IL_0008: ldarg.0    
    IL_0009: ldc.i4.0   
    IL_000a: ldelem.ref 
    IL_000b: castclass  System.Func`1[System.Int32]
    IL_0010: callvirt   Int32 Invoke()/System.Func`1[System.Int32]
    IL_0015: callvirt   Void set_Value(Int32)/A
    

    Roughly this is equivalent to the following C#:

    A a = (A) arg1;
    a.Value = ((Func<int>)arg0[0]).Invoke();
    

    I have no idea what the generated method args are as you didn't post them. Anyway, moving on:

    IL_001a: ldloc.0                                            // {a}
    IL_001b: ldarg.0                                            // {a, arg0}
    IL_001c: ldc.i4.1                                           // {a, arg0, 1}
    IL_001d: ldelem.ref                                         // {a, arg0[1]}
    IL_001e: castclass  IServiceFactory                         // {a, (IServiceFactory) arg0[1] }
    IL_0023: ldarg.1                                            // {a, (IServiceFactory) arg0[1], arg1 }
    IL_0024: castclass  A                                       // {a, (IServiceFactory) arg0[1], (A) arg1 }
    IL_0029: call       A Process(IServiceFactory, A)/TestClass // {a, TestClass.Process( (IServiceFactory) arg0[1], (A) arg1 ) }
    IL_002e: ret   
    

    I've annotated the IL with the state of the stack after each instruction. You can see that when you reach the return instruction, you have two values sitting on the stack. It seems that the ldloc.0 at IL_001a is a mistake and shouldn't be there (additionally, IL_0023-0024 could be replaced with a simple ldloc.0 rather than re-loading arg.1 and casting it to A). This could explain why it works when it's not a static method: that additional object on the stack is treated as the object to invoke the method on, so it's consumed correctly.

    Depending on whether your generated method is supposed to return a value or not, you may need an additional pop to clear the return value of the TestClass.Process method off the stack. Since you said it worked as an instance method, that makes it sound like the generated method returns a value, so you won't need a pop.