0

I have a Method that creates a MethodBuilder Method and defines the Behaviour using ILGenerator and Emit + OpCodes.

This Method was created with the help of a previous StackOverflow Question I made - Thanks to @Marc Gravell for the help

The Behaviour is very specific and there is a reason I must create a method using MethodBuilder rather than standard method definition in C#, The Method is planned to be used for a Harmony PostFix HarmonyMethod Definition to Patch the Logic at runtime, and I don't want to use a static Method since ill be doing a lot of Method Mocks and I don't want to have a lot of static classes and methods, So I Created the following Class:

public class PostFixPatchFactory<T>
{
    public T Value { get; set; }
    public Exception Exception { get; set; }
    public PostFixPatchFactory(T value)
        => Value = value;
    public int TimesTriggered;
    public FieldInfo field;
    public MethodInfo GeneratePostfix()
    {
        string fieldName = $"DynamicField_{Guid.NewGuid()}";

        AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"DynamicAssembly_{Guid.NewGuid()}"), AssemblyBuilderAccess.RunAndCollect);
        ModuleBuilder module = assembly.DefineDynamicModule($"DynamicModule_{Guid.NewGuid()}");
        TypeBuilder typeBuilder = module.DefineType($"DynamicType_{Guid.NewGuid()}", TypeAttributes.Public);
        FieldBuilder field = typeBuilder.DefineField(fieldName, typeof(T), FieldAttributes.Private | FieldAttributes.Static);
        
        if (Value is Exception)
        {
            typeBuilder.DefineMethod("PostFix", MethodAttributes.Static | MethodAttributes.Public, typeof(void), null)
                .GetILGenerator().ThrowException(typeof(T));
        }
        else
        {
            MethodBuilder method = typeBuilder.DefineMethod("PostFix", MethodAttributes.Static | MethodAttributes.Public,
                typeof(void), new Type[] { typeof(T).MakeByRefType() });

            var il = method.GetILGenerator();            

            method.DefineParameter(1, ParameterAttributes.None, "__result");

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldsfld, field);
            il.Emit(OpCodes.Stobj, typeof(T));
            il.Emit(OpCodes.Ret);
        }
        var type = typeBuilder.CreateType();
        type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, Value);
        return type.GetMethod("PostFix", 
BindingFlags.Public | BindingFlags.Static);
    }
    public bool Prefix()
    {
        TimesTriggered++;
        return false;
    }
}

This Class is used as follows:

var harmony = new Harmony(nameof(Program));

        HarmonyMethod prefix = new HarmonyMethod(typeof(Program).GetMethod(nameof(Prefix)));

        var postFixPatchFactory = new PostFixPatchFactory<long>(6);
        
        MethodInfo originalMethod = typeof(Program).GetMethod(nameof(TestMethodToChange));

        MethodInfo scenarioToMockExample2 = typeof(ScenariosToMock).GetMethod("Example", BindingFlags.Static | BindingFlags.NonPublic);

        HarmonyMethod scenarioToMockExample2PostFix = new HarmonyMethod(g.GeneratePostfix());

        harmony.Patch(scenarioToMockExample2, prefix, scenarioToMockExample2PostFix);

        var result = TestMethodToChange();

        Console.WriteLine($"list result:[{result.Join(null,",")}]");

This works very nicely - except its missing one feature that I want - which is to have a property on the PostFixPatchFactory called TimesExcecuted or something of that variety so that I can track how many times a Method I patched got excecuted - I am struggling to Implement this feature - I want this feature to be part of the IL Emit Code within the new method definition - and I want the method to be accessible as an Instance public field or Property so that it can be used as follows:

int timesExcecuted = postFixPatchFactory.TimesExcecuted;
Console.WriteLine($"Method has been Triggered: {timesExcecuted}");

Please Help me - i'm not very good at IL or Opcodes so any help would be greatly Appreciated

I have tried using something along the lines of a new FieldBuilder:

FieldBuilder prop = typeBuilder.DefineField($"DynamicProperty_{Guid.NewGuid()}", typeof(int), 
            FieldAttributes.Public | FieldAttributes.Static);

And use something along the lines of the following ILCode:

            il.Emit(OpCodes.Ldsfld, prop);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Add);

