一次Reboot=975年
在开始本文之前先做一个小调查:有多少朋友喜欢Reboot?答案A:我喜欢,每天都要Reboot N次。答案B:我不喜欢,最好一个星期都不要Reboot。如果没有意外的话,相信大部分人都可以归到B那一类,毕竟很少人会愿意中断自己所做的事情,把正在编辑的文章、代码等等一一存盘,然后重新启动,等待几分钟,再重新打开文档、代码编辑的。
不过,很遗憾的是,新版本的.NET的安装在Vista以及以上(以后用Vista+表示)操作系统中安装通常是需要重新启动的,2003,XP或者以下则是不需要的。看到这里可能有些朋友会很奇怪:为什么XP不用重启,Vista反而要重启呢?这不是变差了吗?谈这个问题,必须要从Mscoree.dll谈起。大部分使用.NET编程的朋友可能并不熟悉mscoree.dll,但是,实际上mscoree.dll可以说是.NET/CLR中最为核心的一个部件之一,没有Mscoree,CLR根本无从加载。当你在执行一个.NET得EXE的时候,最先加载的不是CLR的DLL(mscorwks.dll),而是这个Mscoree.dll。详情可以参见我之前写过的一篇文章: .NET中的幕后英雄:MSCOREE.DLL
因为Mscoree.dll是.NET一个核心的组成部分,可想而知,每次在安装新版本的.NET的时候,Mscoree.dll都会被更新,以加入新功能或者Bug Fix。更新的时候所面临的主要问题是:如果Mscoree.dll正在被使用,如何更新mscoree.dll?在Vista之前,也就是2003,XP,乃至之前的操作系统中,安装程序(Admin也可以)可以直接重命名mscoree.dll(这个操作即使mscoree.dll正在被使用也可以成功),然后再copy新的mscoree.dll到系统目录即可。但是,在Windows Vista+,Mscoree.dll被认为是Windows的核心组成部分之一,并且由WRP(Windows Resource Protection)所保护。如果要更新,必须得通过一种叫做CBS(Component Based Servicing)的机制加以更新(注:我对这些安装相关的技术并不太熟悉,如果细节描述上有错误欢迎指正)。这种机制虽然更加安全、健壮,但是也多了一项限制:如果Mscoree.dll正在被使用,系统必须重启才可以完成对Mscoree.dll的更新。换句话说,在Windows Vista+,安装3.5,4.0,5.0,6.0,…的.NET每一次都得要重启。比如有1亿用户安装了NET 4.0而需要重启,假定重启一次需要5分钟(包括存盘等准备工作),那么这个重启动作就浪费了所有人500000000分钟=975年!!!如果考虑到每个.NET版本的安装都要这么来一次,那么总的时间浪费可以说是天文数字!这也许有点危言耸听,但是无论如何,有一件事情是清晰的:对于像.NET这种用户群比较广泛的程序平台而言(注意我说的是.NET,不是SDK也不是VS,.NET是一个运行时、虚拟机,是最终用户所运行程序使用的),任何一个微小的细节可能都有着巨大的影响。虽然.NET用户群尚未达到这种规模,我们也不希望因为这个问题影响.NET的普及率。
.NET 4.0的解决方案
我们的目标是希望减少重启的次数。这里谈到的重启是为了修改MSCOREE.dll而导致的重启。为了解决这个问题,我们可以先从DLL的本质入手。DLL是Windows的一种代码共享机制,这种共享是通过DLL输出函数、然后其他EXE/DLL来直接调用DLL所输出的函数来实现的。换句话说,一个DLL对于外部而言,最重要的是这个DLL的输出函数,包括其接口和功能。每次对DLL的修改,无非是接口的变化和功能的变化。接口的变化由于兼容性的要求,一般不会减少或者修改,而是增加。功能上,通常变化不会太大,一般是Bug Fix,同样,也是因为兼容性的要求。当然了,新的接口的新功能除外。那么有没有一种方法,可以自由修改MSCOREE.dll的功能和接口,而又不需要修改MSCOREE.dll的内容呢?听上去这似乎是不可能完成的任务,但是仔细思考一下,不难意识到:如果我们将MSCOREE.DLL的代码重新定位到另外一个DLL,这个DLL不受Windows约束,可以自由修改,那么问题是不是就解决了呢?答案是肯定的,这就是MSCOREEI.dll的由来。当然了,具体的细节要比这里讨论的复杂不少,这一次我就谈到这里,欢迎各位朋友参与讨论MSCOREE和MSCOREEI是如何共同作用的。因为现在这个相关项目还没有完全结束,现在暂时我还不能完全透露所有细节,在合适的时间内,我将发布本次文章的续集,并为大家公布答案。
CLR小组中存在着大量的回归测试,这些回归测试会定期执行来发现CLR中的Bug,Developer在Checkin之前,也需要执行这些测试的一部分(大概是10小时左右,如果全部跑的话估计要好几天)。这些测试对于保证CLR的质量是至关重要的。有时候,这些测试会偶尔失败,比如跑100次失败大概一到两次,有些极端的例子甚至是10000次才失败一次。像这种问题通常是很难调试的。在前面调试Bug的神兵利器:通过WinDbg条件断点收集Log这篇文章中,我讲到了如何通过条件断点收集各种信息来判断Bug究竟出在哪里。但是,这个方法还是不太管用,因为它不能够反复执行某个程序。下面我要讲一种技巧可以用来调试类似这样的问题,这种技巧主要适用于下面几种情况:
- 在程序出错的时候,某些信息、状态已经丢失,无法通过当前出错时候的状态推断出之前的状态。说的稍微具体一点就是,比如某个变量变成了NULL导致Access Violation,但是很难直接推断出为什么这个变量变成了NULL
- 程序运行时间较长,很难直接单步调试
- 程序较难修改加入打印代码(比如加入新代码并编译非常花时间,或者该程序没有源代码
- 该程序运行次数较多的时候才能发现问题,也就是说问题不是每次都出现
#2和#4决定了一步步调试基本上是不可能的。#1和#3则意味着我们必须得使用条件断点来收集信息来判断代码的错误,因为直接调试出错的位置是不可行的。下面了我来讲一下如何用CDB(其实就是WinDbg的无UI版本,WinDbg=CDB+UI)来做到:
- 反复执行程序
- 当程序出错的时候自动暂停
- 通过条件断点收集信息,只保留出错时候的那一次Log
我们先假设我们需要调试的程序叫做Hello.exe,每次出问题的现象是,调用某个函数Hello!Func()的时候,其参数arg为NULL。Arg这个变量是由某个全局变量g_arg传入而来。我们可以通过硬件的数据断点来查看每次将g_arg赋值为NULL的情况(当然了,赋值为NULL并不代表是错误,只有传入Hello!Func的时候为NULL才是错误)。程序一般要跑10000次才可能发现问题。使用下面的命令行可以做到反复收集Func1(Func2、Func3因为类似,这里就不列出了)执行时候的g_arg的值并放入Log文件中,并且如果发现调用Hello!Func的时候arg参数为NULL,则停止程序:
for /L %i in (1, 1, 10000) DO CDB.exe -c "bu Hello!Func \".echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g } \"; ba w4 Hello!g_arg \“.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }\”; g" -G -logo debug.log Hello.exe
我们来简单分析一下:
- 一开头的For语句用于执行CDB命令10000次,也就是调试Hello.exe一万次
- -c命令指定让CDB在程序开始的时候执行下面的命令
- bu Hello!Func “.echo Inside Hello!Func; dv; .if (poi(arg)!=0) { g }意思是每次Hello!Func被执行的时候,打印Inside Hello!Func,之后打印所有局部变量和参数(包括arg),如果发现arg!=NULL,则继续。注意上面命令中的\”是转义符,代表真正的引号,避免冲突。
- ba w4 Hello!g_arg “.if (poi(Hello!g_arg)==0) { .echo g_arg changes to NULL; kb; }”意思是每次如果g_arg被修改成NULL,打印出Callstack
- g命令表示让程序开始执行
- -G:表示让CDB忽略程序结束的时候的Breakpoint,避免CDB在运行结束的时候停下,保证CDB可以持续执行不需要人工干预
- -logo debug.log:表示让CDB把每次输出的结果放入Debug.log中,并且每次都新建立文件,也就是说,会把上一次覆盖。这正好是我们需要的,因为我们设置了一旦程序错误则停止,那么这一次的Debug.log才是需要保留的
除了用-c指定初始的命令之外,也可以使用-cf来指定一个文件包含任意条CDB命令,如果CDB命令较多,可以采用这种方法。
本文说道的方法是比较有效的,我自己曾经使用过这种方法解决过不少比较棘手的问题。如果碰到了此种需要运行10000次才能重现问题的Bug,不妨试一下本文的方法。
我们先来看一个在Outlook上运行.NET插件的一个情景。暂时机器上面安装的是CLR v1.1,Outlook上运行了一个Addin,在v1.1上编写和测试完毕,运行良好。之后,用户在机器上面安装v2.0。因为Outlook采取的方式是总是启动最新的.NET Framework(这也是有原因的,因为Outlook希望能够运行所有的版本的.NET Addin),Outlook自动会运行CLR v2.0(包括.NET Framework v2.0,v3.0, v3.5)。因为v2.0和v1.1之间并不是100%兼容,v1.1上编写的Addin在v2.0的CLR将有可能无法正确执行。也就是说,安装了一个新版本的.NET Framework可能会导致类似Outlook这样的支持插件的应用程序上的旧插件无法正确工作!
如果我们来看一下类似Outlook这样基于插件(Plugin或者Addin)的程序而言,选择CLR的版本大概有这么几种方式:
1. 总是最新:如上所述,总是选取最新的CLR加载存在兼容性问题。
2.总是坚持加载某个固定的版本:比如v1.1或者v2.0:如果总是固定某个版本,那么基于另外的CLR的版本的Addin将很难正常运行,要么是因为基于v1.1的CLR的Addin在v2.0 CLR上因为兼容性问题磕磕碰碰,要么是基于v2.0 CLR的Addin根本无法在v1.1 CLR上运行。
3. 加载Addin,第一个Addin所加载的CLR将是这个进程中的唯一CLR(注意目前CLR v1.X、v2.0不支持在一个进程中加载多个版本的CLR):先不提这种方法对于加载的CLR版本有一些随机性,不管是第一个Addin是v2.0还是v1.1,最终结果和上面几种方法并无出入。
可见,在目前的.NET/CLR的架构下,对于这种基于插件的应用程序运行多个基于不同CLR版本的插件并没有很好的解决方案。结果是,用户选择安装新版本的.NET可能会影响已有的程序。显然这在一定程度上将会影响到人们使用新版.NET的积极性,甚至导致拒绝升级到最新版本,显然,CLR开发小组是不愿意看到这种事情发生的。解决这个问题大致有两类思路:
1. 保持100%兼容,vN总是可以完美运行在vM上(M>N)
2. 承认100%兼容是不可能完成的任务,反之,允许多个不同版本的CLR共同执行
显然,方法一是完全不可行的,原因很简单,开发过应用程序平台的朋友们都知道,新版本的平台和旧版本的平台总是会由于各种原因不兼容。一些常见的原因有:
1. 旧的API被新的API所取代,旧API无法在新版本中使用。虽然常见的情况是新API和旧API并存,不过一旦并存了若干的版本之后,包袱总有被丢弃的一天。
2. 已有的API行为因为有若干缺陷,必须修改其行为。这种情况比较少见,通常的方法是加一个新的API,但是这种情况还是客观存在的。
3. 用户程序依赖于一些未定义行为,而这些未定义行为在新版本中有所改变(比如一个API的Bug,一个实现细节,或者CLR DLL的名字,等等)
4. 新的版本中有Bug,导致已有API行为改变
5. 使用某个固定的版本号
等等。因此,CLR采取的是第二个思路:支持多个不同版本的CLR互不干扰的共同执行,也就是Side By Side。注意,这里的Side By Side是一个很广义的词汇,它所指的是不同的CLR彼此之间互不干扰。这里的互不干扰也是有好几种层次的:
1. Out-Of-Process Side By Side:机器上可以安装不同版本的CLR,每个进程可以运行不同版本的CLR,互相之间互不干扰,共享机器范围的资源(如磁盘,注册表等)。目前v1.X、v2.0实现了这个功能。
2. In-Process Side By Side:同一个进程内可以运行多个CLR,每个CLR实例互不干扰,把对方看成本机代码。这里又分为几个层次:
a. 不同版本的CLR可以在同一个进程内加载,不允许同一个版本CLR加载多次
b. 允许加载同一个版本的CLR多次,彼此之间互不影响
可以看到,如果CLR可以支持在同一个进程中加载不同版本的CLR,也就是支持2.a,那么前面所提到的那个问题也就迎刃而解:v1.1的Addin运行在v1.1上,v2.0的Addin运行在v2.0上,顿时两个Addin便可以同时运行,互不干扰了!
幸运的是,CLR开发小组已经注意到了这个问题,并且在v4.0的CLR中实现了多个不同版本CLR的In-Process SxS,简称In-Proc SxS(也就是上面2.a所提到的内容)。下面本文将详细介绍v4.0中In-Proc SxS功能。
V4.0的In-Proc SxS简介
在v4.0中CLR支持下列情况的In-Proc SxS:
1. v2.0和v4.0共存
2. v1.1和v4.0共存
而V1.1和V2.0则是不能够被同时加载到进程中。也就是说,进程中<4.0的CLR只能存在一个实例,这样做的原因非常简单:<4.0的CLR版本本身是不支持In-Proc SxS的,也就是说v1.1和v2.0一旦在同一个进程内加载是会出现各种各样的问题的。并且,我们不希望因为要支持SxS而去修改v1.1和v2.0,这样做的代价太大,同时也会把整个问题域变得更加复杂,因此最后决定不支持<4.0的CLR多于一个实例。当然了,>=4.0的CLR是可以多个并存的,也就是说V4.0,V5.0,v6.0,等等,都是可以和平共处在同一个进程内。原因很简单,>4.0的CLR是In-Proc SxS Aware的。
前面提到过,总是加载最新版本的CLR这种方式是存在问题的,因为新版本不可能完全兼容旧版本,因此,保持兼容性的最佳方式是不允许“加载最新”(Bind to latest)这种方式存在,换句话说,为v4写的程序缺省应该总是在v4上运行,而不应该自动被“提升”至V5上运行。
因为<4.0的CLR是不支持In-Proc SxS的,因此为了让这些CLR和新的V4和平共处,并且行为不变,必须满足下面几条:
1. 老程序的行为必须和原来保持一致,这包括已有程序的加载和已有的Hosting API
2. <v4.0的CLR看不到>=v4.0的CLR,因为它们生活在两个不同的世界中
3. 已有的Hosting API只允许加载一个<v4.0的CLR,并且无法加载v4.0及以上的CLR
可以看到,已有的HostingAPI因为没有设计成支持In-Proc SxS,在v4.0的时候会面临淘汰。而在v4.0的时候,v4.0的CLR(其实严格来说是Shim,也就是mscoree.dll)必须得有一套新的API。
CLR所做的修改
从v2.0到v4.0,从不支持In-Proc SxS到支持In-Proc SxS,CLR做出了不少的修改,这里面有不少的挑战。其中一个比较明显的修改是CLR的实现原来位于mscorwks.dll,现在被修改成了CLR.dll,同时JIT的实现原来是mscorjit,现在则是clrjit。原因非常简单,为了让已有的v1.1和v2.0的代码看不到V4的存在,避免v2.0的DLL把v4.0的CLR误认为是V2的。如果不做名字修改,已有的v2.0的代码很有可能仍然可以找到v4.0,因为内部的很多代码都是需要查找mscorwks.dll的。如果找不到这个DLL自然就找不到CLR了。
除此之外,CLR的代码也做出了不少的改变,比较主要的有:
1. 修改对全局的共享资源的使用。比如原来总是用一个固定名字的Mutex或者和进程名字相关的临时文件,现在这些代码必须得要修改了,要和该CLR的实例绑定起来(比如和首地址)。
2. 修改对于其他CLR的DLL的加载和查找。以前也许可以写FindModule(“mscorwks.dll”),现在不能这么写了,而是通过其他方法来查找(比如注册表)。
3. 对于版本号的一些假设。原来可以直接处理任何版本的代码,现在也许需要分情况处理<v4.0和>=v4.0。
4. 对于旧的Hosting API,修改其实现使之无法加载v4.0的CLR,但是又可以和v4的CLR共处而不会出问题
5. 增加新的API,支持In-Proc SxS
6. Activation,也就是CLR的启动的Logic基本上重写,为了处理v1.1、v2.0、v4.0之间的各种不同的SxS或者非SxS的情况。
本文因为不是剖析v4.0中SxS实现的文章,对于CLR本身的修改也就到这里点到为止。不过,如果你的程序也有类似的问题,那么你的程序可能也要修改才可以支持SxS了。
Activation Policies
这里所说的Activation Policies,指的是加载CLR的一些规则,知道了这些规则,才可以很好的在v4.0的CLR下使用SxS。这里所需要讨论的Activation被分成三种不同的情况:
Application Activation
这里说的Application Activation就是普通的执行一个EXE程序。规则最简单来说是这样:
1. >= 4.0的EXE总是运行在EXE所被编译的CLR版本上
2. <4.0的EXE优先运行在被编译的CLR版本上,如果此版本不存在,则运行最新的小于V4.0版本
我们来看几个例子:
| EXE被编译的CLR版本号 | 机器上安装有CLR 1.1? | 机器上安装有CLR 2.0? | 机器上安装有CLR 4.0? | 结果 |
| 1.1 | 是 | 无所谓 | 无所谓 | 加载CLR 1.1 |
| 2.0 | 无所谓 | 是 | 无所谓 | 加载CLR 2.0 |
| 1.1 | 否 | 是 | 无所谓 | 加载CLR 2.0 |
| 1.1 | 否 | 否 | 是 | 失败 |
| 2.0 | 无所谓 | 否 | 是 | 失败 |
怎么看一个EXE被编译的CLR版本号?很简单,使用CorFlags就可以了:
C:\Windows\Microsoft.NET\Framework\v2.0.50727>corflags regasm.exe
Microsoft (R) .NET Framework CorFlags Conversion Tool. Version 4.0.20818.0
Copyright (c) Microsoft Corporation. All rights reserved.
Version : v2.0.50727
CLR Header: 2.5
PE : PE32
CorFlags : 11
ILONLY : 1
32BIT : 1
Signed : 1
COM Activation
COM Activation指的是本地代码创建一个基于托管代码的COM对象,也就是通常所说的CCW。在最新的CLR V4中,所有的托管的COM对象都必须绑定到它所被编译的CLR版本,除非:
1. 注册表中SupportedRuntimeVersions中有大于该版本的版本号(注意RuntimeVersion这个只是用来做一个很简单的检查,如果当前的最新CLR版本小于这个值则出错。并不代表说一定要在这个特定的CLR版本中加载)
2. 如果进程中已经加载了一个<=2.0的CLR版本,并且该托管对象对应的CLR版本也是<=2.0,那么该托管对象则会自动在该已经被加载的CLR版本中加载
V2 Hosting Activation
这里指的是V2及以前的Hosting API,包括Mscoree.dll的大部分Export函数以及支持的一系列基于COM的Hosting接口(严格来说只是类似COM)。规则很简单,保持V2的行为不变,无视V4的存在,也就是说:
1. CorBindToRuntime(NULL)无法加载v4及以上
2. CorBindToRuntime(v4.XXXXX)会失败
这符合之前对于V2 Hosting API的说法,只有新的API才可以支持V4的加载。
注意:上面几种Activation方法,都可以通过Config文件控制。如果Config文件中存在useLegacyV2RuntimeActivationPolicy并且其值为TRUE的话,恢复原来的V2的行为,也就是如果对应的CLR版本不存在,允许绑定到更新的>=v4.0的版本。
New Hosting API简介
前面提到过CLR V4有一套新的Hosting API。其实说是CLR V4,倒不如说是Shim(mscoree.dll) V4所提供的API。显然CLR不能启动它自己,因此需要Shim来代劳。已有的CorBindToRuntime这一套API基本上都被认为是“Deprecated”。新的一套API采用的是类似COM的接口方式,从一个新的API CreateInterface获得。比较重要的新接口有:
1. ICLRMetaHost:用于绑定某个版本的CLR,列举所有的CLR,等等,取代了原来的CorBindToRuntime
2. ICLRRuntimeInfo:代表某个特定版本的CLR,如V2.0.50727。可以查询其状态,目录,版本号,等等
3. ICLRMetaHostPolicy:代表绑定某个版本CLR的相关的策略,基于策略、托管程序集、版本号、配置文件等做出策略决定。注意该接口不负责加载CLR,而只是返回一个预计的CLR版本作为结果。
其中,ICLRMetaHost和ICLRRuntimeInfo可以说是In-Proc SxS新API的核心,因为这两个接口的定义方式决定了它们支持工作在不同多个CLR版本上,而不像已有的API总是假定当前只有一个CLR版本。下面举一个简单的例子:
1: #include <stdio.h>
2: #include <corerror.h>
3: #include <metahost.h>
4: #include <windows.h>
5: #include <atlbase.h>
6:
7: #define IfFailReturnHr(msg) if (FAILED(hr)) { wprintf(L"%s (hr=0x%x)\n", msg, hr); return hr;}
8:
9: HRESULT LoadAddin(LPCWSTR lpwszVersion, LPCWSTR lpwszAddinTypeName)
10: {
11: wprintf(L"Getting runtime host interface for %s\n", lpwszVersion);
12:
13: CComPtr<ICLRMetaHost> pMH = NULL;
14: HRESULT hr = CreateInterface(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMH));
15: IfFailReturnHr(L"Create Instance for ICLRMetaHost failed.");
16:
17: CComPtr<ICLRRuntimeInfo> pRuntime = NULL;
18: hr = pMH->GetRuntime(lpwszVersion, IID_PPV_ARGS(&pRuntime));
19: IfFailReturnHr(L"GetRuntime failed.");
20:
21: DWORD cchDir = MAX_PATH;
22: WCHAR wszDir[MAX_PATH];
23: hr = pRuntime->GetRuntimeDirectory(wszDir, &cchDir);
24: IfFailReturnHr(L"GetRuntimeDirectory failed.");
25:
26: wprintf(L"Runtime directory=%s\n", wszDir);
27:
28: CComPtr<ICLRRuntimeHost3> pHost;
29: hr = pRuntime->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pHost));
30: IfFailReturnHr(L"GetRuntime failed.");
31:
32: CComPtr<IUnknown> pAddin;
33: DWORD dwRet = 0;
34: hr = pHost->CreateManagedObject(lpwszAddinTypeName, IID_PPV_ARGS(&pAddin));
35: IfFailReturnHr(L"CreateManagedObject.");
36:
37: return S_OK;
38: }
39:
40: int _cdecl wmain(int argc, __in_ecount(argc) WCHAR **argv)
41: {
42: LoadAddin(L"v2.0.50727", L"AddinV2, AddinV2");
43: LoadAddin(L"v4.0.20506", L"AddinV4, AddinV4");
44: }
45:
下面我们来简单看一下这段代码中最核心的LoadAddin函数的实现:
1. 首先,调用CreateInterface获得ICLRMetaHost接口
2. 之后,从ICLRMetaHost接口获得v4.0.20506/v2.0.50727对应的ICLRRuntimeInfo接口
3. 调用ICLRRuntimeInfo::GetRuntimeDirectory获得CLR所存在的目录。这里没有实际意义,只是为演示之用
4. 调用GetInterface获得该Runtime对应的ICLRRuntimeHost3接口并返回
5. 调用ICLRRuntimeHost3::CreateManagedObject来创建Addin。这个过程中对应的CLR版本会自动启动。(这里Beta1有一个小Bug:如果调用ICLRRuntimeHost::Start方法,2.0中会返回E_NOTIMPL。这个Bug在Beta2中已经被修好了)
大家从这里可以看到,因为ICLRRuntimeInfo以及通过调用GetInterface获得的接口总是对应着某个特定的CLR版本如v2.0或者v4.0等,这套API便支持了In-Proc SxS。新的基于插件的应用程序如果想应用In-Proc SxS,也应该使用类似方法采用这一套API来启动其插件。
运行该程序其结果如下:
Getting runtime host interface for v2.0.50727
Runtime directory=C:\Windows\Microsoft.NET\Framework\v2.0.50727\
Addin V2: I'm running in CLR v2.0.50727
Getting runtime host interface for v4.0.20506
Runtime directory=C:\Windows\Microsoft.NET\Framework\v4.0.20506\
Addin V4: I'm running in CLR v4.0.20506
结束语
相信看到这里,大家对In-Proc SxS应该有一个清晰的认识了。CLR v4.0为了支持In-Proc SxS,支持Non-Impactful Install方面,做了不少工作,这可以说是CLR自V2版本中Generics被引入以来的最大的一个改动。这一切都是为了V4版本的CLR可以最大限度的兼容已有版本,保护用户现有的.NET应用程序不受影响,从而让用户可以放心的采用.NET平台开发程序,而不用过于担心兼容性方面的风险。
什么是契约
我们先来看一个很简单的例子:
Void WordList.Insert(string word)
这个函数负责将word以升序插入到WordList中的单词列表中,word不可以为NULL。
上面这些说明文字都是用来描述此函数的行为的。当使用该函数的调用者看到这些说明文字的时候,便知道函数应该如何调用以及在不同情况下的函数行为,换言之,上面这段说明文字简单的描述了函数调用者和被调用者的一种约定,这种约定也被称之为契约(Contracts)。契约一般来讲可以分成三类,包括:
1. Precondition:函数调用之前需要满足何种条件:比如,参数word不可以为NULL
2. Postcondition:函数调用之后需要满足何种条件:比如,参数word被加入到WordList的成员m_wordList中,m_wordList元素个数+1
3. Invariant:函数调用之前之后总是需要满足的条件是什么:比如,m_wordList中的单词总是以升序排列
契约式设计这个概念是Bertrand Meyer提出的,并在Eiffel Programming Language这本书中有详细的描述,Eiffel语言本身对契约式设计支持也非常好,有兴趣的朋友可以尝试并比较一下。
.NET 4.0中的Contracts
在.NET 4.0中引入了对契约式设计的支持,我们来看一下,如果上面那个例子用4.0中的契约式设计功能应该如何编写:
1: public void WordList.Insert(string word)
2: {
3: CodeContract.Requires(word != null);
4: CodeContract.Ensures(CodeContract.OldValue<int>(_words.Count) + 1 == _words.Count);
5: CodeContract.EnsuresOnThrow<ApplicationException>(CodeContract.OldValue<int>(_words.Count) == _words.Count);
6: …
7: }
8:
其中:
1. Contract.Requires是Precondition
2. Contract.Ensures是PostCondition
3. Contract.EnsuresOnThrow是Postcondition,和Ensures的区别是这是在Throw的情况下需要满足的Postcondition
可以看到,Contracts在被显式的放在代码当中,而不仅仅是说明性的文字,那这样有什么好处呢?
1. 提供了运行时支持:这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的一样的对话框报错,如下:

