quentin-starin
quentin-starin

Reputation: 26688

How to use a local object in a class dynamically generated with IL Emit

I'm not even sure if this is possible.. In a method I am creating a dynamic assembly, defining a type, and emitting IL for a constructor for that type. This method takes an IEnumerable<Action> as a parameter and I'd like to be able to use that reference inside the class I am generating.

I've written some database migration helpers to work with either FluentMigrator or MigratorDotNet, and I am trying to implement unit tests to verify correct function. With FluentMigrator I am able to instantiate a runner and pass it instances of Migration classes. With MigratorDotNet, however, it requires me to pass it an assembly that it scans for Migration class to instantiate and run - hence the dynamic generation.

This is the base class I'm dynamically sub-classing:

    public class ActionMigration : Migration
    {
        private readonly IEnumerable<Action<Migration>> _up;
        private readonly IEnumerable<Action<Migration>> _down;
        public ActionMigration(Action<Migration> migration) : this(migration, migration) { }
        public ActionMigration(Action<Migration> up, Action<Migration> down) : this(new[] { up }, new[] { down }) { }
        public ActionMigration(IEnumerable<Action<Migration>> actions) : this(actions, actions) { }
        public ActionMigration(IEnumerable<Action<Migration>> up, IEnumerable<Action<Migration>> down) { _up = up; _down = down; }
        public override void Down() => _down?.ForEach(m => m(this));
        public override void Up() => _up?.ForEach(m => m(this));
    }

This is my code generating the dynamic implementation:

    private Assembly BuildMigrationAssembly(IEnumerable<Action<Migration>> actions)
    {
        var assemblyName = $"mdn_test_{Guid.NewGuid()}";
        var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndSave);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyBuilder.GetName().Name, assemblyName + ".dll");
        BuildMigrationClass(moduleBuilder, 1, actions);

        return assemblyBuilder;
    }

    private void BuildMigrationClass(ModuleBuilder moduleBuilder, long version, IEnumerable<Action<Migration>> actions)
    {
        var baseType = typeof(ActionMigration);
        var typeBuilder = moduleBuilder.DefineType($"Migration{version}",
            TypeAttributes.Public | TypeAttributes.Class |
            TypeAttributes.AutoClass | TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
            baseType);

        var migAttrType = typeof(MigrationAttribute);
        var migAttrCtor = migAttrType.GetConstructor(new[] { typeof(long) });
        typeBuilder.SetCustomAttribute(migAttrCtor, BitConverter.GetBytes(version));

        var baseCtor = baseType.GetConstructor(new[] { typeof(IEnumerable<Action<Migration>>) });
        var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, null);
        var ilg = ctor.GetILGenerator();
        ilg.Emit(OpCodes.Ldarg_0);
        // how can I pass the local 'actions' object to the base constructor here?
        ilg.Emit(OpCodes.Call, baseCtor);
        ilg.Emit(OpCodes.Nop);
        ilg.Emit(OpCodes.Nop);
        ilg.Emit(OpCodes.Ret);
    }

I opened a project and created some sample sub-classes to inspect:

namespace MdnTest
{
    [Migration(1)]
    public class Migration1 : EasyMigrator.Tests.Integration.Migrators.MigratorDotNet.ActionMigration
    {
        public Migration1() : base(new List<Action<Migration>>()) { }
    }
}

Or:

namespace MdnTest
{
    [Migration(1)]
    public class Migration1 : EasyMigrator.Tests.Integration.Migrators.MigratorDotNet.ActionMigration
    {
        static private readonly IEnumerable<Action<Migration>> _actions;
        public Migration1() : base(_actions) { }
    }
}

This is the IL they generate:

.class public auto ansi beforefieldinit 
  MdnTest.Migration1
    extends [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration
{
  .custom instance void [Migrator.Framework]Migrator.Framework.MigrationAttribute::.ctor(int64) 
    = (01 00 01 00 00 00 00 00 00 00 00 00 ) // ............
    // int64(1) // 0x0000000000000001

  .method public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    // [14 31 - 14 66]
    IL_0000: ldarg.0      // this
    IL_0001: newobj       instance void class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>::.ctor()
    IL_0006: call         instance void [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>)
    IL_000b: nop          

    // [14 67 - 14 68]
    IL_000c: nop          

    // [14 69 - 14 70]
    IL_000d: ret          

  } // end of method Migration1::.ctor
} // end of class MdnTest.Migration1

Or:

.class public auto ansi beforefieldinit 
  MdnTest.Migration1
    extends [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration
{
  .custom instance void [Migrator.Framework]Migrator.Framework.MigrationAttribute::.ctor(int64) 
    = (01 00 01 00 00 00 00 00 00 00 00 00 ) // ............
    // int64(1) // 0x0000000000000001

  .field private static initonly class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>> _actions

  .method public hidebysig specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8

    // [15 31 - 15 45]
    IL_0000: ldarg.0      // this
    IL_0001: ldsfld       class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>> MdnTest.Migration1::_actions
    IL_0006: call         instance void [EasyMigrator.Tests]EasyMigrator.Tests.Integration.Migrators.MigratorDotNet/ActionMigration::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<class [mscorlib]System.Action`1<class [Migrator.Framework]Migrator.Framework.Migration>>)
    IL_000b: nop          

    // [15 46 - 15 47]
    IL_000c: nop          

    // [15 48 - 15 49]
    IL_000d: ret          

  } // end of method Migration1::.ctor
} // end of class MdnTest.Migration1

I'm not sure how to adapt this to what I'm trying to achieve.. Can I just plop a reference to this local object that exists outside the context of the dynamic assembly in an IL load instruction? Could Expressions help? Maybe trying to pass it in the constructor is the wrong way to go about it - perhaps instead overriding the up and down implementations (can I get like a function handle to an Action and emit a call to it, rather than pass in the reference to the IEnumerable?).

I'm passingly familiar with assembly and IL and after reviewing the operations I'm starting to think I might not be able to do what I'm attempting to do.

So, is this even possible, and if so can someone nudge me in the right direction?

For the curious, the full code is here.

Upvotes: 1

Views: 396

Answers (1)

Andrey Tretyak
Andrey Tretyak

Reputation: 3231

You can path reference to object that exist outside dynamic assembly by defining constructor with parameter of type IEnumerable<Action<Migration>> for your dynamic class:

var ctor = typeBuilder.DefineConstructor(
    MethodAttributes.Public,
    CallingConventions.Standard, 
    new[] { typeof(IEnumerable<Action<Migration>>) });

Then use those parameter to path it in base class constructor:

var ilg = ctor.GetILGenerator();
ilg.Emit(OpCodes.Ldarg_0);        // load 'this' onto stack 
ilg.Emit(OpCodes.Ldarg_1);        // load constructor argument onto the stack 
ilg.Emit(OpCodes.Call, baseCtor); // call base constructor
ilg.Emit(OpCodes.Ret);

After this you will be able to create instance of your dynamic class using Activator:

var type = typeBuilder.CreateType();
var args = new object[] { new List<Action<Migration>>() };
var instance = Activator.CreateInstance(type, args);

Upvotes: 0

Related Questions