OpCodes.Call 与 OpCodes.Callvirt (使用.net 中的动态方法编程备忘录7)

本文详细介绍了.NET中OpCodes.Call和OpCodes.Callvirt指令的堆栈转换行为及其在多态调用中的差异。Callvirt用于后期绑定方法调用,考虑了对象的运行时类型,而Call则不涉及后期绑定,直接调用指定方法。在某些情况下,Callvirt可能比Call更安全,但可能导致服务器环境中出现不一致的行为。为确保兼容性,建议根据方法声明类型动态选择使用Call或Callvirt。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MSDN中,对 OpCodes.Call 与 OpCodes.Callvirt 的解释,分别为:

 

OpCodes.Call

 

调用由传递的方法说明符指示的方法。

 

堆栈转换行为依次为:

1.将从 arg1 到 argN 的方法参数推送到堆栈上。

2.从堆栈中弹出从 argN 到 arg1 的方法参数;通过这些参数执行方法调用并将控制转移到由方法说明符表示的方法。完成后,被调用方方法生成返回值并将其发送给调用方。

3.将返回值推送到堆栈上。

 

call 指令调用由通过该指令传递的方法说明符指示的方法。方法说明符是元数据标记,它指示将调用的方法和将被传递到该方法的放到堆栈上的参数的数目、类型和顺序以及要使用的调用约定。tail (Tailcall) 前缀指令可以紧靠 call 指令之前,以指定转移控制之前应释放当前方法状态。如果调用将控制转移到比原始方法信任级别更高的方法,则不释放堆栈帧。而是继续无提示执行,就像尚未提供 tail 一样。元数据标记带有用来确定是对静态方法、实例方法、虚方法还是全局函数进行调用所需的足够的信息。在所有这些情况中,目标地址完全从方法说明符确定(与用于调用虚方法的 Callvirt 指令相比,后者的目标地址还取决于 Callvirt 前推送的实例引用的运行时类型。)

将参数按从左到右的顺序放到堆栈上。即,先计算第一个参数并将其放到堆栈上,然后处理第二个参数,接着处理第三个参数,直到将所有需要的参数都按降序放置在堆栈的顶部为止。有三个重要的特殊情况:

1. 对实例方法(或虚方法)的调用必须在任何用户可见的参数前推送该实例引用。实例引用不得是空引用。元数据中带有的签名不在参数列表中为 this 指针包含项;它而是使用位来指示方法是否需要传递 this 指针。

2. 使用 call(而不是 callvirt)调用虚方法是有效的;这指示将使用方法指定的类(而不是从所调用的对象动态指定的类)来解析该方法。

3. 请注意,可以通过 call 或 callvirt 指令调用委托的 Invoke 方法。

如果系统安全机制没有授予调用方对被调用方法的访问权限,则可能引发 SecurityException。在 Microsoft 中间语言 (MSIL) 指令被转换为本机代码时(而不是在运行时),可能进行安全检查。

注意:
对值类型调用 System.Object 的方法时,考虑使用 constrained 前缀和 callvirt 指令,而不是发出 call 指令。因而不需要根据值类型是否重写方法来发送不同 IL,从而避免了潜在的版本问题。对值类型调用接口方法时考虑使用 constrained 前缀,因为实现接口方法的值类型方法可以用 MethodImpl 更改。这些问题在 Constrained 操作码里有更详细的说明。
 

下面的 Emit 方法重载可以使用 call 操作码:

ILGenerator.Emit(OpCode, MethodInfo)

ILGenerator.EmitCall(OpCode, MethodInfo, Type[])

 

OpCodes.Callvirt:

 

对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。

堆栈转换行为依次为:

将对象引用 obj 推送到堆栈上。

将从 arg1 到 argN 的方法参数推送到堆栈上。

从堆栈中弹出从 arg1 到 argN 的方法参数和对象引用 obj;通过这些参数执行方法调用并将控制转移到由方法元数据标记引用的 obj 中的方法。完成后,被调用方方法生成返回值并将其发送给调用方。

