我们先来看一个在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是可以多个并存的,也就是说V4.0,V5.0,v6.0,等等,都是可以和平共处在同一个进程内。原因很简单,>4.0的CLR是In-Proc SxS Aware的。
前面提到过,总是加载最新版本的CLR这种方式是存在问题的,因为新版本不可能完全兼容旧版本,因此,保持兼容性的最佳方式是不允许“加载最新”(Bind to latest)这种方式存在,换句话说,为v4写的程序缺省应该总是在v4上运行,而不应该自动被“提升”至V5上运行。
因为
1. 老程序的行为必须和原来保持一致,这包括已有程序的加载和已有的Hosting API
2. <v4.0>=v4.0的CLR,因为它们生活在两个不同的世界中</v4.0>
3. 已有的Hosting API只允许加载一个<v4.0><p>可以看到,已有的HostingAPI因为没有设计成支持In-Proc SxS,在v4.0的时候会面临淘汰。而在v4.0的时候,v4.0的CLR(其实严格来说是Shim,也就是mscoree.dll)必须得有一套新的API。</p> <p><b><u>CLR</u></b><b><u>所做的修改</u></b><b><u></u></b></p> <p>从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了。</p> <p>除此之外,CLR的代码也做出了不少的改变,比较主要的有:</p> <p>1. 修改对全局的共享资源的使用。比如原来总是用一个固定名字的Mutex或者和进程名字相关的临时文件,现在这些代码必须得要修改了,要和该CLR的实例绑定起来(比如和首地址)。</p> <p>2. 修改对于其他CLR的DLL的加载和查找。以前也许可以写FindModule(“mscorwks.dll”),现在不能这么写了,而是通过其他方法来查找(比如注册表)。</p> <p>3. 对于版本号的一些假设。原来可以直接处理任何版本的代码,现在也许需要分情况处理<v4.0>=v4.0。</v4.0></p> <p>4. 对于旧的Hosting API,修改其实现使之无法加载v4.0的CLR,但是又可以和v4的CLR共处而不会出问题</p> <p>5. 增加新的API,支持In-Proc SxS</p> <p>6. Activation,也就是CLR的启动的Logic基本上重写,为了处理v1.1、v2.0、v4.0之间的各种不同的SxS或者非SxS的情况。</p> <p>本文因为不是剖析v4.0中SxS实现的文章,对于CLR本身的修改也就到这里点到为止。不过,如果你的程序也有类似的问题,那么你的程序可能也要修改才可以支持SxS了。</p> <p><b><u>Activation Policies</u></b></p> <p>这里所说的Activation Policies,指的是加载CLR的一些规则,知道了这些规则,才可以很好的在v4.0的CLR下使用SxS。这里所需要讨论的Activation被分成三种不同的情况:</p> <p><b>Application Activation</b></p> <p>这里说的Application Activation就是普通的执行一个EXE程序。规则最简单来说是这样:</p> <p>1. >= 4.0的EXE总是运行在EXE所被编译的CLR版本上</p> <p>2. </p> <p>我们来看几个例子: </p> <table border="1" cellspacing="0" cellpadding="0"><tbody> <tr> <td valign="top" width="164"> <p><b>EXE</b><b>被编译的</b><b>CLR</b><b>版本号</b><b></b></p> </td> <td valign="top" width="132"> <p><b>机器上安装有CLR 1.1</b><b>?</b><b></b></p> </td> <td valign="top" width="132"> <p><b>机器上安装有CLR 2.0?</b></p> </td> <td valign="top" width="132"> <p><b>机器上安装有CLR 4.0?</b></p> </td> <td valign="top" width="132"> <p><b>结果</b><b></b></p> </td> </tr> <tr> <td valign="top" width="164"> <p><b>1.1</b></p> </td> <td valign="top" width="132"> <p>是</p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>加载CLR 1.1</p> </td> </tr> <tr> <td valign="top" width="164"> <p><b>2.0</b></p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>是</p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>加载CLR 2.0</p> </td> </tr> <tr> <td valign="top" width="164"> <p><b>1.1</b></p> </td> <td valign="top" width="132"> <p>否</p> </td> <td valign="top" width="132"> <p>是</p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>加载CLR 2.0</p> </td> </tr> <tr> <td valign="top" width="164"> <p><b>1.1</b></p> </td> <td valign="top" width="132"> <p>否</p> </td> <td valign="top" width="132"> <p>否</p> </td> <td valign="top" width="132"> <p>是</p> </td> <td valign="top" width="132"> <p>失败</p> </td> </tr> <tr> <td valign="top" width="164"> <p><b>2.0</b></p> </td> <td valign="top" width="132"> <p>无所谓</p> </td> <td valign="top" width="132"> <p>否</p> </td> <td valign="top" width="132"> <p>是</p> </td> <td valign="top" width="132"> <p>失败</p> </td> </tr> </tbody></table></v4.0>
怎么看一个EXE被编译的CLR版本号?很简单,使用CorFlags就可以了:
C:\Windows\Microsoft.NET\Framework\v2.0.50727>corflags regasm.exeMicrosoft (R) .NET Framework CorFlags Conversion Tool. Version 4.0.20818.0Copyright (c) Microsoft Corporation. All rights reserved.Version : v2.0.50727CLR Header: 2.5PE : PE32CorFlags : 11ILONLY : 132BIT : 1Signed : 1
COM Activation
COM Activation指的是本地代码创建一个基于托管代码的COM对象,也就是通常所说的CCW。在最新的CLR V4中,所有的托管的COM对象都必须绑定到它所被编译的CLR版本,除非:
1. 注册表中SupportedRuntimeVersions中有大于该版本的版本号(注意RuntimeVersion这个只是用来做一个很简单的检查,如果当前的最新CLR版本小于这个值则出错。并不代表说一定要在这个特定的CLR版本中加载)
2. 如果进程中已经加载了一个
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></stdio.h>2: #include <corerror.h></corerror.h>3: #include <metahost.h></metahost.h>4: #include <windows.h></windows.h>5: #include <atlbase.h></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; </iclrmetahost>14: HRESULT hr = CreateInterface(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMH));15: IfFailReturnHr(L"Create Instance for ICLRMetaHost failed.");
16:17: CComPtr<iclrruntimeinfo> pRuntime = NULL; </iclrruntimeinfo>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; </iclrruntimehost3>29: hr = pRuntime->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pHost));30: IfFailReturnHr(L"GetRuntime failed.");
31:32: CComPtr<iunknown> pAddin; </iunknown>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.50727Runtime directory=C:\Windows\Microsoft.NET\Framework\v2.0.50727\Addin V2: I'm running in CLR v2.0.50727Getting runtime host interface for v4.0.20506Runtime 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平台开发程序,而不用过于担心兼容性方面的风险。