.NET 4.0新功能介绍:In Process Side By Side

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