将返回值推送到堆栈上。

callvirt 指令对对象调用后期绑定方法。也就是说,基于 obj 的运行时类型而不是在方法指针中可见的编译时类型选择方法。Callvirt 可用于调用虚方法和实例方法。tail 前缀可以紧靠 callvirt 指令之前 (Tailcall),以指定在转移控制前应释放当前堆栈帧。如果调用将控制转移到比原始方法信任级别更高的方法,将不释放堆栈帧。

方法元数据标记提供要调用的方法的名称、类和签名。与 obj 关联的类是属于实例的类。如果该类定义匹配指示的方法名称和签名的非静态方法,则调用该方法。否则按顺序检查此类的基类链中的所有类。如果未找到任何方法,则是错误的。

Callvirt 在调用方法前从计算堆栈中弹出该对象和关联的参数。如果该方法具有返回值,则在方法完成后将该返回值推送到堆栈上。在被调用方,obj 参数被作为参数 0 访问,arg1 被作为参数 1 访问,依此类推。

将参数按从左到右的顺序放到堆栈上。即,先计算第一个参数并将其放到堆栈上,然后处理第二个参数,接着处理第三个参数,直到将所有需要的参数都按降序放置在堆栈的顶部为止。必须在任何用户可见参数前推送实例引用 obj(callvirt 始终需要的)。签名(在元数据标记中携带)不需要在参数列表中为该指针包含项。

请注意,还可以使用 Call 指令调用虚方法。

如果在与 obj 关联的类中或其任何基类中未能找到具有指示名称和签名的非静态方法,则引发 MissingMethodException。此异常通常是在将 Microsoft 中间语言 (MSIL) 指令转换为本机代码时而不是在运行时进行检测。

如果 obj 为空,则引发 NullReferenceException。

如果系统安全机制没有授予调用方对被调用方法的访问权限,则引发 SecurityException。在 CIL 被转换为本机代码时(而不是在运行时),可能进行安全检查。

注意:
对值类型调用 System.Object 的方法时,考虑使用 constrained 前缀和 callvirt 指令。因而不需要根据值类型是否重写方法来发送不同 IL,从而避免了潜在的版本问题。对值类型调用接口方法时考虑使用 constrained 前缀,因为实现接口方法的值类型方法可以用 MethodImpl 更改。这些问题在 Constrained 操作码里有更详细的说明。
 

下面的 Emit 方法重载可以使用 callvirt 操作码:

ILGenerator.Emit(OpCode, MethodInfo)

ILGenerator.EmitCall(OpCode, MethodInfo, Type[])

 

看MSDN,Callvirt是后期绑定,似乎是,此调用可实现多态,即调用基类的virtual方法,如果继承类重写,实际调用时是调用继承类的方法。

 

据此,如果不是一定要调用基类的方法,好像 Callvirt 比 Call 更安全,并能代替 Call。

我是如此想的,也是如此做的。在我的本子上如此使用,测试时没问题,但在服务器上运行,则可能会报错,在其他同事机器上运行,有的机器可正常运行,有的则报错。

 

将报错的动态方法以 Reflector.exe 反编译成 C# 代码,复制到 C# 项目中,编译后,再用 Reflector.exe 查看其 IL 代码,并与动态生成的 IL 比较,发现除了有些调用,C#编译器以 Call 方式调用,而动态代码以 Callvirt 方式调用外,其他没什么差别。

 

最后不得已,将所有调用,都做了类似如下的修改:

 

MethodInfo method = objType.GetMethod(。。。);
if (objType == method.DeclaringType)
 il.Emit(OpCodes.Call, method);
else
 il.Emit(OpCodes.Callvirt, method);

这样处理后,在服务器上运行,终于不再报错。

 

虽然问题解决,但仍有些莫名其妙。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值