2. 提供了静态分析支持:通过静态分析Contracts,静态分析工具可以比较容易掌握函数的各种有关信息,甚至可以作为Intellisense
看到这里,可能有些朋友会有一些疑问:Contracts可以做条件检查并且弹出类似Assert的对话框,这个和Assert有何区别呢?其实,Contracts和Assert的区别主要在于:
1. Contracts的意图更加清晰,通过不同的Requires/Ensures等等调用,代表不同类型的条件,比单纯的Assert更容易理解和进行自动分析
2. Contracts的位置更加统一,将3种不同条件都放在代码的开始处,而非散见在函数的开头和结尾,便于查找和分析。
3. 不同的开发人员、不同的小组、不同的公司、不同的库可能都会有自己的Assert,这就大大增加了自动分析的难度,也不利于开发人员编写代码。而Contracts直接被.NET 4.0支持,是统一的。
当然了,Contracts也和Assert有一些非常类似的地方,比如Contracts和Assert都可以运行时检查错误,也可以在随意的在代码中关闭打开。VS中支持Contracts的几种不同的典型配置:
1. CONTRACTS_FULL:打开所有Contracts
2. CONTRACTS_PRECONDITIONS:仅有Precondition
3. RequireAlways Only:仅有RequireAlways。RequireAlways的意思等会讲到
这些选项都可以通过项目的Code Contracts页面来进行修改,这个页面是通过安装Contracts工具包获得的:

