CLR上的接口调用也是在运行时检查的

作者: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里去调用接口方法不同。这是因为反射本身就是通过托管代码实现的,也就是说在进到虚拟机底层之前,在托管代码里就已经做了很多检查,这些检查发现所要调用的方法与实际提供的被调用对象不匹配,于是在托管代码的层次上就抛出了异常。

你可能感兴趣的:(.net,Microsoft,VM)