But this did not work and returned a Invalid Code Exception

1 Answers1

1
il.Emit(OpCodes.Ldsfld, prop);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Add);

You're leaving "prop + 1" on the stack. You need to save it back to the field

il.Emit(OpCodes.Stsfld, prop);

I tried the following code in .net 6

AssemblyBuilder assembly =
         AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"DynamicAssembly_{Guid.NewGuid()}"),
            AssemblyBuilderAccess.RunAndCollect);
      ModuleBuilder module = assembly.DefineDynamicModule($"DynamicModule_{Guid.NewGuid()}");
      TypeBuilder typeBuilder = module.DefineType($"DynamicType_{Guid.NewGuid()}", TypeAttributes.Public);
      FieldBuilder field =
         typeBuilder.DefineField(fieldName, typeof(T), FieldAttributes.Private | FieldAttributes.Static);

      FieldBuilder prop = typeBuilder.DefineField($"DynamicProperty_{Guid.NewGuid()}", typeof(int),
         FieldAttributes.Public | FieldAttributes.Static);

      MethodBuilder method = typeBuilder.DefineMethod("PostFix", MethodAttributes.Static | MethodAttributes.Public,
         typeof(void), new Type[] { typeof(T).MakeByRefType() });

      var il = method.GetILGenerator();

      method.DefineParameter(1, ParameterAttributes.None, "__result");


      il.Emit(OpCodes.Ldarg_0);
   il.Emit(OpCodes.Ldsfld, field);
   il.Emit(OpCodes.Stobj, typeof(T));
   il.Emit(OpCodes.Ldsfld, prop);
   il.Emit(OpCodes.Ldc_I4_1);
   il.Emit(OpCodes.Add);
   il.Emit(OpCodes.Stsfld, prop);
   il.Emit(OpCodes.Ret);


   var t = typeBuilder.CreateType();
      var ot = Activator.CreateInstance(t);
      var m = t.GetMethod("PostFix");
      
      
      for (var i = 0; i < 5; i++)
         m.Invoke(null, new Object[] { value });


      var propVal = t.GetField(prop.Name).GetValue(null);

   Debug.WriteLine($"propVal is {propVal}");

The output was

propVal is 5
Ready Cent
  • 1,821
  • 3
  • 18
  • 25
  • wow, ill try that out. I didn't even notice that – NannerBannaner23 Apr 12 '23 at 11:40
  • I've tried that, this is what the ILCode looks like: `il.Emit(OpCodes.Ldarg_0);` `il.Emit(OpCodes.Ldsfld, field);` `il.Emit(OpCodes.Stobj, typeof(T));` `il.Emit(OpCodes.Pop);` `il.Emit(OpCodes.Ldsfld, prop);` `il.Emit(OpCodes.Ldc_I4_1);` `il.Emit(OpCodes.Add);` `il.Emit(OpCodes.Stsfld, prop);` `il.Emit(OpCodes.Ret);` I recieved a `System.Reflection.TargetInvocationException` with the following Message: `InvalidProgramException: Common Language Runtime detected an invalid program.` – NannerBannaner23 Apr 12 '23 at 11:44
  • Not sure why you are doing il.Emit(OpCodes.Pop) after stobj. As far as I can tell, the stack would be empty there. Your method was previously working when you just did ret right after the stobj. Given that the method returns void, the stack had to be empty or it would have failed at runtime. – Ready Cent Apr 12 '23 at 12:26
  • the code you added is identical to the one I shared above. – NannerBannaner23 Apr 12 '23 at 12:27
  • I see ill try removing the Pop – NannerBannaner23 Apr 12 '23 at 12:27
  • I've removed the Pop, but i still get a `InvalidProgramException` – NannerBannaner23 Apr 12 '23 at 12:29
  • I edited the original answer to include a full code sample of it working for me without the pop. Does it work for you if you copy/paste that? Another general thing I'd recommend when working with ILGenerator is to save your dynamic assembly to disk (you can use the lokad nuget package if you're on .net core) and then open the assembly in ILSpy. You're likely to get a better explanation there than "Invalid Program". In general being able to see your decompiled code will make your life much easier – Ready Cent Apr 12 '23 at 12:45