现在,Visual C++ 2005在任何类型上都提供了可以具有析构函数/finalizer的功能,无论这种类型是托管的还是本机的。在它为托管类型的情况下,编译器会将析构函数映射到IDisposable::Dispose方法。这意味着能够用C++语言编写同样的方法,如图3中的第四个代码片断所示,其中,阅读器的析构函数/Dispose方法将会自动被调用,就像在C#中使用“using”语句一样。当在堆栈上创建某一类型时,它的析构函数会在它超出其使用范围时被调用。
托管扩展的一个最大的问题是对指针的使用。指针被用于各种各样的任务,而其情况也是复杂多变的,因而非常难以理解。在某一特定的代码段中要解读自己在和哪一种指针打交道需要有一定程度的天赋。这种复杂性在下一个版本中会被去掉。在Visual C++ 2005中,指针还是原原本本的C++指针。它们指向稳定的对象,而您则可以用指针进行算术操作。指向对象的指针的生命周期必须由开发人员显式管理。当使用指针时,运行库不会负责对指针带来的垃圾进行清理。
现在让我们看一下Visual C++ 2005的设计人员是如何解决这一问题的。与Visual Studio .NET 2003和Visual Studio 2005中使用new运算符返回指针不同,gcnew运算符返回一个“句柄”这是一种新构造,在语法中用“^”符号来表示。该句柄引用托管堆中的整个对象。也就是说,它们不能用来指向类型的内部,而编译器对它们的使用有许多限制,以此来强制执行这种行为,而这也可以帮助开发人员正确并安全地使用句柄。句柄不允许进行指针算术运算,也不可以被强制转换为空指针或是任何整数类型。然而,星号和箭头运算符仍被用来取消对它的引用。
这并不意味着您不能再获得一个指向垃圾回收堆上的指针。与在C#中组合&运算符与固定的关键字相似,在Visual C++ 2005中,pin_ptr抽象类型允许您检索指向托管堆上对象的钉住指针。只要这个指针存在,托管堆中的对象就会被钉住,这可以防止垃圾回收器在回收的过程中移动它。Visual C++ 2005还引入了跟踪引用运算符,用百分号(%)来表示。当在C++中引入本机的&引用运算符时,大多数开发人员都知道可以把它理解成一个指向对象的指针,在使用时是由编译器来自动清除的。在大多数情况下,%对^而言就像&对*一样。
在托管的环境下,将本机引用指向托管对象就像将本机指针指向托管对象一样危险。在指针与引用幕后的基本原理就是:被引用的对象并不会被四处移动。跟踪引用和本机引用很相似,唯一例外的是,跟踪引用引用托管堆上的对象,并且对其进行“跟踪”,即便是它们被垃圾回收器移走。百分号运算符也被用来“提取托管对象的地址”,所以就像&运算符在应用于本机类型时返回指向该对象的指针一样,%运算符在应用于托管引用类型时会返回一个指向该对象的句柄。
一般来说,当C++开发人员知道标准在控制它们的语言时,他们会感到心安理得。由于这个原因,为了促进第三方的采用,并确保语言向前发展的稳定性,这种新的语法采集众长而成为一个称为C++/CLI的提议标准。。在2003年10月,ECMA选举出了一个特别工作组,名为TG5,致力于分析和采用这一标准,就像WG21作为ISO C++的管理团体一样。。实际上,WG21中的关键人物也在TG5中工作。。他们的计划是在2004年年底将其C++/CLI标准化。
互操作选项
在Visual Studio .NET 2003的所有基于。NET框架的语言中,Visual C++ 7.1提供了最好的互操作功能。它具有实现实际的互操作方案所必需的功能,Quake II到。NET框架的移植便是例证,具体细节请访问http://www.vertigosoftware.com/Quake2.htm.Visual C++ 2005进一步扩展了这一功能。
在托管与本机环境中,使用。NET 互操作有四种主要途径:COM 互操作可以使用Runtime Callable Wrappers(RCW)与COM Callable Wrappers(CCW)来实现。。通用语言运行库(CLR)负责类型封送(除非在极少的情况下使用自定义封送拆收器),并且这些调用的开销很大。需要非常小心地尽量避免接口往来过于频繁,否则就会出现很严重的性能问题。还需要保证这些包装一直与其底层的组件保持一致。也就是说,在简单的互操作场景而试图使用大量的本机COM代码时,COM 互操作非常有用。
第二种互操作选择是使用P/Invoke.要达到此目的,可以使用DLLImport属性,并且在方法声明中为想要导入的函数指定属性。封送是按照它在声明中的指定方式来处理的。然而,只有在有代码需要通过DLL导出公开必须的功能时,DLLImport才是有用的。
当需要从本机代码调用托管代码时,CLR宿主也是一种选择。在这种情况下,本机应用程序必须驱动所有的执行:设置主机、绑定到运行库、启动主机、检索适当的应用程序域、设置调用上下文、查找所需的程序集和类,并调用所需类上的操作。在控制发生什么以及何时发生方面,这无疑是最健壮的解决方案之一,但这也会带来让人难以置信的枯燥,并需要许多自定义代码。
第四种选择,也有可能是最简单并最可行的选择,就是使用C++的互操作功能。通过设置/clr开关,编译器会生成中间代码(MSIL)而不是本机代码。唯一被生成为本机代码的是那些无法被编译成中间代码的代码,其中包括带有内联asm块的函数,以及使用像Streaming SIMD Extensions (SSE)这样一些特定于CPU的固有特性的操作。Quake II就是使用/clr开关移植到。NET的。Vertigo软件小组花费了一天的时间将原来由C编写的游戏代码成功地编译成C++代码,然后设置了/clr开关。他们的代码很快就可以运行在。NET框架上。在不添加任何附加的二进制文件而只是简单地加入适当的头文件的情况下,托管C++和本地C++可以相互调用,而无需部分开发人员做一些额外的工作。编译器负责创建适当的)转换代码来往返在两种环境之间。
这给C++开发人员带来了一些问题。问题之一就是现在声名狼籍的混合DLL加载问题,Visual Studio .NET 2002和Visual Studio .NET 2003的用户都受此问题的影响。如果正在运行加载器锁(Loader Lock)内的本机代码并且引用一个还没有加载的程序集中的托管类型,CLR会非常友善地加载这一程序集。它是通过调用LoadLibrary来实现的。当然,LoadLibrary会尝试获得加载器锁,这会碰到死锁问题。开发人员和产品经理如果听说这个问题在即将推出的版本中会得到解决一定非常高兴。
/clr开关对C++开发人员来说是一个极好的工具,但它也有一些缺点。正如本文之前提到的一样,由/clr开关产生的映像既包含本机代码又包含托管代码,这有时会导致问题的出现。首先,这些混合映像并不是遵循CLI的(这意味着,例如,它们将无法在Rotor上运行)。它们有本机的入口点,而当频繁跨越托管边界时会带来极大的转换开销。但最重要的是,这些本机入口点的存在会对使用包括反射在内的程序集的工具带来极大的危害。为了使用反射来检查一个映像,必须首先加载程序集并执行它。只有在所有的初始化都执行完毕时,反射才能检查元数据。遗憾的是,反射无法正确地加载包含有本机入口点的托管程序集。
此外,Visual Studio .NET 2003很少生成可验证的代码,即使它这样做,它花费在处理一些其他重要问题上的时间也会比较多。而中间代码对无法验证的指令有着一流的支持(可以进行指针算术运算,执行间接加载和访问本机堆),可验证的代码能够处理一些需要部分信任的情况,而这又可以支持Visual Studio 2005提供一些丰富功能。ClickOnce部署依赖于部分信任,与SQL Server 2005中的托管代码宿主一样。Visual C++ 2005开发小组的主要目标之一就是让编译器能够在开发人员开发非混合和可验证的映像产品时有所帮助。它们通过引入两个新的编译器开关来实现这一点:/clr:pure和/clr:safe.不过,在深入讲解如何使用这些新开关之前,需要分析一下C++ 互操作的工作原理。
正常运行(It Just Works)
在Visual Studio .NET 2003中,C++ 互操作技术被称为IJW或“正常运行(It Just Works)”。在即将推出的版本中,这被改为一个更具描述性的名称“互操作技术”。那么,它是如何“正常运行”的呢?对于每个由应用程序使用的本机方法而言,编译器同时创建了一个托管的入口点和一个非托管的入口点。它们中的一个是实际的方法实现,而另外一个是转发转化代码,它创建适当的转换并进行任何必要的封送处理。托管入口点几乎总是实际的方法实现,唯一的例外是该方法的代码无法用中间代码表示或者开发人员使用“#pragma unmanaged”编译器指令来强制要求将入口点实现为本机代码。
当使用一个IJW转发转化代码时(例如,当本机入口点是转发转化代码时),编译器提供转化代码的实现,并通过一个偏移量或导入地址表(Import Address Table,IAT)跳转来调入实际的实现。IJW转化代码处理的合理时间大约在50到300个周期之间,不过,精心设计的测试用例可以使这个数字减至10那么小。当转发的转化代码是中间代码时,托管的P/Invoke就会派上用场。P/Invoke仅包含一个声明而没有实际的方法实现;CLR提供了对转化代码的运行时支持的功能。这些转发的转化代码通常都会比同等配置的本地机器实现稍微慢一点点。
如上所述,使用IJW使每个函数都有两个入口点,一个托管的接口和一个非托管的接口。但某些构造需要这些入口点的调用地点在编译时进行填充(例如函数指针和vtable)。而如果编译器在编译时无法知道运行时调用地点的托管状态,则它应该选择哪一个入口点呢?在Visual Studio .NET 2003中,编译器总是会选择非托管入口点。当然,如果调用方确实是托管的,则上述做法就会造成一些麻烦,这称为Double P/Invoke问题。在这种情形下,托管调用对非托管转化代码进行的转换刚好又转换回托管代码,这样的操作会导致几个大的不必要的开销。
Visual C++ 2005提出了几个解决方案。第一个方案就是使用__clrcall关键字,通过这个关键字,可以指定是否基于每个方法发出非托管入口点。使用这个关键字添加函数声明可以防止生成非托管入口点(这样做的一个缺点就是该函数就不能被本机代码直接调用)。__clrcall关键字也可以放置在函数指针上,这样在编译器有所选择的情况下,可以使用托管入口点来填充该指针。Visual C++ 2005提供的第二个解决方案是通过运行库检查来自动消除Double P /Invoke问题,而cookie将帮助运行库确定是否可以跳过非托管的转化程序,从而将调用直接转发至托管入口点。不过,这一功能不可能最终解决问题。
第三个解决方案是纯中间代码。新的/clr:pure编译器选项指示编译器生成一个不包含本机构造的纯托管映像。这样不仅可以产生遵循CLI的程序集来支持部分信任的情况,而且通过防止生成非托管的转化代码解决了Double P/Invoke问题。结果是,每个函数只有一个入口点(托管入口点),这样,虚表(vtable)和函数指针就决不会使用非托管入口点进行填充了。
然而,仅仅因为代码是遵循CLI的并不意味着它就是可验证的,而这对于支持低信任级别的情况(例如当从文件共享加载代码时)是一个重要的目标。所以,Microsoft引入了一个更为严格的编译器选项,称为/clr:safe.对于C++开发人员来说,这是可验证性的圣杯。使用这个开关会使编译器确保生成的程序集是完全可验证的;任何无法验证的结构都会产生编译时错误。例如,试图将一个整型指针编译成一个变量将会产生这样的错误:“int* = this type is not verifiable”,并指出包含非法结构的行。在一些情况下,走向这个极端是适当的。例如,将要作为SQL Server 2005中的存储过程运行的所有托管C++代码都应该使用此选项进行编译。
数据及代码的托管与非托管环境中,不包含任何/clr选项将导致生成完全的本机映像。使用/clr选项将会产生可以包含托管与非托管代码和数据的混合映像。通过使用/clr:pure选项生成的纯中间代码不会包含任何非托管代码,尽管这仍不能保证是可验证的,而且可以包含本机类型。安全中间代码是可验证性的最终目标(只针对。NET框架而言)。简而言之,这两种新的编译模式都将使以前不可能实现或者难以实现的多种情况变为现实。