在这里提醒大家一下,在使用Contracts功能之前,一定要下载最新版的Contracts开发工具包:http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
这个工具包提供了一系列的Contracts所需要的一些工具,文档以及VS的插件。不安装这个工具包将无法使用Contracts的功能。
刚才我们谈到了Requires和Ensures两种条件,这里把.NET 4.0中的最常用的几种Contracts列一下:
1. Requires:函数入口处必须满足的条件
2. Ensures:函数出口处必须满足的条件
3. Invariants:所有成员函数出口处都必须满足的条件
4. Assertions:在某一点必须满足的条件
5. Assumptions:在某一点必然满足的条件,用来减少不必要的警告信息
其中,对于Invariant需要稍作一点说明。因为Invariant是需要对每个成员函数都需要起到作用,显然如果把这个条件放在每个函数的末尾处可以起到这个效果,但是这么做显得比较笨。.NET 4.0中采取的方式是这样的:使用某个成员函数作为Invariant,上面标记ContractInvariantMethod属性,然后在这个成员函数里面用Contracts.Invariant来指定每一条Invariant条件。这样,Invariant就会对每个函数起作用了。同时,这个Invariant方法也应该标记上PureAttribute属性,表明该函数不存在副作用(不会修改对象的状态)
Contracts的奥秘
看到这里,不知道有些朋友发现没有,不管是Ensures还是Invariant,它们的位置并不是代码应该所存在的位置。对于Ensures来讲,它是函数出口条件,那么必然在出口时候被调用,但是为什么Ensures写在前面呢?同样的,Invariant是对每个函数起作用,如果单独写一个函数作为Invariant怎么保证它会被每个函数调用到呢?其实这些都是很合理的问题。首先,Ensure和Invariant的这种写法是很合理的,原因之前也提到过了,剩下的问题是,.NET如何保证这些条件会在正确的时候被执行。其实,在编译的时候,Contracts的工具包中有一个小工具ccrewrite,这个工具负责将编译出来的二进制代码进行调整,如下:

