寄宿(hosting)使任何应用程序都能利用clr的功能。特别要指出的是,它使现有应用程序至少能部分使用托管代码编写。另外,寄宿还为应用程序提供了通过编程来进行自定义和扩展的能力。
允许可扩展性意味着第三方代码可在你的进程中运行。在windows中将第三方dll加载到进程中意味着冒险。dll中的代码很容易破坏应用程序的数据结构和代码。dll还可能企图利用应用程序的安全上下文来访问它本来无权访问的资源。clr的appDomain功能解决了所有这些问题。AppDomain允许第三方、不受信任的代码在现有的进程中运行,而CLR保证数据结构、代码和安全上下文不被滥用或破坏。
程序员经常讲寄宿和AppDomain与程序集的加载和反射一起使用。这4种技术一起使用,使clr称为一个功能及其丰富和强大的平台。本章重点在于寄宿和AppDomain。
CLR寄宿
.net在windows平台的顶部运行。这意味着.net必须用windows能理解的技术来构建。首先,所有托管模块和程序集文件都必须使用windows PE文件格式,而且要么是windows EXE文件,要么是Dll文件。
开发clr时,Microsoft实际是把它实现成包含在一个dll中的com服务区。也就是说,Microsoft为clr定义了一个标准的com接口,并为该接口和com服务器分配了guid。安装.net时,代表clr的com服务器和其他com服务器一样在windows注册表中注册。
任何windows应用程序都能寄宿clr。但不要通过调用CoCreateInstance来创建clr com服务器的实例。相反,你的非托管宿主应该调用MetaHost.h文件中声明的CLRCreateInstance函数。CLRCreateInstance函数在MSCorEE.dll文件中实现,该文件一般在system32目录中。这个dll被人们亲切地称为垫片(shim),它的工作是决定创建哪个版本的clr:垫片dll本身不包含clr com服务器。
一台机器可以安装多个版本的clr,但只有一个版本的MSCorEE.dll文件。机器上安装的MSCorEE.dll是与机器上安装的最新版本的clr一起发布的那个版本。
CLRCreateInstance函数可返回一个ICLRMetaHost接口。宿主应用程序可调用这个接口的GetRuntime函数,指定数组要创建的clr的版本。然后,垫片将所需版本的clr加载到宿主的进程中。
默认情况下,当一个托管的可执行文件启动时,垫片会检查可执行文件,提取当初生成和测试应用程序时使用的lcr的版本信息。但应用程序可以在它的xml配置文件中设置requiredRuntime和supportedRuntime来覆盖默认行为。
GetRuntime函数返回指定非托管ICLRRuntimeInfo接口的指针。有个这个指针后,就可以利用GetInterface方法获取ICLRRuntimeHost接口。宿主应用程序可调用该接口定义的方法做如下事情
1 设置宿主管理器
2 获取clr管理器
3 初始化并启动clr
4 加载程序集并执行其中的代码
5 停止clr,阻止任何更多的托管代码在windows进程中运行
注意:一个clr加载到winows进程之后,变永远不能卸载;clr从进程卸载的唯一途径就是终止进程,这会造成windows清理进程使用的所有资源。
AppDomain
CLR COM服务器服务器初始化时会创建一个AppDomain。AppDomain是一组程序集的逻辑容器
CLR初始化时创建的第一个AppDomain称为默认AppDomain,这个默认的AppDomain还有在windows进程终止时才会被注销。
除了默认AppDomain,正在使用非托管com接口方法或托管类型方法的宿主还可要求clr创建额外的AppDomain。AppDomain是为了提供隔离而设计的。下面总结了AppDomain的具体功能。
1 一个AppDomain的代码不能直接访问另一个AppDomain的代码创建的对象
一个AppDomain中的代码创建了一个对象后,该对象便被该AppDomain拥有。换言之,它的生存期不能超过创建它的代码所在的AppDomain。一个AppDomain中的代码要访问另一个AppDomain中的对象,只能使用按引用封送或者按值封送的语义。这就强制建立了清晰的分隔和边界,因为一个AppDomain中的代码不能直接引用另一个AppDomain中的代码创建的对象。这种隔离使得AppDomain能很容易地从进程中卸载,不会影响其他AppDomain正在运行的代码。
2 AppDomain可以卸载
CLR不支持从AppDomain中卸载特定的程序集。但可以告诉clr卸载一个AppDomain,从而卸载该AppDomain当前包含的所有程序集。
3 AppDomain可以单独保护
AppDomain创建后会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。正式由于存在这些权限,所以当宿主加载一些代码后,可以保证这些代码不会破坏(或读取)宿主本身使用的一些重要数据结构。
4 AppDomain可以单独配置
AppDomain创建后会管理一组配置设置,这些设置主要影响clr在AppDomain中加载程序集的方式。涉及搜索路劲、版本绑定重定向、劵影赋值以及加载器优化。
提示:windows的一个重要特色就是让每个应用程序都在自己的进程地址空间中运行。这就保证了一个应用程序的代码不能访问另一个应用程序使用的代码或数据。进程隔离可防范安全漏洞、数据破坏和其他不可预测的行为,确保了windows系统以及在它上面运行的应用程序的健壮性。遗憾的是,在windows中创建进程的开销很大。win32 createProcess函数的速度很慢,而且windows需要大量内存来虚拟化进程的地址空间。但是,如果应用程序完全由托管代码构成,同时这些代码没有调用非托管代码,那么在一个windows进程中运行多个托管应用程序是没有问题的。AppDomain提供了保护、配置和终止其中每一个应用程序所需的隔离。
图22-1演示了一个windows进程,其中运行着一个CLR COM服务器。该CLR当前管理着两个AppDomain(虽然在一个windows进程中可以运行的AppDomain数量没有硬性限制)。每个AppDomain都有自己的loader堆,每个loader堆都记录了自AppDomain创建以来访问过哪些类型。Loader堆中的每个类型对象都有一个方法表,方法表中的每个记录项都指向jit编译的本机代码(前提是方法至少执行过一次)。
除此之外,每个AppDomain都加载了一些程序集。AppDomain #1(默认AppDomain)有三个程序集:myApp.exe,TypeLib.dll和System.dll。AppDomain#2有两个程序集Wintellect.dll和system.dll。
两个AppDomain都加载了system.dll程序集。如果这两个AppDomain都使用来自system.dll的一个类型,那么两个AppDomain的loader堆会为相同的类型分别分配一个类型对象:类型对象的内存不会由两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型定义的方法时,方法il代码会进行jit编译,生成的本机代码单独与每个AppDomain关联,而不是由调用它的所有AppDomain共享。
不共享类型对象的内存或本机代码显得有些浪费。但AppDomain的设计宗旨就是提供隔离:clr要求在卸载某个AppDomain并释放其所有资源时不会影响到其他任何AppDomain。复制clr的数据结构才能保证这一点。另外,还保证多个AppDomain使用的类型在每个AppDomain中都有一组静态字段。
有的程序集本来就要有多个AppDomain使用。最典型的例子就是MSCorLib.dll。该程序集包含了system.object,system.int32以及其他所有.net密不可分的类型。clr初始化时,该程序集会自动加载,而且所有AppDomain都共享该程序集中的类型。为了减少资源消耗,MSCorLib程序集以一种AppDomain中立的方式加载。也就是说,针对以AppDomain中立方式加载的程序集,clr会为他们维护一个特殊的loader堆。该loader对中的所有类型对象,以及为这些类型定义的方法jit编译生成的所有本机代码,都会由进程中所有AppDomain共享。遗憾的是,共享这些资源所获得的收益并不是没有代价,这个代价就是,以AppDomain中立方式加载的所有程序集永远不能卸载。要回收他们占用的资源,唯一的办法就是终止windows进程,让windows去回收资源。
跨越AppDomain边界访问对象
一个线程能执行一个AppDomain中的代码,再执行另一个AppDomain的代码。Thread.GetDomain()方法向CLR询问它正在执行哪个AppDomain。AppDomain的FriendlyName属性获取AppDomain的友好名称(默认AppDomain使用可执行文件的名称作为友好名称)
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication7 { class Program { static void Main(string[] args) { Marshalling(); } private static void Marshalling() { //获取AppDomain引用(“调用线程”当前正在该AppDomain中执行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //每个AppDomain都分配了友好字符串名称(以便调试) //获取这个AppDomain的友好名称并显示它 String CallingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("默认AppDomain友好的名称={0}",adCallingThreadDomain); //获取并显示我们的AppDomain中包含了“Main”方法的程序集 String exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("包含“Main”方法的程序集={0}", exeAssembly); //定义局部变量来引用一个AppDomain AppDomain ad2 = null; //************************************************************************************************************ //************************************************************ DEMO 1:使用“按引用封送”进行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo1 按引用封送", Environment.NewLine); //新建一个AppDomain(从当前AppDomain继承安全性和配置) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //将我们的程序集加载到新AppDomain,构造一个对象,把它封送回我们的AppDomain(实际得到对一个代理的引用) mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType());//CLR在类型上撒谎了 //证明得到的是对一个代理对象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); //看起来像是在MarshalByRefType上调用了一个方法,实则不然。 //我们是在代理类型上调用了一个方法,代理是线程切换到拥有对象的那个 //AppDomain,并在真实的对象上调用这个方法 mbrt.SomeMethod(); //卸载新的AppDomain AppDomain.Unload(ad2); //此时,mbrt引用了一个有效的代理对象;代理对象引用一个无效的AppDomain try { mbrt.SomeMethod(); Console.WriteLine("调用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("调用失败,AppDomain被卸载了"); } //************************************************************************************************************ //************************************************************ DEMO 2:使用“按值封送”进行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo2 按值封送", Environment.NewLine); ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); //对象的方法返回所返回对象的副本 //对象按值(而非按引用)封送 MarshalByValType mbvt= mbrt.MethodWithReturn(); //证明得到的是对一个代理对象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); //看起来在MarshalByValType上调用一个方法,实际也是如此 Console.WriteLine("Return object created " + mbvt.ToString()); //卸载新的AppDomain AppDomain.Unload(ad2); //此时,mbrt引用了一个有效的x代理对象;代理对象引用一个无效的AppDomain try { //卸载AppDomain之后调用mbvt方法不会抛出异常 Console.WriteLine("Return object created " + mbvt.ToString()); Console.WriteLine("调用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("调用失败,AppDomain被卸载了"); } //************************************************************************************************************ //************************************************************ DEMO 3:使用不可封送的类型进行跨AppDomain通信 *** //************************************************************************************************************ ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); try { NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName);//抛出异常:未标记为可序列化 } catch (SerializationException) { Console.WriteLine("抛出异常:未标记为可序列化"); } Console.ReadKey(); } } //该类型的实例可跨越AppDomain的边界“按引用封送” public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} ctor running in {1}", GetType(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWithReturn() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } public NonMarshalableType MethodArgAndReturn(string callingDomainName) { //注意:callingDomainName是可序列化的 Console.WriteLine("Calling from '{0}' to '{1}'.", callingDomainName, Thread.GetDomain().FriendlyName); NonMarshalableType t=new NonMarshalableType(); return t; } } //该类的实例可跨越AppDomain的边界“按值封送” [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creationTime = DateTime.Now;//注意:DateTime是可序列化的 public MarshalByValType() { Console.WriteLine("{0} ctor running in {1}, Created no {2:D}", GetType(), Thread.GetDomain().FriendlyName, m_creationTime); } public override string ToString() { return m_creationTime.ToLongDateString(); } } //该类的实例不能跨AppDomain边界进行封送 //[Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); } } }