.NET 4.0 新特性:
1. In Process Side By Side(进程内SxS)
2. Type Equivalency (等价类型)
3. 其他CLR V4 新特性
4. Contracts(契约式编程)
5. 其他BCL新特性
.NET 4.0 中的契约式编程
契约式编程不是一门崭新的编程方法论。C/C++ 时代早已有之。Microsoft 在 .NET 4.0 中正式引入契约式编程库。契约式编程是一种相当不错的编程思想,每一个开发人员都应该掌握。它不但可以使开发人员的思维更清晰,而且对于提高程序性能很有帮助。值得一提的是,它对于并行程序设计也有莫大的益处。
我们先看一段很简单的,未使用契约式编程的代码示例。
// .NET 代码示例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
return this.denominator;
}
}
}
上述代码表示一个在 32 位有符号整型范围内的有理数。数学上,有理数是一个整数 a 和一个非零整数 b 的比,通常写作 a/b,故又称作分数。由此,我们知道,有理数的分母不能为 0 。所以,上述代码示例的构造函数还需要写些防御性代码。通常 .NET 开发人员会这样写:
// .NET 代码示例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
if (denominator == 0)
throw new ArgumentException("The second argument can not be zero.");
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
return this.denominator;
}
}
}
下面我们来看一下使用契约式编程的 .NET 4.0 代码示例。为了更加方便的说明,在整个示例上都加了契约,但此示例并非一定都加这些契约。
// .NET 代码示例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
Contract.Requires(denominator != 0, "The second argument can not be zero.");
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
Contract.Ensures(Contract.Result<int>() != 0);
return this.denominator;
}
}
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(this.denominator != 0);
}
}
在 .NET 4.0 中,契约式编程变得简单优雅。.NET 4.0 提供了契约式编程库,它完全基于 CONTRACTS_FULL, CONTRACTS_PRECONDITIONS Symbol 和 System.Diagnostics.Debug.Assert 方法、System.Environment.FastFail 方法的封装。
何谓契约式编程
契约是减少大型项目成本的突破性技术。它一般由 Precondition(前置条件), Postcondition(后置条件) 和 Invariant(不变量) 等概念组成。.NET 4.0 除上述概念之外,还增加了 Assert(断言),Assume(假设) 概念。
契约的思想很简单。它只是一组结果为真的表达式。如若不然,契约就被违反。那按照定义,程序中就存在纰漏。契约构成了程序规格说明的一部分,只不过该说明从文档挪到了代码中。开发人员都知道,文档通常不完整、过时,甚至不存在。将契约挪移到代码中,就使得程序可以被验证。
1. Assert
Assert(断言)是最基本的契约。.NET 4.0 使用 Contract.Assert() 方法来特指断言。它用来表示程序点必须保持的一个契约。
Contract.Assert(this.privateField > 0);
Contract.Assert(this.x == 3, "Why isn’t the value of x 3?");
断言有两个重载方法,首参数都是一个布尔表达式,第二个方法的第二个参数表示违反契约时的异常信息。
当断言运行时失败,.NET CLR 仅仅调用 Debug.Assert 方法。成功时则什么也不做。
2. Assume
.NET 4.0 使用 Contract.Assume() 方法表示 Assume(假设) 契约。
Contract.Assume(this.privateField > 0);
Contract.Assume(this.x == 3, "Static checker assumed this");
Assume 契约在运行时检测的行为与 Assert(断言) 契约完全一致。但对于静态验证来说,Assume 契约仅仅验证已添加的事实。由于诸多限制,静态验证并不能保证该契约。或许最好先使用 Assert 契约,然后在验证代码时按需修改。
当 Assume 契约运行时失败时, .NET CLR 会调用 Debug.Assert(false)。同样,成功时什么也不做。
3. Preconditions
.NET 4.0 使用 Contract.Requires() 方法表示 Preconditions(前置条件) 契约。它表示方法被调用时方法状态的契约,通常被用来做参数验证。所有 Preconditions 契约相关成员,至少方法本身可以访问。
Contract.Requires(x != null);
Preconditions 契约的运行时行为依赖于几个因素。如果只隐式定义了 CONTRACTS PRECONDITIONS 标记,而没有定义 CONTRACTS_FULL 标记,那么只会进行检测 Preconditions 契约,而不会检测任何 Postconditions 和 Invariants 契约。假如违反了 Preconditions 契约,那么 CLR 会调用 Debug.Assert(false) 和 Environment.FastFail 方法。
假如想保证 Preconditions 契约在任何编译中都发挥作用,可以使用下面这个方法:
Contract.RequiresAlways(x != null);
为了保持向后兼容性,当已存在的代码不允许被修改时,我们需要抛出指定的精确异常。但是在 Preconditions 契约中,有一些格式上的限定。如下代码所示:
if (x == null) throw new ArgumentException("The argument can not be null.");
Contract.EndContractBlock(); // 前面所有的 if 检测语句皆是 Preconditions 契约
这种 Preconditions 契约的格式严格受限:它必须严格按照上述代码示例格式。而且不能有 else 从句。此外,then 从句也只能有单个 throw 语句。最后必须使用 Contract.EndContractBlock() 方法来标记 Preconditions 契约结束。
看到这里,是不是觉得大多数参数验证都可以被 Preconditions 契约替代?没有错,事实的确如此。这样这些防御性代码完全可以在 Release 被去掉,从而不用做那些冗余的代码检测,从而提高程序性能。但在面向验证客户输入此类情境下,防御性代码仍有必要。再就是,Microsoft 为了保持兼容性,并没有用 Preconditions 契约代替异常。
4. Postconditions
Postconditions 契约表示方法终止时的状态。它跟 Preconditions 契约的运行时行为完全一致。但与 Preconditions 契约不同,Postconditions 契约相关的成员有着更少的可见性。客户程序或许不会理解或使用 Postconditions 契约表示的信息,但这并不影响客户程序正确使用 API 。
对于 Preconditions 契约来说,它则对客户程序有副作用:不能保证客户程序不违反 Preconditions 契约。
A. 标准 Postconditions 契约用法
.NET 4.0 使用 Contract.Ensures() 方法表示标准 Postconditions 契约用法。它表示方法正常终止时必须保持的契约。
Contract.Ensures(this.F > 0);
B. 特殊 Postconditions 契约用法
当从方法体内抛出一个特定异常时,通常情况下 .NET CLR 会从方法体内抛出异常的位置直接跳出,从而辗转堆栈进行异常处理。假如我们需要在异常抛出时还要进行 Postconditions 契约验证,我们可以如下使用:
Contract.EnsuresOnThrows
其中小括号内的参数表示当异常从方法内抛出时必须保持的契约,而泛型参数表示异常发生时抛出的异常类型。举例来说,当我们把 T 用 Exception 表示时,无论什么类型的异常被抛出,都能保证 Postconditions 契约。哪怕这个异常是堆栈溢出或任何不能控制的异常。强烈推荐当异常是被调用 API 一部分时,使用 Contract.EnsuresOnThrows
C. Postconditions 契约内的特殊方法
以下要讲的这几个特殊方法仅限使用在 Postconditions 契约内。
方法返回值 在 Postconditions 契约内,可以通过 Contract.Result
Contract.Ensures(0 < Contract.Result<int>());
假如方法返回 void ,则不必在 Postconditions 契约内使用 Contract.Result
前值(旧值) 在 Postconditions 契约内,通过 Contract.OldValue
1. 方法的旧有状态必定存在其值。比如 Preconditions 契约暗含 xs != null ,xs 当然可以被计算。但是,假如 Preconditions 契约为 xs != null || E(E 为任意表达式),那么 xs 就有可能不能被计算。
Contract.OldValue(xs.Length); // 很可能错误
2. 方法返回值不能被旧有表达式引用。
Contract.OldValue(Contract.Result<int>() + x); // 错误
3. out 参数也不能被旧有表达式引用。
如果某些标记的方法依赖方法返回值,那么这些方法也不能被旧有表达式引用。
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // 错误
旧有表达式不能在 Contract.ForAll() 和 Contract.Exists() 方法内引用匿名委托参数,除非旧有表达式被用作索引器或方法调用参数。
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK
Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // 错误
4. 如果旧有表达式依赖于匿名委托的参数,那么旧有表达式不能在匿名委托的方法体内。除非匿名委托是 Contract.ForAll() 和 Contract.Exists() 方法的参数。
Foo( ... (T t) => Contract.OldValue(... t ...) ... ); // 错误
D. out 参数
因为契约出现在方法体前面,所以大多数编译器不允许在 Postconditions 契约内引用 out 参数。为了绕开这个问题,.NET 契约库提供了 Contract.ValueAtReturn
public void OutParam(out int x)
{
Contract.Ensures(Contract.ValueAtReturn(out x) == 3);
x = 3;
}
跟 OldValue 一样,当编译器能推导出类型时,泛型参数可以被忽略。该方法只能出现在 Postconditions 契约。方法参数必须是 out 参数,且不允许使用表达式。
需要注意的是,.NET 目前的工具不能检测确保 out 参数是否正确初始化,而不管它是否在 Postconditions 契约内。因此, x = 3 语句假如被赋予其他值时,编译器并不能发现错误。但是,当编译 Release 版本时,编译器将发现该问题。
5. Object Invariants
对象不变量表示无论对象是否对客户程序可见,类的每一个实例都应该保持的契约。它表示对象处于一个“良好”状态。
在 .NET 4.0 中,对象的所有不变量都应当放入一个受保护的返回 void 的实例方法中。同时用[ContractInvariantMethod]特性标记该方法。此外,该方法体内在调用一系列 Contract.Invariant() 方法后不能再有其他代码。通常我们会把该方法命名为 ObjectInvariant 。
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
}
同样,Object Invariants 契约的运行时行为和 Preconditions 契约、Postconditions 契约行为一致。CLR 运行时会在每个公共方法末端检测 Object Invariants 契约,但不会检测对象终结器或任何实现 System.IDisposable 接口的方法。
6. Contract 静态类中的其他特殊方法
.NET 4.0 契约库中的 Contract 静态类还提供了几个特殊的方法。它们分别是:
A. ForAll
Contract.ForAll() 方法有两个重载。第一个重载有两个参数:一个集合和一个谓词。谓词表示返回布尔值的一元方法,且该谓词应用于集合中的每一个元素。任何一个元素让谓词返回 false ,ForAll 停止迭代并返回 false 。否则, ForAll 返回 true 。下面是一个数组内所有元素都不能为 null 的契约示例:
public T[] Foo
{
Contract.Requires(Contract.ForAll(array, (T x) => x != null));
}
B. Exists
它和 ForAll 方法差不多。
7. 接口契约
因为 C#/VB 编译器不允许接口内的方法带有实现代码,所以我们如果想在接口中实现契约,需要创建一个帮助类。接口和契约帮助类通过一对特性来链接。如下所示:
[ContractClass(typeof(IFooContract))]
interface IFoo
{
int Count { get; }
void Put(int value);
}
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo
{
int IFoo.Count
{
get
{
Contract.Ensures(Contract.Result<int>() >= 0);
return default(int); // dummy return
}
}
void IFoo.Put(int value)
{
Contract.Requires(value >= 0);
}
}
.NET 需要显式如上述声明从而把接口和接口方法相关联起来。注意,我们不得不产生一个哑元返回值。最简单的方式就是返回 default(T),不要使用 Contract.Result
由于 .NET 要求显式实现接口方法,所以在契约内引用相同接口的其他方法就显得很笨拙。由此,.NET 允许在契约方法之前,使用一个局部变量引用接口类型。如下所示:
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo
{
int IFoo.Count
{
get
{
Contract.Ensures(Contract.Result<int>() >= 0);
return default(int); // dummy return
}
}
void IFoo.Put(int value)
{
IFoo iFoo = this;
Contract.Requires(value >= 0);
Contract.Requires(iFoo.Count < 10); // 否则的话,就需要强制转型 ((IFoo)this).Count
}
}
8. 抽象方法契约
同接口类似,.NET 中抽象类中的抽象方法也不能包含方法体。所以同接口契约一样,需要帮助类来完成契约。
9. 契约方法重载
所有的契约方法都有一个带有 string 类型参数的重载版本。如下所示:
Contract.Requires(obj != null, "if obj is null, then missiles are fired!");
这样当契约被违反时,.NET 可以在运行时提供一个信息提示。目前,该字符串只能是编译时常量。但是,将来 .NET 可能会改变,字符串可以运行时被计算。但是,如果是字符串常量,静态诊断工具可以选择显示它。
10. 契约特性
A. ContractClass 和 ContractClassFor
这两个特性,我们已经在接口契约和抽象方法契约里看到了。ContractClass 特性用于添加到接口或抽象类型上,但是指向的却是实现该类型的帮助类。ContractClassFor 特性用来添加到帮助类上,指向我们需要契约验证的接口或抽象类型。
B. ContractInvariantMethod
这个特性用来标记表示对象不变量的方法。
C. Pure
Pure 特性只声明在那些没有副作用的方法调用者上。.NET 现存的一些委托可以被认为如此,比如 System.Predicate
D. RuntimeContracts
这是个程序集级别的特性(具体如何,俺也不太清楚)。
E. ContractPublicPropertyName
这个特性用在字段上。它被用在方法契约中,且该方法相对于字段来说,更具可见性。比如私有字段和公共方法。如下所示:
[ContractPublicPropertyName("PublicProperty")]
private int field;
public int PublicProperty { get { ... } }
F. ContractVerification
这个特性用来假设程序集、类型、成员是否可被验证执行。我们可以使用 [ContractVerification(false)] 来显式标记程序集、类型、成员不被验证执行。
.NET 契约库目前的缺陷
接下来,讲一讲 .NET 契约库目前所存在的一些问题。
1. 值类型中的不变量是被忽略的,不发挥作用。
2. 静态检测还不能处理 Contract.ForAll() 和 Contract.Exists() 方法。
3. C# 迭代器中的契约问题。我们知道 Microsoft 在 C# 2.0 中添加了 yield 关键字来帮助我们完成迭代功能。它其实是 C# 编译器做的糖果。现在契约中,出现了问题。编译器产生的代码会把我们写的契约放入到 MoveNext() 方法中。这个时侯,静态检测就不能保证能够正确完成 Preconditions 契约。
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.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.
3. 已有的Hosting API只允许加载一个
可以看到,已有的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. 对于版本号的一些假设。原来可以直接处理任何版本的代码,现在也许需要分情况处理
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
2: #include
3: #include
4: #include
5: #include
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
14: HRESULT hr = CreateInterface(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMH));
15: IfFailReturnHr(L"Create Instance for ICLRMetaHost failed.");
16:
17: CComPtr
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
29: hr = pRuntime->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pHost));
30: IfFailReturnHr(L"GetRuntime failed.");
31:
32: CComPtr
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平台开发程序,而不用过于担心兼容性方面的风险。更多信息请浏览http://www.cnblogs.com/anytao/archive/2009/05/22/must_net_31.html 打下