It Just Works
在Visual Studio .NET 2003,C++的interop技术叫做IJW或者“It Just Works”,在新版本中,已换成了更贴切的“Interop技术”。那它的工作原理是怎样的呢?对程序中的每一个本地方法,编译器同时生成一个托管和非托管进入点,它们中只有一个是真正方法的实现,另一个则是转发器,可进行相应的转换和必要的调度。托管进入点通常是真正方法的实现,除非代码不能解释为MSIL或开发者使用“#pragma unmanaged”强制指定进入点的实现为本地机器码。
当一个IJW转发器起作用时——例如转发到本地代码中,编译器提供转换的实现,并且通过偏移或IAT(Import Address Table)调用实际的实现代码;虽然针对特别开发的示例程序,转发器的一次调用所耗费的时钟周期可降到10,但通常来说,会在50至300个时钟周期之间。当转发器是MSIL时,将使用托管的P/Invoke,因为P/Invoke是由一系列声明和非真正代码实现组成的,所以在运行时,由CLR对转换提供支持;相对于本地代码,同样的转发器实现只是稍稍慢一点。
正如前面所提到的,对于每个函数,IJW都有两个进入点,托管和非托管,但它们的结构决定了,对进入点的调用,在编译时就要确定好(例如指针和vtable)。编译器在编译时,可不知道运行时的调用点托管状态,那它会选择哪一个进入点呢?在Visual Studio .NET 2003中,编译器始终会选择非托管的进入点,这样在调用者是托管代码时,这会产生一个问题,我们称其为“Double P/Invoke”,如插4所示。在这种情况下,非托管转换器对托管调用的转换,结果又被转换回到了托管代码中。
图2:Double P/Invoke问题
对此,Visual C++ 2005提供了一些解决方案,首先,是关键字__clrcall,它可以指定是否发布非托管进入点,在函数声明时,加上此关键字可防止产生非托管进入点(不利之处在于此函数以后都不能从本地代码中直接调用了);同时,关键字__clrcall也能加在函数指针前。其次,是由Visual C++ 2005提供的Double P/Invoke自动消除,通过运行时检查和cookie,并由运行时决定是否可以跳过非托管转换器,而直接把调用转发到托管进入点中。
最后一个解决方案就是纯MSIL了,新的编译器选项/clr:pure,可使编译器生成一个不含本地结构的纯托管映像。实际上,它不但可生成遵从CLI标准的程序集,而且通过防止产生非托管转换很好地解决了Double P/Invoke问题。这样,对每一个函数,只有一个进入点——托管进入点,因为没有了非托管进入点,vtable和函数指针也就不存在了。
虽然代码遵从CLI标准,但不一定意味着它是可验证的,而在例如文件共享等那些低度可信赖环境中,可验证性是非常重要的。为此,微软引入了一个更严格的编译器选项/clr:safe,这对那些需要可验证性代码的C++开发者来说,无疑是雪中送炭。此选项可使编译器生成绝对可验证的程序集,而任何不可验证的程序代码结构,都会导致一个编译时错误。例如,试图编译一个整型指针的变量将会导致错误“int* = this type is not verifiable”,并会指出是在代码中哪一行。而SQL Server 2005存储例程里的所有托管C++代码,都必须用此选项编译。
图3:编译模式
图3图示了托管和非托管的数据及代码结构,它们由不同的编译器选项生成。如果不包含任何/clr选项,将生成本地映像;使用/clr将生成包含托管和非托管代码及数据的混合映像。而纯MSIL,是通过/clr:pure选项生成的,它不会包含任何非托管代码,虽然在可验证及包含本地数据类型方面并不提供保证。在基于 .NET Framework的平台上,安全MSIL是可验证性代码的理想选择;简单地说,这两个新的编译模式可应用于不同的情况,甚至可以完成在以前看来是不可能或很难完成的任务。