DynamicMethod 反射发出对 Func<Task> 的调用

DynamicMethod Reflection Emit a call to a Func<Task>

我正在为一些内部库找出 Reflection.Emit 并且无法调用作为参数传入的 Func。我的场景测试使用图中用Linqpad转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);
    }
    
}

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

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

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

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

您使用 ILGenerator.DeclareLocalILGenerator 执行此操作。我们可以使用 ldloc/stloc 并传入 returned 的 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 看起来毫无意义:我们从堆栈中取出一个变量,将其移动到本地,然后立即将其从本地复制回堆栈中为了return吧。完全放弃本地:

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