动态方法反射发出对 Func<Task> 的调用

时间:2021-03-25 17:11:34

标签: c# reflection.emit dynamicmethod

我正在为一些内部库找出 Reflection.Emit 并坚持调用作为参数传入的 Func。我的场景测试使用的是图片中用Linqpad传输到IL的代码 Code to IL 我在 DynamicMethod 中复制 IL 的代码如下

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);
    }
    
}

代码编译并返回,但是当您调用 WrapScope 委托 del(Func<Task>) 时,它会抛出 System.InvalidProgramException:公共语言运行时检测到无效程序。 运行这个 DynamicMethod 可能有什么问题? 谢谢

1 个答案:

答案 0 :(得分:2)

您的主要问题是您在槽 0 中存储了一个变量,但您从未声明槽 0。如果我们查看您的代码 on SharpLab,我们可以看到 IL 声明了每个槽方法使用:

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

(也就是说我们有 1 个插槽,索引为 0,类型为 Task)。

您使用 ILGenerator.DeclareLocal 使用 ILGenerator 执行此操作。我们可以使用 ldloc/stloc 并传入返回的 LocalBuilder,而不是使用编号的 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);

这一切都很好,但它包含了很多不必要的说明。 Linqpad 在 Debug 中为您提供编译器的输出,这会发出许多不必要的 NOP 等。您会看到 Release 模式下的 SharpLab 不显示这些,我们可以简单地删除它们。 br.s 也无关紧要,因为它会无条件跳转到下一条指令,因此我们也可以将其删除:

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);

现在 stloc/ldloc 看起来毫无意义:我们从堆栈中取出一个变量,将其移动到本地,然后立即将其从本地复制回并按顺序复制到堆栈中返回它。完全抛弃本地:

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