作者:RednaxelaFX
主页:
http://rednaxelafx.iteye.com
日期:2009-06-02
系列笔记:
JVM在校验阶段不检查接口的实现状况
为什么JVM与CLR都不对接口方法调用做静态校验?
刚才的一帖,
JVM在校验阶段不检查接口的实现状况,我提到JVM在处理invokeinterface时,如果遇到被调用对象没有实现指定的接口时,在运行时抛出异常的行为。那么.NET的CLR在这方面的行为又如何呢?
CLR对MSIL的校验是与JIT同时进行的。JIT编译器一边检查代码的正确性,一边生成native code;一个方法被调用时,如果还没有JIT过的话,要在JIT之后才进入运行状态。于是CLR执行托管方法可以分为一个[加载-链接-校验-JIT]的阶段与一个[执行]的阶段。与JVM一样,如果CLR遇到被调用对象没有实现指定的接口的状况,也是在运行时抛出异常的。
要观察这个行为也同样需要对MSIL做些操作。先用这个源码来编译得到exe:
TestInterfaceCall.cs
using System;
static class TestInterfaceCall {
static void Main(string[] args) {
IFoo f = new FooImpl();
f.Method();
Bar b = new Bar();
((IFoo)b).Method(); // << watch this
}
}
public interface IFoo {
void Method();
}
public class FooImpl : IFoo {
public virtual void Method() {
Console.WriteLine("FooImpl.Method()");
}
}
public class Bar {
public virtual void AnotherMethod() {
Console.WriteLine("Bar.AnotherMethod()");
}
}
然后利用ILDASM将其反编译为MSIL:
TestInterfaceCall.il
// Microsoft (R) .NET Framework IL Disassembler. Version 3.5.30729.1
// Copyright (c) Microsoft Corporation. All rights reserved.
// Metadata version: v2.0.50727
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 2:0:0:0
}
.assembly TestInterfaceCall
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 )
.custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // ....T..WrapNonEx
63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows.
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module TestInterfaceCall.exe
// MVID: {075D6351-0AF0-459F-A822-40E817B58699}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// Image base: 0x03010000
// =============== CLASS MEMBERS DECLARATION ===================
.class private abstract auto ansi sealed beforefieldinit TestInterfaceCall
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 33 (0x21)
.maxstack 1
.locals init (class IFoo V_0,
class Bar V_1)
IL_0000: nop
IL_0001: newobj instance void FooImpl::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void IFoo::Method()
IL_000d: nop
IL_000e: newobj instance void Bar::.ctor()
IL_0013: stloc.1
IL_0014: ldloc.1
IL_0015: castclass IFoo
IL_001a: callvirt instance void IFoo::Method()
IL_001f: nop
IL_0020: ret
} // end of method TestInterfaceCall::Main
} // end of class TestInterfaceCall
.class interface public abstract auto ansi IFoo
{
.method public hidebysig newslot abstract virtual
instance void Method() cil managed
{
} // end of method IFoo::Method
} // end of class IFoo
.class public auto ansi beforefieldinit FooImpl
extends [mscorlib]System.Object
implements IFoo
{
.method public hidebysig newslot virtual
instance void Method() cil managed
{
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "FooImpl.Method()"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method FooImpl::Method
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method FooImpl::.ctor
} // end of class FooImpl
.class public auto ansi beforefieldinit Bar
extends [mscorlib]System.Object
{
.method public hidebysig newslot virtual
instance void AnotherMethod() cil managed
{
// Code size 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Bar.AnotherMethod()"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Bar::AnotherMethod
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method Bar::.ctor
} // end of class Bar
// =============================================================
// *********** DISASSEMBLY COMPLETE ***********************
// WARNING: Created Win32 resource file D:\experiment\test\TestInterfaceCall.res
与前一帖一样,这里我们要避免castclass指令干扰测试的结果。把Main()里的IL_0015那行注释掉,然后再用ILASM重新编译为TestInterfaceCall.exe。
运行结果如下:
D:\experiment\test>TestInterfaceCall.exe
FooImpl.Method()
Unhandled Exception: System.EntryPointNotFoundException: Entry point was not found.
at IFoo.Method()
at TestInterfaceCall.Main(String[] args)
可以看到,这一点上CLR与JVM的行为一致。
===================================================================
CLR 2.0之前是用全局的接口查找表来做接口方法调用的分发(dispatch)的。到2.0之后,CLR改用基于stub的方式来做接口方法调用分发。对接口方法的调用点,JIT一开始会生成一个间接调用指向一个lookup stub,后者又指向一个resolver stub,用于查找实际的方法实现;对同一个被调用对象类型成功调用2次之后会生成特化于那个类型的dispatch stub(是一个monomorphic inline cache),然后记录该stub调用失败的次数,达到100次之后就退化回到非特化的resolver stub。
如果被调用对象没有实现指定的接口,则在调用点初次被调用时,进到resolver stub之后会发现找不到接口方法的具体实现,然后就抛出异常。
===================================================================
值得注意的是,这两帖里提到的“接口方法调用”都是指正常的、直接的调用,而不是通过反射去做的调用。如果把这帖里的代码例子中第9行的((IFoo)b).Method();改为反射调用:
typeof(IFoo).GetMethod("Method").Invoke(b, new object[0]);
则编译和校验都不会出现问题,而运行时抛出的异常会是这样的:
Unhandled Exception: System.Reflection.TargetException: Object does not match target type.
at System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at TestInterfaceCall.Main(String[] args)
与直接在IL里去调用接口方法不同。这是因为反射本身就是通过托管代码实现的,也就是说在进到虚拟机底层之前,在托管代码里就已经做了很多检查,这些检查发现所要调用的方法与实际提供的被调用对象不匹配,于是在托管代码的层次上就抛出了异常。