可以看到,ccrewrite负责将各种条件的位置进行调整,最终使得条件处于正确的位置。
接口级别的契约
除了在方法实现中加入契约之外,接口也可以加入契约。接口首先需要加上一个ContractClassAttribute,指向对应的ContractClass:
1: [ContractClass(typeof(IFooContract))]
2: interface IFoo {
3: int Count { get; }
4: void Put(int value );
5: }
而ContractClass则需声明ContractClassForAttribute,说明是IFoo的ContractClass,然后显式实现IFoo,并加入Contract:
1: [ContractClassFor(typeof(IFoo))]
2: sealed class IFooContract : IFoo {
3: int IFoo.Count {
4: get {
5: Contract.Ensures( 0 <= Contract.Result<int>() );
6: return default( int ); // dummy return
7: }
8: }
9:
10: void IFoo.Put(int value)
11: {
12: Contract.Requires( 0 <= value );
13: }
一个完整的例子
现在我们来看一个完整的例子。在这个例子中WordList使用了Contract.Requires/Ensures/EnsuresOnThrow/Invariant等契约,还附送几个Bug,程序比较简单,这里就不多说了:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Diagnostics.Contracts;
6:
7: namespace ContractDemo
8: {
9: class WordList
10: {
11: private List<string> _words;
12:
13: public WordList(int capacity)
14: {
15: Contract.Requires(capacity > 0);
16: _words = new List<string>(capacity);
17: }
18:
19: public void Insert(string word)
20: {
21: Contract.Requires(word != null);
22: Contract.Ensures(Contract.OldValue<int>(_words.Count) + 1 == _words.Count);
23: Contract.EnsuresOnThrow<ApplicationException>(Contract.OldValue<int>(_words.Count) == _words.Count);
24:
25: int i;
26: for (i = 0; i < _words.Count; ++i)
27: {
28: int compare = string.Compare(word, _words[i]);
29: if (compare > 0)
30: break;
31: else if (compare == 0)
32: throw new ApplicationException("Already exist!");
33: }
34:
35: _words.Insert(i, word);
36: }
37:
38: [ContractInvariantMethod()]
39: internal void Invariant()
40: {
41: // No null string
42: Contract.Invariant(Contract.ForAll(_words, w => w != null));
43:
44: // Make sure the words are in ascending order
45: Contract.Invariant(IsAscending());
46: }
47:
48: [Pure]
49: internal bool IsAscending()
50: {
51: bool isAscending = true;
52: for (int i = 1; i < _words.Count; ++i)
53: {
54: if (string.Compare(_words[i - 1], _words[i]) > 0)
55: {
56: isAscending = false;
57: break;
58: }
59: }
60:
61: return isAscending;
62: }
63: }
64:
65: class Program
66: {
67: static void Main(string[] args)
68: {
69: WordList wordList = new WordList(0);
70: wordList.Insert("Hello");
71: wordList.Insert("World");
72: }
73: }
74: }
在最新一期的.NET 4.0新特性系列课程中,我和我的同事将给大家介绍Beta1中的一些新特性,具体信息如下:
.NET 4.0中的新特性系列课程(3):.NET 4.0 Beta1 Interop 新特性介绍 (Level 200)
讲 师:张羿、朱永泰
课程简介:.NET 4.0 Beta1在Interop,也就是互操作功能上有了较大的改进,主要是能够帮助开发者更自由的自定义互操作的行为,以及查找互操作中出现的错误。这次讲座我们主要介绍4个新特性:NOPIA、Customization of Com interop stubs、interop stub diagnostics, Custom QI。
Update:因为时间关系,我们只介绍Stub Method Redirection(也就是Customization of COM interop stubs), Custom QI, IL stub diagnostics。其中Stub Method Redirection我们已经有一篇文章提及,详情请点击这里。
.NET Framework v4.0和VisualStudio 2010 Beta1已经出来有阵子了,估计有些喜欢尝鲜的朋友已经下载试用了。这一次发布包含了大量的新功能。我们上海CLR开发团队会编写一系列的文章介绍Interop的相关新功能。我来给大家简单介绍一下Stub Method Redirection功能。这个功能是CLR上海开发团队设计、开发并测试的新功能之一,这一次我们上海CLR小组共开发了下面几个功能
1. Managed TlbImp (Rewrite)
2. Stub Method Redirection
3. IL Stub ETW Diagnostics
4. Custom QueryInterface
而在CodePlex上面:
1. 发布了TlbImp的最新版本,包括基于规则的Customization(具体可以参考:这一篇)
2. 即将发布IL Stub Diagnostics Tool,可以方便大家直接观看IL Stub,内部使用IL Stub ETW Diagnostics新功能实现
除此之外,还有一些功能是由美国团队开发的:
1. NO PIA
2. IL Stub Everywhere
3. Limit Pumping
4. PreferComThanRemoting
除了NOPIA在我之前的文章已经介绍过之外,其他功能我们会陆续写文章介绍。这次我们先介绍Stub Method Redirection。在介绍这个功能之前,有必要先介绍一下相关的背景知识:
什么是IL Stub
大家都知道,在进行Interop调用的时候,CLR会对参数进行转换(也就是所谓的Marshalling),然后再调用到目标函数。这样一个参数转换和Marshalling实际上是一小段Stub(桩代码)来负责的,比如在调用MessageBox的时候,MessageBox_IL_STUB就是负责Marshalling和参数调用的Stub:

当然了,这里的Stub的内容只是一个简单的抽象,实际的内容会比这个复杂一些。在实际情况下,CLR在第一次执行MessageBox的时候,会动态生成MessageBox对应的IL STUB,使用内部的类似于ReflectionEmit的机制直接输出IL代码的Byte Code,然后交给JIT来编译之,比如MessageBox对应的IL Stub是这样子的:
1: .maxstack 6
2: .locals (native int,int32,native int,int32,native int,native int,int32,native int,int32,int32,int32,int32)
3: // Initialize {
4: /*( 0)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
5: /*( 1)*/ call void [mscorlib] System.StubHelpers.StubHelpers::DemandPermission(native int)
6: // } Initialize
7: // Marshal {
8: /*( 0)*/ ldc.i4.0
9: /*( 1)*/ stloc.0
10: IL_000c: /*( 0)*/ nop // argument {
11: /*( 0)*/ ldarg.0
12: /*( 1)*/ stloc.1
13: /*( 0)*/ ldc.i4.1
14: /*( 1)*/ stloc.0
15: /*( 0)*/ nop // } argument
16: /*( 0)*/ nop // argument {
17: /*( 0)*/ ldc.i4.0
18: /*( 1)*/ stloc.s 0x4
19: /*( 0)*/ ldarg.1
20: /*( 1)*/ brfalse IL_0037
21: /*( 0)*/ ldarg.1
22: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
23: /*( 1)*/ ldc.i4.2
24: /*( 2)*/ add
25: /*( 1)*/ stloc.3
26: /*( 0)*/ ldc.i4 0x105
27: /*( 1)*/ ldloc.3
28: /*( 2)*/ clt
29: /*( 1)*/ brtrue IL_0037
30: /*( 0)*/ ldloc.3
31: /*( 1)*/ localloc
32: /*( 1)*/ stloc.s 0x4
33: IL_0037: /*( 0)*/ ldc.i4.1
34: /*( 1)*/ ldarg.1
35: /*( 2)*/ ldloc.s 0x4
36: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
37: /*( 1)*/ stloc.2
38: /*( 0)*/ ldc.i4.2
39: /*( 1)*/ stloc.0
40: /*( 0)*/ nop // } argument
41: /*( 0)*/ nop // argument {
42: /*( 0)*/ ldc.i4.0
43: /*( 1)*/ stloc.s 0x7
44: /*( 0)*/ ldarg.2
45: /*( 1)*/ brfalse IL_006c
46: /*( 0)*/ ldarg.2
47: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
48: /*( 1)*/ ldc.i4.2
49: /*( 2)*/ add
50: /*( 1)*/ stloc.s 0x6
51: /*( 0)*/ ldc.i4 0x105
52: /*( 1)*/ ldloc.s 0x6
53: /*( 2)*/ clt
54: /*( 1)*/ brtrue IL_006c
55: /*( 0)*/ ldloc.s 0x6
56: /*( 1)*/ localloc
57: /*( 1)*/ stloc.s 0x7
58: IL_006c: /*( 0)*/ ldc.i4.1
59: /*( 1)*/ ldarg.2
60: /*( 2)*/ ldloc.s 0x7
61: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
62: /*( 1)*/ stloc.s 0x5
63: /*( 0)*/ ldc.i4.3
64: /*( 1)*/ stloc.0
65: /*( 0)*/ nop // } argument
66: /*( 0)*/ nop // argument {
67: /*( 0)*/ ldarg.3
68: /*( 1)*/ stloc.s 0x8
69: /*( 0)*/ ldc.i4.4
70: /*( 1)*/ stloc.0
71: /*( 0)*/ nop // } argument
72: /*( 0)*/ nop // return {
73: /*( 0)*/ nop // } return
74: // } Marshal
75: // CallMethod {
76: /*( 0)*/ ldloc.1
77: /*( 1)*/ ldloc.2
78: /*( 2)*/ ldloc.s 0x5
79: /*( 3)*/ ldloc.s 0x8
80: /*( 4)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
81: /*( 5)*/ ldc.i4.s 0x30
82: /*( 6)*/ add
83: /*( 5)*/ ldind.i
84: /*( 5)*/ ldind.i
85: /*( 5)*/ calli unmanaged stdcall int32(int32,native int,native int,int32)
86: // } CallMethod
87: // UnmarshalReturn {
88: /*( 1)*/ nop // return {
89: /*( 1)*/ stloc.s 0xa
90: /*( 0)*/ ldc.i4.5
91: /*( 1)*/ stloc.0
92: /*( 0)*/ ldloc.s 0xa
93: /*( 1)*/ stloc.s 0x9
94: /*( 0)*/ ldloc.s 0x9
95: /*( 1)*/ nop // } return
96: /*( 1)*/ stloc.s 0xb
97: // } UnmarshalReturn
98: // Unmarshal {
99: /*( 0)*/ nop // argument {
100: /*( 0)*/ nop // } argument
101: /*( 0)*/ nop // argument {
102: /*( 0)*/ nop // } argument
103: /*( 0)*/ nop // argument {
104: /*( 0)*/ nop // } argument
105: /*( 0)*/ nop // argument {
106: /*( 0)*/ nop // } argument
107: /*( 0)*/ leave IL_00b3
108: IL_00b3: /*( 0)*/ ldloc.s 0xb
109: /*( 1)*/ ret
110: // } Unmarshal
111: // Cleanup {
112: IL_00b6: /*( 0)*/ ldloc.0
113: /*( 1)*/ ldc.i4.1
114: /*( 2)*/ ble IL_00ca
115: /*( 0)*/ ldloc.s 0x4
116: /*( 1)*/ brtrue IL_00ca
117: /*( 0)*/ ldloc.2
118: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
119: IL_00ca: /*( 0)*/ ldloc.0
120: /*( 1)*/ ldc.i4.2
121: /*( 2)*/ ble IL_00df
122: /*( 0)*/ ldloc.s 0x7
123: /*( 1)*/ brtrue IL_00df
124: /*( 0)*/ ldloc.s 0x5
125: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
126: IL_00df: /*( 0)*/ endfinally
127: // } Cleanup
128: .try IL_000c to IL_00b3 finally handler IL_00b6 to IL_00e0
129:
可以看到IL代码非常多,这些都是CLR内部自动生成的。因为看到这些代码有助于开发者理解内部工作原理和找到错误(一般来说是开发者本身的问题,比如MarshalAs写错了),我们将发布一个工具可以让你看到IL Stub具体内容,底层是通过调用另外一个CLR V4 Interop的新功能:IL Stub ETW Diagnostics实现的,以后有机会我会写另外一篇文章介绍。至于IL代码本身的相关内容可以参考Experts IL Assembler和Common Language Infrastructure Annotated Standard.
总的来说,一般的IL Stub总要负责下面几件事情:
1. 安全检查
2. 参数转换,包括返回值
3. 调用目标函数,检查返回值,可能会抛出异常
4. 清理临时内存
其实还有一些其他细节问题如切换GC模式等,建立Frame等等,但是这些属于CLR内部细节问题,这里不再赘述。
IL Stub的问题
IL Stub目前为止都工作的很好。其实,CLR内部本来不是所有情况下都是用IL Stub,2.0以前还存在所谓的ML Stub (Marshalling Language),专门工作在x86下,IL则是工作在x64和IA-64上,后来美国团队将之整合,现在就只有IL Stub了。看起来现在的IL Stub就足够了,不过事实上我们认为ILStub仍然存在一些问题:
1. 无法调试
a. 目前VS暂时不支持调试IL代码
b. 即使可以调试,绝大多数开发者根本不熟悉IL代码
c. IL代码是动态生成,增大了调试支持实现的难度
d. 较难通过工具直接看到(我们即将发布新工具支持看到IL Stub)
2. 不够灵活
a. IL Stub是CLR根据内置规则生成(也就是MarshalAs那一套),开发者无法加入新的规则
b. 开发者无法使用自己的Stub来替换ILStub
3. 组件化和维护性:CLR有大量生成IL Stub的代码,这些代码非常复杂,规则繁多,大大增加了CLR的复杂度,而且本身是由C++写成,较难维护
我们的Vision
既然IL Stub本身有这么多问题,那么我们应该如何解决这些问题呢?在开发Stub Method Redirection新功能之前,我们Team内部有一些讨论,达成的共识如下:
1. CLR只支持最简单的calli调用本地代码
2. IL Stub由编译时刻工具生成:ILStubGen.exe
a. 工具内置数据转换规则
b. 用户可通过插件自定义
3. 生成的IL Stub通过calli调用本地代码
4. Interop类型和Stub直接嵌入在目标程序中:NO PIA是朝这个方向的正确一步
5. CLR运行时刻加载IL Stub:Stub Method Redirection支持该功能
可以看到,按照如上的方法,CLR可以完全从生成IL stub的任务中解放出来,IL Stub的生成也从动态(运行时)转为静态(编译时),并且可以用C#编写,解决了调试、性能、组件化,维护性的众多问题。为了实现这个美好的Vision,有很多工作要做,而且这些工作显然没法在一个Release之内完成,因此我们采取的方法是迭代渐进式的。也就是说,每个Release都会添加一些功能,和这个Vision更加接近。这个Release,我们做的就是NO PIA,以及Stub Method redirection(的一部分)。
Stub Method Redirection
所谓Stub Method,也就是用户编写的编译时刻决定的Stub,可以用任意语言编写,CLR在运行时刻不会动态生成IL Stub,而是会使用用户自定义的Stub,而实现这个的秘诀就是:
ManagedToNativeComInteropStubMethodAttribute
这个Attribute有两个参数:
1. Type:Stub Method所位于的类
2. Name:Stub Method的名称。虽然我们也想实现所谓的methodof功能(类似typeof),但是让C#在4.0中替我们加上这个功能不是太现实,因此我们就先使用名字来查找,速度稍慢,但是因为相关查找只用进行一次,而且可以通过NGEN来避免查找(NGEN来负责查找然后把查找结果直接写入本地代码中),因此速度上不存在问题。
一旦在接口(非接口不可以)的某个方法上面添加上这个Attribute,CLR就知道根据这个Attribute来找Stub,而非自己生成。
用户可以通过这个功能做下面的事情:
1. 编写自己的Stub
a. 加以优化(比如内存池之类的)
b. 提供自定义的类型转换
2. 编写第三方工具自己生成Stub(不过一般来讲这个会是由CLR和.NET Framework提供)
任何编写的Stub Method必须满足下面这些要求:
1. 必须是静态
2. 第一个参数是接口类型
3. 其他参数和对应接口方法完全一致
4. 必须和对应接口位于同一个Assembly,这既是简化,也符合我们的Vision
5. 必须满足访问性要求:从接口的方法必须可以访问到Stub,这个和逻辑上的调用顺序是一致的
6. 不可以是generic
一旦不满足要求,CLR在执行方法的时候会抛出异常,比如:

这个信息是我和PM MM讨论数次之后决定的,目的是让其尽量清晰。
对于一个Stub Method来讲,通常的格式是这样子的:
1: class FooStubClass
2: {
3: internal static void ForwardFooStub(IFoo thisObject, string arg)
4: {
5: try{
6: // Step 1: 托管参数转换到非托管参数(In)
7: // Step 2: 获得调用目标函数的地址
8: // Step 3: 通过Delegate调用目标函数
9: // Step 4: 非托管参数转换到托管参数(Out)
10: // Step 5: 转换返回值
11: }
12: finally
13: {
14: // Step 6: 清理工作
15: }
16: }
17: }
18:
下面分别解释一下:
1. 托管参数转换到非托管参数(In):一般这里调用Marshal的对应函数来进行转换,比如Marshal.StringToBSTR
2. 获得调用目标函数的地址:这个稍微复杂一点,注意因为是COM,所以需要通过虚函数表来获得:
1: //
2: // Get interface pointer
3: //
4: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
5:
6: //
7: // Get target
8: //
9: IntPtr pTarget = IntPtr.Zero;
10:
11: unsafe
12: {
13: void** pVtbl = *(void***)pIntf;
14: pTarget = new IntPtr(*(pVtbl + 7)); // IUnknown => 3, IDispatch => 4
15: }
16:
比如上面的代码就获得了_this的IFoo指针,然后获取了虚函数表第八项(跳过IUnknown3个函数,IDispatch 4个函数)作为函数指针
3. 通过Delegate调用目标函数:这一步骤需要首先调用Marshal.GetDelegateForFunctionPointer获得函数指针对应的Delegate,注意Delegate的参数必须得是对应非托管的类型,比如MessageBox对应的delgate是(IntPtr, IntPtr, IntPtr, int),然后再调用delegate,传入参数
4. 非托管参数转换到托管参数(Out):转换的时候既要包括IN也要包括OUT,比如[in, out]char []这种情况,必须两种方向都要照顾到,IN在调用之前转换,而OUT则是在调用之后转换
5. 转换返回值:这个没太多好说的,和OUT比较类似
6. 清理工作:转换不要忘记清理中间生成的临时数据,比如string转换到char *需要调用Marshal.StringToCoTaskMemAnsi转换,之后调用Marshal.FreeCoTaskMem释放,释放则是在Cleanup中作
最后是一个完整的例子:
1: Using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Runtime.InteropServices;
6: using System.Runtime.CompilerServices;
7:
8: namespace StubMethodDemo
9: {
10: [ComImport]
11: [Guid("0741BD5F-549A-46FD-A857-0E3B23620399")]
12: interface IFoo
13: {
14: [MethodImplAttribute(MethodImplOptions.InternalCall)]
15: [ManagedToNativeComInteropStubAttribute(typeof(FooStubClass), "IFoo_Hello_Stub")]
16: void Hello(string name);
17: }
18:
19: [ComImport]
20: [Guid("68389CF3-212B-449D-83CB-0DD4572FEF03")]
21: class Foo : IFoo
22: {
23: [MethodImplAttribute(MethodImplOptions.InternalCall)]
24: public extern void Hello(string name);
25: }
26:
27: class FooStubClass
28: {
29: public delegate int IFoo_Hello_Delegate(IntPtr _this, IntPtr a);
30:
31: public void IFoo_Hello_Stub(IFoo _this, string name)
32: {
33: IntPtr nativeArg_name = IntPtr.Zero;
34:
35: try
36: {
37: //
38: // Marshal CLR => Native
39: //
40: nativeArg_name = Marshal.StringToBSTR(name);
41:
42: //
43: // Get interface pointer
44: //
45: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
46:
47: //
48: // Get target
49: //
50: IntPtr pTarget = IntPtr.Zero;
51:
52: unsafe
53: {
54: void** pVtbl = *(void***)pIntf;
55: pTarget = new IntPtr(*(pVtbl + 7)); // IUnknown => 3, IDispatch => 4
56: }
57:
58: //
59: // Make the call
60: //
61: Delegate dele = Marshal.GetDelegateForFunctionPointer(pTarget, typeof(IFoo_Hello_Delegate));
62: IFoo_Hello_Delegate targetDelegate = (IFoo_Hello_Delegate)dele;
63: int hr = targetDelegate(pIntf, nativeArg_name);
64: if (hr < 0)
65: Marshal.ThrowExceptionForHR(hr);
66:
67: //
68: // Marshal Native => CLR
69: //
70:
71: //
72: // Marshal return
73: //
74: }
75: finally
76: {
77: //
78: // Cleanup
79: //
80: if (nativeArg_name != IntPtr.Zero)
81: Marshal.FreeBSTR(nativeArg_name);
82: nativeArg_name = IntPtr.Zero;
83: }
84: }
85: }
86:
87: class Program
88: {
89: static void Main(string[] args)
90: {
91: Foo myFoo = new Foo();
92: myFoo.Hello("Foo!");
93: }
94: }
95: }
96:
作者:张羿
转载请注明出处
这个星期三也就是5月19日下午2点半开始我将为大家带来CLR开发课程系列之六。这一次我打算讲一些.NET比较本质的内容:Metadata和IL代码。欢迎希望对.NET的运作机理和工作方式能够有进一步深入了解的朋友收听。本次讲座的具体信息如下:
公共语言运行库(CLR)开发系列课程(6):.NET中间语言(IL)入门 (Level 200)
讲 师:张羿
课程简介:.NET本质上是一个执行中间语言(Intermediate Language) 代码的虚拟机,任何在.NET上运行的语言(如C#)都需要将本身翻译为IL代码,然后交给.NET执行。因此,理解IL代码对于理解.NET本身的工作方式、编写正确和高效的代码、查找和定位错误各种运行错误是非常有效的。本次讲座将介绍有关IL代码的基本知识,可以帮助大家理解大多数常见的IL代码。
推荐指数: 
Visual Studio 2010和.NET 4.0的Beta1版本终于发布了!目前Beta1还只是对MSDN的订阅者开放,到美国时间20日星期三,也就是我们的21日星期四的时候,Visual Studio 2010 / .NET 4.0 Beta1将公开对外发布。这个版本是可以安装的Setup,而非上次的虚拟机镜像,因此对于大家的机器要求会放松一些。想要观看安装图片的朋友们可以点击这篇Blog:http://www.itsmywindows.com/visual-studio-2010-first-look-installation
这一次发布距离上次的CTP发布过了大概半年的时间,这半年的时间有数量众多的新特性被开发出来,并被加入到Beta1版本之中。在接下来的几篇Blog和Webcast(具体请参看中文MSDN的相关预告),我将主要介绍.NET 4.0 Beta1中的一些新特性,特别是我们上海CLR开发小组所负责开发的一些关于Interop的新功能,尽请期待!
微软上海VS TeamArch小组的中文Blog上面最近开始了一个系列文章,主要翻译Cameron Skinner的一系列介绍Visual Studio Team System 2010 Architecture CTP的一些新功能的Blog,目前有下面两篇:
VSTS 2010 Architecture 第一章:Modeling Project
Visual Studio Team System 2010 Architecture- 前言
建议对VS 2010 Architecture CTP有兴趣的朋友可以关注一下。
VS TeamArch小组的Blog主页是:http://blogs.technet.com/teamarchchina/
最近在网上发现一个小程序Windows Live Writer Backup Utility可以用来备份Windows Live Writer的Blog设置。我有好几个Blog,使用Windows Live Writer在不同Blog上面发布非常方便,但是因为我有时候会重装一下系统试一下其他的系统,比如Windows 7 Beta,并且马上可能就要最近出RC,因此,如果可以备份Blog的设置的话是非常方便的。这个程序非常简单,勾上你要备份的数据,然后选择Backup即可:
![clip_image002[4] clip_image002[4]](http://blogs.msdn.com/blogfiles/yizhang/WindowsLiveWriter/CorFlagsWindowsLiveWriterBackupUtilityBu_120B4/clip_image002%5B4%5D_thumb.jpg)
可是点击Backup,这个程序最终居然抛出异常:
![clip_image004[4] clip_image004[4]](http://blogs.msdn.com/blogfiles/yizhang/WindowsLiveWriter/CorFlagsWindowsLiveWriterBackupUtilityBu_120B4/clip_image004%5B4%5D_thumb.jpg)
仔细看看,发现这个Exception是BadImageFormatException,无法加载CabLib这个Assembly。我的OS是64-bit,会不会是主程序和Assembly之间一个是64-bit一个是32-bit呢?打开TaskManager,找到这个进程,发现这个进程确实是64-bit的,
![clip_image006[4] clip_image006[4]](http://blogs.msdn.com/blogfiles/yizhang/WindowsLiveWriter/CorFlagsWindowsLiveWriterBackupUtilityBu_120B4/clip_image006%5B4%5D_thumb.jpg)
然后再用Corflags查看一下RarLib:
C:\Program Files (x86)\Windows Live Writer Backup>corflags cablib.dll
Microsoft (R) .NET Framework CorFlags Conversion Tool. Version 3.5.21022.8
Copyright (c) Microsoft Corporation. All rights reserved.
Version : v2.0.50727
CLR Header: 2.5
PE : PE32
CorFlags : 16
ILONLY : 0
32BIT : 0
Signed : 0
发现这个Assembly是PE32,不是ILONLY,因此这个Assembly只能以32-bit执行,再看看主程序:
C:\Program Files (x86)\Windows Live Writer Backup>corflags LiveWriterBackup.exe
Microsoft (R) .NET Framework CorFlags Conversion Tool. Version 3.5.21022.8
Copyright (c) Microsoft Corporation. All rights reserved.
Version : v2.0.50727
CLR Header: 2.5
PE : PE32
CorFlags : 1
ILONLY : 1
32BIT : 0
Signed : 0
这个EXE是PE32,ILONLY,说明这个程序是以Any CPU编译的,也就是说在64-bit机器上缺省64-bit运行(但是也可以在32-bit下运行),在32-bit机器上以32-bit运行。至此问题就很清楚了,主程序以64-bit运行,尝试加载32-bit的CabLib失败。解决方法很简单,强制主程序以32bit运行:
C:\Program Files (x86)\Windows Live Writer Backup>corflags LiveWriterBackup.exe
/32bit+
Microsoft (R) .NET Framework CorFlags Conversion Tool. Version 3.5.21022.8
Copyright (c) Microsoft Corporation. All rights reserved.
之后再次运行LiveWriterBackup程序,问题解决。如果你也正巧正在运行64-bit的系统,并且运行某些.NET程序时候发生了BadImageException,这时候可以怀疑是32-bit/64-bit相关的问题,并使用本文所使用的方法来确定问题并解决。
不知道各位使用.NET开发的朋友是否有遇到过一些非常奇怪的问题而不知道如何下手呢?这个时侯CLR本身提供的StressLog功能就非常有用了。这个StressLog可以在很多时候把CLR所做的事情记录下来,比如,对于一个很简单的最后抛出异常的.NET程序Log大致如下:
| STRESS LOG: facilitiesToLog = 0x8000ffff levelToLog = 16 MaxLogSizePerThread = 0x20000 (131072) MaxTotalLogSize = 0x2000000 (33554432) CurrentTotalLogChunk = 6 ThreadsWithLogs = 3 Clock frequency = 0.014 GHz Start time 22:47:37 Last message time 22:47:44 Total elapsed time 6.520 sec THREAD TIMESTAMP FACILITY MESSAGE ID (sec from start) -------------------------------------------------------------------------------------- 1638 6.519607729 : `SYNC` SafeExitProcesses: exitcode = -2146233082 1638 0.842361250 : `GC` CreateHandle: 0000000000261338 1638 0.838161973 : `EH` In CLRVectoredExceptionHandler, Exception = e0434f4d, Context = 00000000001AE4C0, IP = 00000000779B649D SP = 00000000001AEA60 1638 0.837840633 : `GC` CreateHandle: 0000000000261340 1638 0.837838329 : `EH` in Thread::SetLastThrownObject: obj = 0000000002B76238 1638 0.837837281 : `EH` Exception HRESULT = 0x80131600 Message String 0x0000000002B96188 (db will display) InnerException 0000000000000000 MT 0000000000000000 (BAD MethodTable) 1638 0.837834418 : `EH` ******* MANAGED EXCEPTION THROWN: Object thrown: 0000000002B76238 MT 000007FEF40FADC8 (System.ApplicationException) rethrow 0 1638 0.837646265 : `CLASSLOADER` DoRunClassInit: returning SUCCESS for init 000007FEF40F2E10 (System.Collections.HashHelpers) in appdomain 000000000035CCC0 1638 0.837618049 : `CLASSLOADER` RunClassInit: Returned Successfully from class contructor for type 000007FEF40F2E10 (System.Collections.HashHelpers) 1638 0.837617141 : `CLASSLOADER` DoRunClassInit: returning SUCCESS for init 000007FEF40F2E10 (System.Collections.HashHelpers) in appdomain 000000000035CCC0 … … (中间省去1000余行) … 1638 0.328838232 : `GC` CreateHandle: 00000000002613E0 1638 0.312699170 : `CLASSLOADER` Attempted to set new native file 02088e80, old file was 00000000, location in the image=f3cc1008 1754 0.269468466 : `ALWAYS` SetupThread managed Thread 00000000020815D0 Thread Id = 2 ------------ Last message from thread 1754 ----------- 1638 0.268254415 : `GC` CreateHandle: 00000000002613E8 1638 0.268252879 : `GC` CreateHandle: 00000000002615F0 1638 0.219663812 : `ALWAYS` SetupThread managed Thread 00000000003B8BC0 Thread Id = 1 1638 0.219644256 : `GC` CreateHandle: 00000000002613F0 1638 0.219643209 : `GC` CreateHandle: 00000000002615F8 1d9c 0.200751492 : `CORDB`ALWAYS` Debugger Thread spinning up ------------ Last message from thread 1d9c ----------- 1638 0.187761783 : `GC` CreateHandle: 00000000002613F8 1638 0.187758151 : `GC` CreateHandle: 00000000002611F8 ------------ Last message from thread 1638 ----------- ---------------------------- 625 total entries ------------------------------------ |
可以看到这个程序最后(注意Log的最前面是程序最后发生的事情,是反过来的)抛出了一个System.ApplicationException, HR=0x80131600,导致程序终止。当然了,实际的情况会比这个复杂得多,这里只是一个例子而已。这些信息详细说明了CLR的运行情况,主要供CLR小组的开发人员使用。但是这并不意味着这些信息对于一般.NET开发人员没有用处,其实这些信息对于了解托管程序的运行状况是很有用的,并且如果运行中出现了错误,这些错误也会被写到StressLog中。当然了,解决一般的问题也许并不需要使用StressLog,但是如果你手头的问题没有任何线索可循,不妨试一下StressLog,也许会有意想不到的效果。如果想对某条有疑问的具体信息进行解读,除了参考错误信息之外,也可以在Rotor代码中查找相应代码行,从而确定大概是什么意思。比如AppDomain::Unload方法中可以查找到STRESS_LOG宏输出了Unload domain这条信息:
1: void AppDomain::Unload(BOOL fForceUnload)
2: {
3: …
4: STRESS_LOG3 (LF_APPDOMAIN, LL_INFO100, “Unload domain [%d, %d] %p\n”, GetId().m_dwId, GetIndex().m_dwIndex, this);
5: …
6: }
不过注意不是所有信息都可以在Rotor中查找到,因为Rotor中并不包含所有CLR 2.0的代码。
获得StressLog的方法如下:
1. 在命令行中输入:set COMPLUS_StressLog=1
2. 在命令行中启动WinDbg,然后通过WinDbg开始调试程序,直到程序运行到出问题的地方
3. 在WinDbg中输入:.loadby sos mscorwks。这一步骤是用来加载SOS的。SOS是一个用来调试CLR的一个WinDbg的Extension,有机会我会专门写篇文章讲用SOS来调试托管程序,CLR小组内部调试托管程序很多时候都用这个。
4. 在WinDbg中输入!DumpLog
5. 在程序启动的目录下查找StressLog.txt文件,StressLog信息就在里面了
第一步第二步也可以在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework建立一个DWORD类型的名为StressLog的值,设置为1也可以,只是效果是全局的,不如set COMPLUS_StressLog灵活。
在.NET 4.0中引入了一个新功能:Corrupted State Exceptions。听上去名字很神秘,实际上这个功能主要是限制对Exception的错误用法:捕获AccessViolationException/SEHException等可能会造成程序状态错误而无法正确继续的种种异常,具体可以参看CLR程序经理Andrew Pardoe的这篇MSDN文章:http://msdn.microsoft.com/en-us/magazine/dd419661.aspx
前段时间花了几天一直在用WinDbg调试一个比较棘手的Bug。这个Bug是C# Team那边发现的,他们的Testcase跑大概10分钟左右会出一个在CLR内部的ASSERT。比较难调试的主要原因在于ASSERT表明一个全局的数据结构出现了问题,本来不应该用完的数组却已经用完了(因为按照设计,这个数组是边使用边清理的,是不会用完的)。初步想到的有下面几种方案来调试:
1. 设置数据断点
2. 一步一步调试
3. 添加Log代码
设置数据断点的主要问题是不太好确定到底是因为什么原因导致的数据结构问题,而且因为是数组被用完,很难将是到底是哪一个数组元素的加入导致了数组被全部占用,因此无法通过设置数据断点的方法来调试。一步一步的调试显然也没法解决问题,因为这个Testcase本身要跑十分钟,可以想象单步调试运行十分钟的程序会花费多长时间。因此两个方案都被我否决。添加Log代码其实是可以的,只是需要修改代码,每次修改之后需要重新编译代码,然后需要在目标机器上安装,而且C#使用的CLR的Branch并非我们正在开发的Branch,需要重新下载源代码,相对比较麻烦。最后为了解决这个问题,我采取的方法是使用WinDbg的条件断点+Log的方式。大致的方法如下:
第一步:在一个或者多个可疑处设置断点
bu address “command”
bu是WinDbg中的设置Unresolved Breakpoints命令,用起来比较方便,我比较喜欢用。address就是你所要断的代码地址,可以是函数开始,也可以是某一行。Command非常重要,它表示了WinDbg在每次断到address的时候都要执行的命令,不同命令用分号隔开,如:
.echo [Function A]; dv this; kb; g
这几条命令意思是:打印[Function A],打印this指针的值,打印当前调用栈,然后继续执行。大家可以根据实际情况添加一些其他命令打印一些自己所需要的信息。通过上面这套命令打印的内容大致如下:
[FunctionA]
this = 0xABCDEFG
module!FuncA
module!FuncB
module!FuncC
…
可以看出,这条断点如果反复被断,那么在WinDbg的命令窗口中便会把每次断点被Hit的相关信息通过刚才定义的命令打印出来。如果定义了很多这样的断点,那么在命令窗口中就会把整个程序执行的情况打印出来,起到Log的作用,而且可以显示调用栈等信息,比一般的Log要强大许多。
第二步:设置Log
缺省情况下,WinDbg的Buffer大小是有限的,如果程序运行时间比较长,那么Buffer可能会不够,我们通过条件断点打出的信息会被截断。幸好,WinDbg提供了将命令窗口的内容输出到Log中的功能。选择Edit->Open/Close Log File菜单项,WinDbg会显示如下对话框:

在这个对话框里面输入你想要保存的Log文件名即可。如果是添加新的内容而不是覆盖原有的,则勾上Append。
第三步:分析Log
当获得了Log信息之后,下一步就需要分析Log的内容了,这是一件需要耐心、对数据的敏感、以及一点点运气的事情。分析的时候可能发现Log的信息不足,这时就需要添加新的断点或者修改打印的信息,重新收集Log,再加以分析,直到Log信息足够为止。这时WinDbg设置条件断点的优势就出来了,因为不需要修改代码,编译代码,部署代码这样的一个过程,而是只需要键入不同的命令而已。经过几次调整断点位置和打印的信息并重新收集Log,我最终通过分析发现这个Bug是只有可能在特定情况下RCW没有被GC,并且创建线程退出的时候才会出现,具体的内容因为涉及到.NET 4.0中还没有发布的新功能,这里就不多说了。可以看到,如果采用常规的方法,对于这种在特定的条件下才会重现的问题是很难发现的。
总之,使用WinDbg来设置条件断点,打印相关信息,并且输出到Log文件是一种非常强大的调试方法,可以调试一些非常复杂的Bug,而且具有不需要修改代码的灵活性,可以自由定义自己想需要打印的信息和断点设置的位置,主要的缺点是方法稍显复杂,不过如果适应了之后还是很方便的。我强烈推荐大家在遇到比较复杂的Bug的时候,可以尝试使用一下这种方法,可能具有意想不到的效果哦。
这次我将为大家讲解如何使用.NET 4.0中的契约式设计(也可以在.NET 2.0+中使用,需要额外下载安装包),欢迎有兴趣的朋友收听。
地址为:http://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032406872&Culture=zh-CN
.NET 4.0中的新特性系列课程(2):契约式设计 (Level 200)
讲 师:张羿
课程简介:.NET 4.0中引入了契约式设计这一概念,允许程序员在函数体中按照固定的格式显式说明函数的入口,出口等地方所必须满足的条件。这一功能可以有效减少程序 Bug数量,让程序员更容易的理解现有代码,并提供静态检查、动态检查等功能。本次讲座将介绍契约式设计的概念,以及在.NET 4.0中的使用方法。
推荐指数: 
大家好。距离上次我们发布在CodePlex上的新版本TlbImp已经过了快半年了。在这半年的时间内,除了主要进行.NET 4.0相关的新功能开发之外,我们上海CLR小组也没有忘记进行TlbImp相关功能的继续开发,于今年3月9日再次发布了TlbImp的一个新版本:
http://www.codeplex.com/clrinterop/Release/ProjectReleases.aspx?ReleaseId=17579
这次版本中我们引入了两个重要功能:
1. 通过规则自定义互操作程序集以及规则自定义编辑器
2. 回归测试工具
基于规则的自定义功能
我们先来看一下自定义功能。这个新版本的TlbImp允许用户通过自定义的一系列的规则来指定TlbImp如何生成最终的互操作程序集。之前有不少用户向我们提到在使用TlbImp的时候,经常需要对TlbImp生成的结果做一些修改,而且必须是自动化的修改。他们通常使用的方法是先使用ILDASM反汇编,使用Perl脚本修改反汇编代码,然后再使用ILASM重新生成互操作程序集。为了解决这个问题,我们引入了一个新功能,允许用户以非常自由的方式来定义他们最终想要看到的结果。
让我们先来看一个简单的例子:假设我们希望改变互操作程序集中的某个类型的名称。先双击打开TlbImpConfigFileEditor.exe启动自定义文件的编辑器,然后打开我们需要自定义的Type Library,这里我们选择发布版本中Samples\ChangeManagedName\ChangeManagedNameSample.tlb文件,如下:

左边显示的是我们需要自定义的Type Library,而右边,则是我们需要自定义的规则,这些规则可以告诉TlbImp如何修改最终生成的互操作程序集。首先,将我们需要修改的IComparable接口结点从左边拖到右边,松开鼠标,出现如下的对话框:

上面这个对话框是用来创建一个新的规则,规则指定TlbImp对于哪些对象应用何种动作。在这个对话框中我们需要定义这个规则所对应的动作(Action),因此需要在Action下拉框中选择ChangeManagedName,然后点击OK即可。之后编辑器状态如下(需要自己展开结点):

大家可以看到右边已经出现了一个新的规则叫做Change interface name,对应的Category是Type,也就是说这个规则是针对互操作程序集中的类型设置的。Condition指定了规则所需要满足的条件,选中Condition节点(或者其子节点)可以在下面的Condition Expression中看到对应的规则表达式,也就是NativeName Equal ‘ICompareable’,意思是该规则是针对任何名字叫做IComparable的类型。注意因为我们是从IComparable节点直接拖到右边,因此这些条件是编辑器自动生成的。大家如果需要也可以自己通过点击Native Equal IComparable条件来修改,或者点击<Empty>来增加新的条件。现在我们可以点击<Empty>节点,在下拉框中选择TypeKind,第二个下拉框选择Equal,第三个下拉框选择Interface,最终的结果如下:

注意表达式节点的组织方式是类似语法树的样子,也就是说And节点下面的互相之间是And关系,最终的结果总是可以在Condition Expression一栏看到:
( NativeName Equal 'IComparable' ) And ( TypeKind Equal 'Interface')
当编辑好了规则的时候,我们需要指定对应的具体动作的参数。因为我们需要修改对象类型的名称,双击Action下面的NewName子结点会弹出如下对话框:

输入我们想修改成的名字,然后点击OK。
这样一个规则就完成了:

修改完毕之后存盘为ChangeInterfaceName.xml,然后在命令行下面调用TlbImp,使用/config参数引用之前存盘的Config文件(黄色加亮部分:

之后通过ILDASM打开我们生成的结果:

可以看到IComparable已经被改名成了IMyInterface。
TlbImp总共支持下面几种动作(Action):
1. ChangeManagedName:修改类型、函数的名称
2. ResolveTo:将一个类型替换为另外一个类型(可以是另外一个程序集的类型)。现在已经有用户在CodePlex上面提出这个功能需求了:http://clrinterop.codeplex.com/WorkItem/View.aspx?WorkItemId=2565
3. AddAttribute:为任意类型添加任意Attribute
4. PreserveSig:为单个函数或者类型中的所有函数添加PreserveSigAttribute并相应修改函数的原型
5. ConvertTo:修改函数中的参数类型
每种对应的动作在Samples目录下面都有对应的例子,有兴趣的朋友可以参照文档自行实验。
回归测试工具
为了帮助用户在修改TlbImp代码的时候可以更容易保证自己的修改不会引起其他问题,我们引入了一个简单的回归测试工具,大家可以到这里下载:
http://clrinterop.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=17579
下载完毕之后打开Bin目录下面的TlbImpRegressionTestTool.exe,然后通过File菜单打开Testcase目录下面的testcases.xml文件:

可以看到所有的Testcase都被列出来了。下一步在Run菜单里面选择Settings,输入TlbImp2.exe和WinDiff.exe所在位置:

完毕之后,选择Run下面的Run All Testcases或者Run Selected Testcases,该工具便会调用TlbImp2.exe依次运行Testcase来检查TlbImp2的相应功能是否正确:

绿色为测试成功,红色为失败。如果有失败的情况,双击该行可以打开WinDiff比较TlbImp2当前生成的结果和应该生成的结果之间的区别。
最后,希望大家能够积极试用TlbImp的新功能。如果有希望看到的TlbImp的新功能,或者对目前的TlbImp有哪些觉得做的不够好的地方,都可以到下面的地址提出你的宝贵意见: http://clrinterop.codeplex.com/WorkItem/List.aspx