微软中国研发集团服务器与开发工具事业部近期调整了个别研发项目,我们希望专注于创新,并不断加深在部分产品和技术开发上的广度和深度。CLR/Silverlight小组所负责的Interop和JIT项目将近期移交给美国团队,所有工程师也将于近期转入事业部其他的大项目组,这也意味着我们从即日起挥别这个耕耘了三年的中文博客。
如果您对CLR相关的内容感兴趣,请访问美国团队的博客:http://blogs.msdn.com/clrteam/。
最后,感谢大家对本博客的长期关注和支持!我们期待在事业部的其他博客上继续与各位程序员交流我们关注的技术和产品。
一次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是如何共同作用的。因为现在这个相关项目还没有完全结束,现在暂时我还不能完全透露所有细节,在合适的时间内,我将发布本次文章的续集,并为大家公布答案。
我们先来看一个在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 Framework v4.0发布的新功能中,在名字空间System.Runtime.InteropServices新增加了一个叫做ICustomQueryInterface的Interface, 顾名思义,这个Interface的功能就是使得用户可以自己控制QueryInterface这个COM最常用的函数的行为。在v4.0以前,所有作用于托管组件上的QI行为,都是由CLR内部的IUnkown:QueryInterface控制的,比如,如果你QI著名的IDispatch接口时,你得到的永远都是CLR提供的那个IDispatch,诸如此类的还有IMarshal/IProvideClassInfo等一些常用的Interface。如果你非常希望用自己的IDispatch实现来替换clr提供的实现,那么恭喜你,ICustomQueryInterface就是为你而生的!当然,ICustomQueryInterface所带来的,不仅仅是简单的Interface替换,它甚至可以使得Aggregate托管组件也成为现实,wow,如果你了解Aggregation的话,一定会因此而雀跃不已的。我会在另一篇文章中通过例程给大家做一个详细的介绍。
让我们来看看这个Interface的定义吧
1: public interface ICustomQueryInterface
2: {
3: CustomQueryInterfaceResult GetInterface([In]ref Guid iid, out IntPtr ppv);
4: }
5:
是的,就是这么简单,就一个GetInterface方法,再仔细看看它的方法参数,是不是和c++里面的QueryInterface有点神似啊。哈哈,其实你可以把它理解成QueryInterface的托管实现也无妨啊!不过它还有个小小的功能,就是如果自己不想处理这个QI,就返回NotHandled, clr看到这个返回值,就会调用自己的QI实现来帮你处理这个请求,爽吧。
让我们来看看有了这个Interface之后clr内部关于QI的处理流程图吧(画的是英文版,烦请大家将就一下啦,没装viso,画图超级累啊!!!!)
从这个图上我们可以看到,除了不能处理对IUnknown的QI请求(要求别太高嘛),其他统统OK!
理论一大堆了,来实战一下。
看看我们的托管组件的实现
1: using System;
2: using System.Runtime.InteropServices;
3:
4: namespace States
5: {
6: [Guid("00020400-0000-0000-C000-000000001147")]
7: [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
8: public interface ICQ
9: {
10: int func();
11: void slot2();
12: void slot3();
13: void slot4();
14: }
15:
16: [Guid("11120400-0000-0000-C000-000000001148")]
17: [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
18: public interface IA
19: {
20: int FuncA();
21: }
22:
23: [Guid("22220400-0000-0000-C000-000000001149")]
24: [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
25: public interface IB
26: {
27: int FuncB();
28: }
29:
30:
31:
32: [Guid("00020400-0000-0000-C000-000000001150")]
33: [ClassInterface(ClassInterfaceType.None)]
34: public class StatesComServer : ICustomQueryInterface, ICQ, IA, IB
35: {
36: public readonly Guid IID_IA = new Guid("11120400-0000-0000-C000-000000001148");
37:
38: public CustomQueryInterfaceResult GetInterface([In]ref Guid iid, out IntPtr intf)
39: {
40: if (iid == WellKnownGuids.IID_IDispatch)
41: {
42: intf = Marshal.GetComInterfaceForObject(this, typeof(ICQ), CustomQueryInterfaceMode.Ignore);
43: return CustomQueryInterfaceResult.Handled;
44: }
45:
46: if (iid == IID_IA)
47: {
48: intf = IntPtr.Zero;
49: return CustomQueryInterfaceResult.Failed;
50: }
51:
52: intf = IntPtr.Zero;
53: return CustomQueryInterfaceResult.NotHandled;
54: }
55:
56: public int func()
57: {
58: Console.WriteLine("This is Interface ICQ, not the IDispatch!!!");
59: return 2008;
60: }
61:
62: public int FuncA()
63: {
64: Console.WriteLine("This is Interface IA!!!");
65: return 3008;
66: }
67:
68: public int FuncB()
69: {
70: Console.WriteLine("This is Interface IB!!!");
71: return 4008;
72: }
73:
74:
75: #region Empty Functions
76: public void slot2() { }
77: public void slot3() { }
78: public void slot4() { }
79: #endregion
80: }
81:
82: }
83:
这里有两个地方需要解释一下
1)对于ICQ这个接口,他实际上是作为自定义的IDispatch出现的,所以理论上应该他所有的函数声明应该和IDispatch保持一致,但是个人比较偷懒,毕竟这只是一个例程,主要是将如何使用ICustomQueryInterface这个接口,所以后三个方法的用途仅仅是为了保持ICQ的虚拟函数表(一共4个函数)和IDispatch保持一致,如果真的有人在客户端调IDispatch.Invoke(函数表中的第四个函数)的话,那调用必然会被重定向到ICQ.slot4,因为两个函数的参数声明完全不一样,AccessViolationException就无法避免了。
2)再讲一下GetInterface的返回值,如果是CustomQueryInterfaceResult.Failed,意思是QI失败。CustomQueryInterfaceResult.NotHandled意思是让clr去处理这个请求,CustomQueryInterfaceResult.Handled是告诉clr,已经处理好了,指针值保存在intf里面,直接返回给用户就可以了。
再来看看我们的客户端
IDispatch * pDisp = NULL;
printf("Scenario 1: QI IDispatch interface, Expected the Custom IDispatch interface\n");
hresult = pUnknown->QueryInterface(IID_IDispatch, (void**)&pDisp);
UINT count = 0;
hresult = pDisp->GetTypeInfoCount(&count);
printf("Return value of GetTypeInfoCount is %d\n", count);
IA * pA = NULL;
printf("Scenario 2: QI IA interface, Expected failed\n");
hresult = pUnknown->QueryInterface(IID_IA, (void**)&pA);
if (FAILED(hresult))
{
printf("Failed to QI IA with error code %x\n", count);
}
IB * pB = NULL;
printf("Scenario 3: QI IB interface interface, Expected the IB interface\n");
hresult = pUnknown->QueryInterface(IID_IB, (void**)&pB);
long i = 0;
hresult = pB->FuncB(&i);
再来看看我们的输出结果。
Scenario 1: QI IDispatch interface, Expected the Custom IDispatch interface
This is Interface ICQ, not the IDispatch!!!
Return value of GetTypeInfoCount is 2008
Scenario 2: QI iA interface, Expected failed
Failed to QI IA with error code 7d8
Scenario 3: QI IB interface interface, Expected the IB interface
This is Interface IB!!!
在最新一期的.NET 4.0新特性系列课程中,我将继续给大家介绍CLR 4.0中的一些新特性。被次课程聚焦于性能,具体信息如下:
MSDN Webcast - .NET 4.0中的新特性系列课程(5):追踪CLR——使用Windows事件跟踪(ETW)观察公共语言运行时CLR(Level 300)
讲 师: 朱永泰
开始日期: 2009年8月18日 14:30中国
课程简介: CLR 4.0在开发的过程中,全面支持了Windows事件跟踪。作为一个事件提供者,CLR提供了形形色色的事件,诸如垃圾回收(GC),即时编译(Jit)等等。对CLR事件的追踪可以用来度量CLR的性能,以及帮助用户做错误诊断。本次讲座从Windows事件跟踪的基本概念入手,讲解如何追踪CLR事件,进而观察CLR的一些运作机理。
在公共语言运行时(CLR)过往的版本中,安全模型一直是最为复杂的模块之一,由于涉及Evidence,CAS策略等机制,难以被用户使用。在Silverlight中,CLR团队提出了三层安全级别,大大简化了安全模型,得到了很多积极的反馈。所以CLR4.0对之加以改进,希望能帮助用户开发出更为安全的应用程序。
三层安全级别及其运作机制
CLR4.0中的安全级别,从低到高排列如下:
- Transparent
- SafeCritical
- Critical
其运作机制如下图所示,可以用三个箭头加以说明:
- Transparent的代码可以调用SafeCritical的代码
- SafeCritical的代码可以调用Critical的代码
- Transparent的代码不可以调用Critical的代码

下面的代码展示了安全级别的运作机制:
1: using System;
2: using System.Security;
3:
4: // 这个属性使得assembly中没有Security标记的方法默认为Transparent方法
5: [assembly:AllowPartiallyTrustedCallers]
6:
7: namespace SecurityLevel
8: {
9: public class Program
10: {
11: // 标记Foo为Critical方法。
12: [SecurityCritical]
13: static void Foo()
14: {
15: Console.WriteLine("Hello Foo");
16: }
17:
18: static void Main()
19: {
20: // 这个调用会导致以下的异常:
21: // Unhandled Exception: System.MethodAccessException: SecurityLevel.Program.Foo()
22: // at SecurityLevel.Program.Main()
23: Foo();
24: }
25: }
26: }
Main函数由于没有任何安全属性,而且在assembly上有AllowPartiallyTrustedCallers属性,所以他的安全级别是Transparent,根据前文提及的安全机制,不能直接调用Critical函数Foo,于是命令行上显示了异常信息。
应用安全级别构筑体用程序
CLR提供了这样的一个机制,用户只有正确使用这些机制,才能构筑健壮的应用程序。在这里,“正确的使用”指的是合理的设置函数的安全级别,对三个安全级别设置的指导原则如下:
- Critical:通常用来执行高危操作,比如对文件系统的读写。
- SafeCritical:用来做安全方面的检验,或者只做限制性的操作。
- Transparent:来自任何部分信任的程序代码。
举例来说,我们把三层模型应用到cookie的读写上,最底层可以有一个Critical的函数,用来在文件系统上写一个cookie文件。中间层有一个SafeCritical,用于检验cookie操作的文件是否属于特定的文件夹,就好像做一个安全检查:如果通过了,则允许操作;否则的话就拒绝之。
下面的例子展示了一个简化后的应用,从D盘的temp文件夹中删除文件。请参见代码中的注释理解程序。
1: using System;
2: using System.IO;
3: using System.Security;
4:
5: // 这个属性使得assembly没有Security标记的方法默认为Transparent方法
6: [assembly:AllowPartiallyTrustedCallers]
7:
8: namespace SecurityLevel
9: {
10: public class Program
11: {
12: /// <summary>
13: /// 该函数可以删除文件系统上的任意函数。具有最高的安全级别
14: /// </summary>
15: /// <param name="fileName">文件名</param>
16: [SecurityCritical]
17: static void DeleteFile(string fileName)
18: {
19: File.Delete(fileName);
20: }
21:
22: /// <summary>
23: /// 该函数验证待删文件是否在d:\temp中
24: /// </summary>
25: /// <param name="fileName">待删文件名</param>
26: [SecuritySafeCritical]
27: static void DeleteFileFromTemp(string fileName)
28: {
29: if (fileName.StartsWith(@"d:\temp", StringComparison.CurrentCultureIgnoreCase)
30: {
31: DeleteFile(fileName);
32: }
33: else
34: {
35: throw new Exception("待删文件不在temp文件夹中");
36: }
37: }
38:
39: static void Main()
40: {
41: // 该语句运行正常
42: DeleteFileFromTemp(@"d:\temp\a.txt");
43:
44: // 该语句抛出异常
45: DeleteFileFromTemp(@"d:\a.txt");
46: }
47: }
48: }
49:
安全级别和.NET类型系统
理解了安全级别的应用之后,我们来看看安全级别和.NET类型系统之间的关系:
- 安全级别和反射
反射机制提供了这样三个属性来探测一个类型(Type)和方法(MethodInfo)的安全级别
- IsSecurityCritical { get; }
- IsSecuritySafeCritical { get; }
- IsSecurityTransparent { get; }
大家可以观察到,这三个属性是只读的,因为通常境况下,编译器会写入了相关信息。
- 安全级别和继承
以下两点值得注意:关于类型,子类型的安全级别必须等于或者高于父类型的安全级别;关于方法,继承的方法不能改变基类型方法的安全级别
- 安全级别和委托(Delegate)
调用者不能创建一个安全级别更高的Delegate,也不能创建一个指向安全级别更高方法的Delegate。
小结
本文介绍了CLR4.0中引入的三层安全级别以及运作机制,示例了安全级别的设置原则,讲述了安全级别和类型系统的关联。
该工具主要帮助大家诊断Interop Marshalling中间遇到的问题。它的源代码和二进制文件可以在CodePlex上下载:http://clrinterop.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=29745
本文首先介绍关于Interop Marshalling的背景,然后通过一个例子来介绍IL Stub Diagnostic工具的使用。
Interop Marshalling: 当我们在托管代码中调用本地代码或者反过来在本地代码中调用托管代码的时候,我们就在做.Net Interop,如下图所示。当我们在托管代码和本地代码之间传递数据的时候,我们就需要Marshalling。长久以来,Marshalling一直是错误的温床。程序员需要通过设置一些列的属性来告诉CLR他们期待的marshalling逻辑,由于看不到CLR生成的具体代码(当然,在调试器里面可以看到汇编代码),所以和Marshalling相关的错误总是难于诊断。我们这次在codeplex上发布的工具就力图解决这个问题。
Intermediate Language(IL) Stub是由CLR动态生成的用于Interop的代码。它主要做两件事情:1. 在本地代码和托管代码之间Marshal数据;2. 调用目标函数。张羿撰写的CLR 4.0 Beta1新功能:Stub Method Redirection一文中,也对IL Stub做了相关的介绍。
综合以上两点,大家多半可以猜到IL Stub Diagnostics的工作方式:该工具可以实时监测CLR生成的IL Stub,并且把stub的IL代码展示给用户。根据这些信息来诊断Marshalling过程中的错误。
下载文件介绍
本次发行包括了两个下载文件,一个是源代码,留给有兴趣的朋友们自己研究。这里主要介绍二进制文件。解压缩之后有两个文件夹和一个文档。ILStubDiagnostic包含了本工具的可执行文件,Sample中是一个pinvoke的例子,用以展示IL Stub Diagnostic工具的功能。Tutorial是一个简单的教程,由于面向全球用户,用英语撰写,在这里会给出中文对应。
运行环境介绍
由于使用了CLR v4以及ETW的新功能,所以被监测的托管程序必须在安装了v4 CLR的vista(或者win2008、win7)上运行。
例子
1) 启动IL Stub Diagnostic之后,看到如下界面:
2) 单击Start按钮之后,看到界面的右上角有一个环形动画,表示IL Stub Diagnostic正在监测中。
3) 运行sample文件夹下的pinvoke.exe,我们会在IL Stub列表区中得到一些IL Stub项。需要注意的是,由于pinvoke.exe调用的本地代码是32位,所以它需要运行的32位CLR上。关于32位/64位CLR的背景,可以参见我的博文:在64位windows中运行托管程序。
4)在IL列表区右击任何一列的标题,可以选择显示不同的IL Stub信息
5) 在IL列表区单击任何一个感兴趣的stub,在代码区可以看到具体内容。
6) 在工具栏里单击IL Code,可以把代码区最大化。
7) 初次见到IL代码的朋友可能会有些茫然,在工具下方有一组导航按钮帮你找着北。单击“Next Block”和“Previous Block”可以在看到不同的代码块,相关的注释暗示了代码的用途。
8) 在IL Stub代码中,我们用到了StubHelpers的API,并且用下划线标明,把鼠标移到他们上面可以看到相关的解释。
9) 有的时候,一个程序会使用多个IL Stub,界面中的过滤器区域可以帮助大家更迅速的定位自己感兴趣的Stub:
a. 如下图所示,设置过滤器
b. 点击工具栏上的Refresh按钮,IL Stub列表区会根据设置的过滤器更新IL Stub项。同时,过滤器列表中的文字也会被更新。
c. 如果要删除过滤器,可以右击行首,选择“Delete Filter”,并点击工具栏上的Refresh按钮。
今天的介绍就到这里,欢迎大家给我们提供关于这个工具的使用反馈,我们会继续维护并且改进这个工具。
在最新一期的.NET 4.0新特性系列课程中,我们将继续给大家介绍CLR 4.0中的一些新特性,具体信息如下:
MSDN Webcast - .NET 4.0中的新特性系列课程(4):.NET 4.0 安全模型介绍(Level 200)
讲 师: 朱永泰
开始日期: 2009年7月21日 14:30中国
课程简介:CLR 4在安全领域的主要工作室简化安全模型,以便让用户可以更方便的使用。本次讲座会从CLR对自身在整个安全架构的定位谈起,介绍如何提供一个安全沙箱(SandBox),来构筑应用程序。
在最新一期的.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:
作者:张羿
转载请注明出处
.NET 4中发布了最新版本的公共语言运行时,简称CLR (Common Language Runtime) 。这个版本是CLR 2.0之后又一个新的版本,包含着CLR小组几年以来的辛勤工作。
CLR上海团队计划在未来的几个月内陆续介绍其中的一些特性,本文作为一个概览,先作蜻蜓点水,抛砖引玉。也欢迎大家回复本文,告诉我们你所感兴趣的话题,我们会进一步作深入的介绍。
CLR 简介
CLR作为.NET框架中最为底层的部件,扮演着运行托管代码虚拟机的角色,承担着诸如即时编译(Just In Time Compile),垃圾回收(Garbage Collect)等任务。打一个比方,如果把操作系统看做是运行二进制程序的宿主,那么CLR就是托管世界的操作系统。
图一 CLR 在.NET框架中所处的位置
CLR作为.NET框架中的一部分,总是跟着.NET发行,但是近年来.NET的发行版本从2.0一直到3.5, 但是CLR却还一直保留在2.0,如下表所示:
|
.NET框架版本 |
时间 |
CLR |
|
1.0 |
2002.2 |
1.0 |
|
1.1 |
2003.4 |
1.1 |
|
2.0 (Generics) |
2006.1 |
2.0 |
|
3.0 (WPF/WCF/WF) |
2006.11 |
2.0 |
|
3.5 (LINQ) |
2007.11 |
2.0 |
|
4.0 Beta |
2009.5 |
4.0 |
图二 CLR 版本
大家可以看到,2.0的发行已经是三年之前的事情了,在这几年中,CLR小组的工作最后都汇集在了这次发行之中,可谓是众星云集,下面我们一一叙来。
托管与本地代码的互操作
托管代码与本地代码之间的互操作(interop)担负着.NET世界对外联系的责任。比如调用一个本地dll或者COM组件。在CLR 4中,我们作了以下工作,来提高互操作的易用性。
网络广播:http://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032417433&Culture=zh-CN
1. 自定义QI(Custom QI)
当托管代码被COM调用的时候,它扮演着COM组件的角色。对于COM组件来说,IUnknown::QueryInerface(QI)是类型转化的关键。CLR4之前,为每个托管COM组件提供了一个QI实现; CLR4 允许用户自定义QI,大家可以从mscorlib中新增的interface,System.Runtime.InteropServices.ICustomQueryInterface着手了解这一新功能。
2. TlbImp源代码以及自定义工具
在托管代码中调用COM组件,需要这个COM组件用托管语言申明自己的接口,也就是Interop Assembly(IA)。在一般情况下,用户不需要自己动手撰写这些assembly,而可以使用TlbImp这个工具,根据TLB生成IA。在CLR 4的开发中,我们用托管代码把TlbImp重写了,并且把源代码公布在了codeplex上面。
发布TlbImp的源代码的好处之一,是方便使用者根据自己的需求,通过修改源代码来自拓展TlbImp的功能。我们也收集了很多客户需要自定义TlbImp的要求,并且提取了一些呼声最高的自定义请求,制作了TlbImp自定义工具,也在codeplex发行。详见http://blogs.msdn.com/silverlightshanghai/archive/2009/03/13/codeplex-tlbimp.aspx
3. 等价类型
前面提到,COM组件要为.NET所用,需要Interop Assembly。不同版本的COM组件,带来了部署上的问题。在CLR 4.0之中,我们通过等价类型的引入,就部署IA的问题,给出了更好的解决方案。
4. StubMethodReditection
自定义Stub来处理Interop中的Marshalling和目标函数调用;
5. 其他
Interop其他方面的改动,使用COM取代了原先的远程对象访问;让用户自己决定清理RCW的时机等等,会有更为详细的博文作具体介绍。
垃圾回收
垃圾回收一直是CLR中的核心模块,对托管程序运行的性能至关重要。在这个版本中,CLR引入了background GC,和原来的Concurrent GC相比,在GC进行的过程中,会更少的阻断其他进程,从而提高整个CLR的运行效率。同时,此前在sp2中引入的GC::RegisterForFullGCNotification可以让 CLR4.0可以通知用户第二代GC发生,从而使服务器有机会处理负载平衡,使得整个服务器端的处理能力不至于因为GC的发生受到太大的影响。
代码约定
在CLR4.0中,引入了代码约定,更方便用户规范代码的行为,大家可以从System.Diagnostics.Contracts这一命名空间着手,进一步了解其内容。
Corrupted state exception
CLR 4.0中,对异常处理的哲学有了一个改进:在默认情况下,try/catch语句将不能捕获诸如AccessViolationException等异常。因为这些异常的损毁(Corrupt)了机器的状态(state),即使用户捕获了它们,也无法继续执行代码,或者说,继续执行代码也会变得非常危险。
新的安全模型
用过CLR v2的安全模型的朋友们可能还会记得诸如Evidence,Policy以及Permission等概念,这些复杂的对象一起构筑了v2的安全模型的框架,CLR4.0中,安全模型被大大简化,SecurityCritical,SecurSafeCritical等一些安全级别构筑了新的安全模型的基础。
同一个进程,多个CLR
CLR4.0的出现,又添加了一个CLR的版本,尽管我们尽量保证各个不同版本之间的兼容性,但是还是可能出现一些已经开发的组件,需要特定的版本才能运行。为了确保用户过去编写的组件不会因为新的CLR版本而不能运行,CLR4.0中允许用户在一个进程中,运行不同的CLR版本,这样不同的组建就可以各取所需,运行在适合他们的CLR中了。
基本类库
基本类库,也就是mscorlib.dll,包括了诸如System.Object这样在整个类型系统中最为核心的类库。CLR4.0也包含了很多新功能:比如用于支持动态语言的System.Tuple,新的集合类型System.Collections.Generic.SortedSet,用于提高文件系统浏览性能的API,操作注册表的API,以及对内存映射文件的支持等等。
总的来说,CLR4.0相较于CLR2.0,在保证了很高的兼容性的同时,做了大量的改进工作,在之后的一系列博客中,我们团队的成员会进一步作更为具体的介绍,敬请大家期待。
1. 字符串Marshalling基础
在本地代码中,字符串按编码可分为ASNI字符串和Unicode字符串,按实现不同,可分为LPSTR(char*,wchar*)和BSTR。
在托管代码中,与字符串相关的有String,stringBuilder两个。
为了实现本地代码与托管代码中字符串的相互转换,Marshalling 引入了几个属性值。
BSTR系列
UnmanagedType.AnsiBStr
UnmanagedType.BStr [COM Interop中为默认值]
UnmanagedTYpe.TBStr
LPSTR系列:
UnmanagedType.LPStr
UnmanagedType.LPWStr
UnmanagedType.LPTStr[批女哦棵中为默认值]
2.字符串的内存操作
本地代码相关函数:CoTaskMemAlloc,CoTaskMemFree
本地代码相关函数:SysAllocString,SysFreeString
3. String和StringBuilder在Marshalling中的区别
在Marshalling中,如果你要传递的字符串仅仅需要在原有的字符串中改变某个或某几个字符(in-place change),在这种情况下,你就最好在托管代码中使用StringBuilder,而不是string。
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的新功能,尽请期待!
最近在论坛上经常看到一些基本的interop的问题,给我动力写完之前的.net interop入门系列,给刚刚涉足.NET interop的朋友们一个大体上的概念。
每每谈及.NET interop,我的脑中总是出现下面一幅图:
该图代表了.net interop的四个典型场景。之前我的同事和我讨论了.NET和COM互操作的应用:
今天我主要讲一下P/Invoke和Reverse P/Invoke,和COM interop相比,P/Invoke无需注册组件,使用上更轻量,更绿色。
1. P/Invoke
P/Invoke(platform invoke)是.NET调用本地代码(native code)的一种比较轻便的方式。只需要将本地代码编写成动态链接库,然后在c#代码中,声明一个外部静态函数,并且用DllImport属性指明动态连接库的入口。举例如下:
using System;
using System.Runtime.InteropServices;
class PInvoke
{
[DllImportAttribute("user32.dll", EntryPoint = "MessageBoxW")]
public static extern int MessageBoxW(
[In]System.IntPtr hWnd,
[In][MarshalAs(UnmanagedType.LPWStr)] string lpText,
[In][MarshalAs(UnmanagedType.LPWStr)] string lpCaption,
uint uType);
public static void Main()
{
MessageBoxW(IntPtr.Zero, "Hello", "Interop", 0);
}
}
稍加解释这个代码。类PInvoke中,有个MessageBoxW的函数声明,它的实现在user32.dll(系统自带)中,入口是MessageBoxW,参数的构成是根据windows API的声明而定的,我们在Codeplex上有一个工具,专门帮助大家声称一个本地代码(c++)编写的函数在托过代码(c#)中的函数声明,之前我们团队的成员也撰文介绍了这个工具的使用。
有了这个声明以后,在Main中调用MessageBox,就和调用其他托管代码一样轻松自如了。
2. Reverse P/Invoke
接着,我们来看看在本地代码中调用.NET方法。本地代码需要拿到一个.NET委托(delegate),然后把这个delegate当作一个函数指针使用,示例如下:
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public class Program
{
internal delegate void DelegateMessageBox([MarshalAs(UnmanagedType.LPWStr)]string msg);
[DllImport("Native.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void NativeMethod(DelegateMessageBox d);
public static void ShowMessageBox(string msg)
{
MessageBox.Show(msg);
}
public static void Main()
{
NativeMethod(new DelegateMessageBox(ShowMessageBox));
}
}
这个例子中,我们希望本地代码能够调用托管函数ShowMessageBox来显示一个对话框。为了让本地代码可以调用这个函数,我们根据它的声明,定了了一个delegate,并且通过P/Invoke把这个委托传给了本地代码。本地代码可以如下调用托管代码:
#include <stdio.h>
#include <wtypes.h>
extern "C" {
__declspec(dllexport) void NativeMethod(void (__stdcall *pShowMsgBox)(WCHAR *wChar))
{
(*pShowMsgBox)(L"hello reverse interop");
}
}
注意到托管代码中的委托到了本地代码中,就是一个函数指针,本地代码可以像一个普通的函数指针一般调用托管代码。
大家可能注意到dll的声明用了extern “C”,它指明了调用规范是cdecl,在之前的托过代码的DllImport中,也相应的注明了调用约定,关于调用约定的详细介绍,可以参见我的另一篇博客。
今天的介绍就到这里,大家可以把这些示例代码当作一个template,根据实际需求作相应的具体改动。