MiddleTommy
MiddleTommy

Reputation: 389

DynamicMethod Reflection Emit a call to a Func<Task>

I am figuring out Reflection.Emit for some internal libraries and got stuck with calling a Func passed in as an argument. My scenario test uses the code transferred to IL with Linqpad in the picture Code to IL My code to duplicate the IL in a DynamicMethod is the following

public class ScopeTest
{
    public delegate Task WrapScope(Func<Task> value);
    public (WrapScope scope,string id) WrapScopeInId()
    {
        var id = $"wrap~{Guid.NewGuid().ToString().Replace("-",string.Empty)}";

        var mi = typeof(Func<Task>).GetMethod("Invoke");
        var d = new DynamicMethod(id, typeof(Task), new[] { typeof(Func<Task>) });
        var gen = d.GetILGenerator();
        var lab = gen.DefineLabel();

        gen.Emit(OpCodes.Nop);
        gen.Emit(OpCodes.Ldarg_0);
        gen.Emit(OpCodes.Callvirt, mi);
        gen.Emit(OpCodes.Stloc_0);
        gen.Emit(OpCodes.Br_S, lab);
        gen.MarkLabel(lab);
        gen.Emit(OpCodes.Ldloc_0);
        gen.Emit(OpCodes.Ret);

        WrapScope del = (WrapScope)d.CreateDelegate(typeof(WrapScope));
        return (del,id);
    }
    
}

The code compiles and returns but when you call the WrapScope delegate del(Func<Task>) it throws a System.InvalidProgramException: Common Language Runtime detected an invalid program. What could be the problem running this DynamicMethod? Thanks

Upvotes: 2

Views: 431

Answers (1)

canton7
canton7

Reputation: 42225

Your main problem is that you're storing a variable in slot 0, but you never declared slot 0. If we look at your code on SharpLab, we can see that the IL declares each of the slots that the method uses:

.locals init (
    [0] class [System.Private.CoreLib]System.Threading.Tasks.Task
)

(That says we've got 1 slot, index 0, of type Task).

You do this with ILGenerator using ILGenerator.DeclareLocal. We can use ldloc/stloc and pass in the LocalBuilder which is returned, rather than using the numbered ldloc.0/stloc.0.

var taskLocal = gen.DeclareLocal(typeof(Task));

gen.Emit(OpCodes.Nop);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Callvirt, mi);
gen.Emit(OpCodes.Stloc, taskLocal);
gen.Emit(OpCodes.Br_S, lab);
gen.MarkLabel(lab);
gen.Emit(OpCodes.Ldloc, taskLocal);
gen.Emit(OpCodes.Ret);

That's all fine and works, but it contains a lot of unnecessary instructions. Linqpad is giving you the output of the compiler in Debug, which emits lots of unnecessary NOPs, etc. You'll see that SharpLab in Release mode doesn't show those, and we can simply remove them. That br.s is also irrelevant, as it's unconditionally jumping to the next instruction, so we can remove that too:

var taskLocal = gen.DeclareLocal(typeof(Task));

gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Callvirt, mi);
gen.Emit(OpCodes.Stloc, taskLocal);
gen.Emit(OpCodes.Ldloc, taskLocal);
gen.Emit(OpCodes.Ret);

Now the stloc/ldloc looks pointless: we're taking a variable from the stack, moving it into a local, then immediately copying it back out of the local and onto the stack in order to return it. Just ditch the local altogether:

gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Callvirt, mi);
gen.Emit(OpCodes.Ret);

Upvotes: 3

Related Questions