我们先来看一个在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平台开发程序,而不用过于担心兼容性方面的风险。