Shared Source CLI Essentials

>>>  文章華國詩禮傳家—精彩書評選  >>> 簡體     傳統

This concise guide offers a road map for anyone wishing to navigate, understand, or alter the Microsoft Shared Source CLI ("Rotor") code. Written by members of the core team that designed the .NET Framework, this book is for anyone who wants a deeper understanding of what goes on under the hood of the .NET runtime and the ECMA CLI. Microsoft's Shared Source CLI (code-named "Rotor") is the publicly available implementation of the ECMA Common Language Infrastructure (CLI) and the ECMA C# language specification. Loaded with three million lines of source code, it presents a wealth of programming language technology that targets developers interested in the internal workings of the Microsoft .NET Framework, academics working with advanced compiler technology, and people developing their own CLI implementations. The CLI, at its heart, is an approach to building software that enables code from many independent sources to co-exist and interoperate safely. The book is a companion guide to Rotor's code. It provides a road map for anyone wishing to navigate, understand, or alter the Shared Source CLI code. This book illustrates the design principles used in the CLI standard and discusses the complexities involved when building virtual machines. Included with the book is a CD-ROM that contains all the source code and files. After introducing the CLI, its core concepts, and the Shared Source CLI implementation, Shared Source CLI Essentials covers these topics: the CLI type system; component packaging and assemblies; type loading and JIT Compilation; managed code and the execution engine; garbage collection and memory management; and the Platform Adaptation Layer (PAL) - a portability layer for Win32, Mac OS X, and FreeBSD.

  • CLI组件模型介绍

21世纪的程序员有很多烦恼。

首先,优秀的软件比以前复杂多了。仅仅提供一个基于终端的简单命令提示符或一个字符用户界面已经不能被用户接收了;现在的用户需要包括各种优秀虚拟功能的丰富的图形化用户界面。软件相关的数据,已经很少有结构适合存储在本地系统的普通文件中的了;现在的情况是,计算机用户已经十分依赖软件所提供的查询和报表的功能,而为了满足用户的这些需求,常常必须使用关系数据库。而不断变化的业务情况要求对长期存储的数据进行的经常的修改也要求必须使用关系数据库。从前单机环境就能够满足应用程序的部署需要,在这种环境中是通过文件或剪贴板来实现数据共享的;而现在这个星球上的大部分计算机都联入了网络,因此在这些计算机上部署的软件不但必须要具有网络功能,还必须能够适应不断变化的网络环境。简而言之,软件开发早已不再是技术高手单枪匹马就能够完成任务的职业了,它已经变成了一种基于相当复杂的底层框架的集体行为了。

现在的程序员已经不能享受使用贴近处理器的底层工具(例如汇编或C编译器)来从头完成一个完整的软件工程的奢侈了。也很少有人有时间或耐心来编写中间层的框架了,即使是象HTTP协议的实现或者完成一个XML解析器这样简单的工作都很少有人来作,而具有调节底层框架来使系统达到所需的性能和质量的技能的人就更少了。现在软件开发最主要的重心放在了可重用代码和可重用组件上。操作系统加上少量的开发库这种方式已经不能满足软件开发的需要。因此,不管你喜不喜欢,今天的程序员都必须依赖多个不同来源的代码来支持他的应用程序,而且这些代码要能够正确可靠的协同工作。

为了适应当前软件开发模式的变化趋势,出现了一种新的软件开发方法学――基于组件的软件开发,它采用组合多个独立的代码模块的方式来创建应用程序。通过组合多个来源的组件,可以快速和高效的创建应用程序。然而,这门技术对编程工具和软件开发过程提出了新的需求。例如,因为依赖于其它由不被信任或不知名的开发者开发的组件,所以程序执行时的严格控制和在运行时对代码的验证成为软件正常运行最基本的要求。在我们这个遍布网路连接的时代,基于组件的复杂软件常常不需要客户端的操作就能动态更新,而有时这些更新可能是恶意的。如果询问那些计算机病毒的受害者保护她的机器和数据的干净是否必要,或者和一个不熟练的计算机使用者谈论那些他们所经历的,由于安装和卸载应用程序而导致系统莫名其妙的不稳定的情况,您就会发现基于组件的软件造成的问题几乎和它带来的好处一样之多。

多年来,对基于组件的软件开发的商业宣传和所预期的高效,被如何安全的将不同来源的组件组合起来工作的复杂性所抵消了。然而,最近10年中,我们看到了寄宿托管组件的虚拟执行环境在商业上的成功。托管组件是可以独立开发和部署的简单软件部件,不过它们可以在应用程序中安全的共存。托管组件的”托管”,是指它们需要一个虚拟执行环境来提供运行时和执行服务。为了满足组件的需要,这些环境着重致力于提供一个重点在于保证组件之间的安全合作和协作的结构良好的模型,而不只是简单的暴露它下层的处理器和操作系统的物理资源。

如图1-1所抽象描述的那样,虚拟执行环境和托管组件为三个不同的软件群体提供了很多好处:程序员,开发编程工具和编程库的人,以及那些管理在虚拟执行环境中运行的软件的管理员。对于那些使用托管组件开发复杂应用软件的程序员来说,开发工具和编程工具库的出现减少了花费在集成各组件和管理组件间通讯的任务上的时间,提高了生产率。对于工具软件开发者来说(例如编译器开发者),支持框架和一个清晰,说明详细的虚拟机的出现可以减少花费在框架和解决互操作性上的时间,而留出更多的时间来开发工具软件。(high-definition, carefully specified virtual machine??)最后,管理员和计算机用户可以从使用单一的运行时框架和打包模型中获益,并因此更好的控制计算机,所有这些都和特定的处理器和操作系统无关。

 

图1-1     当寄宿在一个虚拟执行环境中时,各组件可以安全的进行协作。

 

CLI虚拟执行环境

       ECMA通用语言框架是一个虚拟执行环境的标准规范。它描述了一个数据驱动的架构,在其中,语言无关的数据块以自装配件,类型安全的软件系统的形式被激活。驱动这个过程的数据,叫做元数据,开发工具用它来描述软件的行为,以及软件在内存中运行的特性。CLI执行引擎利用元数据来达到将各个来源的组件安全装载到一起的目的。各CLI组件在严格控制和监督下共存,然而它们也能够互相访问,可以直接访问哪些需要共享的资源。CLI是一个良好的平衡了可控性和灵活性的模型。

       ECMA,欧洲计算机厂商联合会,是一个已有多年历史的标准化团体。除了发布它自己的标准之外,ECMA也和ISO(国际标准化组织)有着密切的关系,基于这种关系,CLI标准已经被认可为符合ISO/IEC 23271:2003标准,之后根据一篇科技报告,而成为ISO:IEC 23272:2003标准,C#标准也被认可为符合ISO/IEC 23271:2003标准,并最终成为了ISO/IEC 23270:2003标准。

      

CLI标准可以在本书前言所提到的web站点中找到,它也包含在本书的随书光盘中。它由5大部分组成,外加上它的开发库的相关文档。在CLI被标准化的时候,一种名为C#的编程语言也由于共同的努力而被标准化了。C#利用了CLI的大部分特性,并且它是一种易学的面向对象语言,因此我们选择它来完成本书中的绝大部分例子小程序。在形式上,C#和CLI标准是各自独立的(虽然C#标准确实引用了CLI标准),但实际上,这二者是纠缠在一起的,并且许多人认为C#是开发CLI组件的标准语言。

       CLI中的虚拟执行是在它的执行引擎的控制下发生的,执行引擎通过在运行时解释描述组件的元数据来寄宿组件,对于那些不是基于组件的代码也是一样。采用这种方式运行的代码常被称作托管代码,它们是使用可生成与CLI兼容的可执行程序的工具和编程语言来创建的。一个被详细说明的事件链被用来从称为装配件的包装工厂中装载元数据,并将元数据转化为适合在一个机器的处理器和操作系统上运行的可执行代码。图1-2演示了这个事件链的一个简化版本,它将成为本书其它部分的基础。CLI标准的第一部分也详细描述了这个事件链(标准第一部分中的第8节描述了通用类型系统,第11节描述了虚拟执行系统,这些都是非常好的背景资料)。

 

       图1-2   CLI装载序列中的每一步都由前一阶段计算获得的元数据注释驱动,

 

       在某些方面,CLI执行引擎很象一个操作系统,因为它也是一段为在它的控制下执行的代码提供服务(例如装载,隔离和调度)和管理资源(例如内存和IO)的特权代码。还有,在CLI和操作系统中,服务都是即可以明确的被程序所请求,也可以作为执行模型环境的一部分而发挥作用。(环境服务是指在执行环境中持续运行的服务。它们非常重要,因为它们定义了一个系统的运行时计算模型的大部分内容)(Ambient services????)

       在其它方面,CLI很象传统的编译器,链接器和装载器的工具链,因为它也处理内存布局,编译和符号解析的工作。CLI标准不但极力详细描述了托管软件如何工作,还详细描述了非托管软件如何才能和托管软件安全的共存,以便能够无缝的共享计算资源和职责。CLI中系统和工具框架的结合使它成为创建基于组件的软件的唯一强有力的新技术。

 

CLI标准的基础概念

       在CLI标准和执行模型的背后是一系列的核心思想。这些核心思想是以能够帮助开发者们组织和整理他们的代码的抽象或具体的技术的形式包含在CLI设计理念中的。让我们通过一系列的设计原则来思考这些技术。

  • 使用统一的类型系统来暴露所有的可编程项。
  • 将类型打包成完全自描述的可移植单元。
  • 在运行时采用能够将各类型相互独立的方式装载类型,但是要能共享资源。
  • 使用考虑了版本、特定文化的差异(例如日历或字符编码),以及管理策略的灵活的绑定机制解决运行时类型间的依赖问题。
  • 采用能够校验类型安全的方式来描述类型的行为,但不要要求所有的程序都是类型安全的。
  • 采用能够延迟到最后时刻的方式来处理针对特定处理器的任务,例如内存分配和编译。但也要考虑较早执行这些任务的工具。
  • 在一个能够提供运行时策略的责任和执行的特权执行引擎的控制下执行代码。
  • 将运行时服务设计成由可扩展的元数据格式来驱动,这样它们会容易适应新的情况和将来的变化。

在这里我们会谈到一些最重要的概念,然后在整本书的过程中再次详细的说明这些概念。

 

类型

       CLI将世界分为各种类型,程序员们也使用类型来组织他们编写的代码中的结构和行为。组件模型对类型的描述常常是非常简单的:一个类型描述了包含数据的字段和属性,还包含描述它的行为的方法和事件(所有这些都会在第3章中详细讨论)。状态和行为既可以在实例级别中存在(在这种形式中各个组件共享结构,但各个组件不相同),也可以在类型级别中存在(在这种形式中一个独立边界内的所有实例共享一个单一的数据拷贝或方法调度的信息)。最后,组件模型支持标准的面向对象结构,例如继承,基于接口的多态,以及构造器。

       对于执行引擎,程序员和其它类型来说,类型的结构采用元数据来表示,是很有用的。元数据之所以非常重要,是因为它使得来自于不同人员,地方和平台的类型能够和平的共存,并保持独立。缺省情况下,CLI只在需要用到某个类型时才会加载它;链接只有在需要的时候才会被求值,确定地址和编译。一个类型中所有对其它类型的引用都是符号,这就意味着它们是以名称的形式存在,在运行时再确定地址,而不是预先计算好地址和偏移量。通过使用符号引用,可以建立成熟的版本机制,各类型将来的独立版本可以在执行引擎的绑定逻辑中实现。

       使用经典面向对象的单继承语法,一个类型可以从另一个类型继承结构和行为。在子类的定义中会包含它的基类的所有方法和字段,一个子类的实例可以代替它的基类的实例。虽然类型可能只能有一个基类,但它们可以附加实现任意数目的接口。所有的类型都直接,或通过它们的父类的继承关系,间接的继承自基类型System.Object。

       CLI组件模型向程序员暴露了两个更高级的结构:属性和事件,从而扩大了字段和方法的概念。属性允许类型暴露数据,数据的值可通过任意的代码来获取和设置,而不是通过直接的内存访问。从管道的角度来看,属性绝对是语法上的甜头,因为它们在内部表现为方法的形式,但是从语义学的角度来看,属性是一个类型的元数据最好的元素,能够导致更一致的API和更好的开发工具。

       类型使用事件在执行期间通知外部的观察者它们感兴趣的事件(例如,通知数据已经可用,或者内部状态的改变)。为了使外部观察者能够注册它们感兴趣的事件,CLI委托将执行一个回调所必需的信息包装起来。当注册一个事件回调时,程序员可以创建两种委托之一:一种是一个封装了指向类型的一个静态方法的指针的静态委托;另一种是一个将对象的引用和一个方法(对象会在这个方法上被回调)联系起来的实例委托。委托一般以参数的形式传递给事件注册方法;当类型需要触发一个事件时,它只是对那些在类型上注册的委托执行一下回调。

       类型使用字段来存储数据,使用方法来表示行为,从最低限度来说,它是一种采用上述方式来组织编程模块的分层方法。而在这之上,简洁性,完整性,模型,属性,事件,以及其它构造方式提供了附加的结构,可以使用这些结构来创建共享程序库和不同于CLI的运行时服务。

       COMCLI

       长期以来追求重用的软件设计者认为标准化的组件打包和运行时互操作性是最基础的。就像早期使用的穿孔打卡机就是计算函数的可重用的库所显示的那样。微软公司开发组件对象模型(COM)的原因是为了实现统一打包和细粒度的互操作性这两个目的。结果,使用“基于接口”的方法来进行二进制组件打包已经成功地被无数的软件开发者用来部署他们的API和将分散的代码模块化。与CLI不同,COM是一个几乎完全基于共享规则的组件模型,而不是一个共享的执行引擎。各COM组件共享底层的运行时基础结构,并在每一个组件的基础上进行功能协作。这个方法可以是非常有用的,尤其适合于那些计算机资源非常有限的环境,在这些环境中程序员必须压榨每一个字节的性能。或者是环境中存在数量巨大的代码需要暴露组件界面。

       只有在使用COM的共享规则后,组件之间的细粒度二进制互操作性才在Windows操作系统上的软件运行中普遍起来。它广泛而成功的作为一种方法来使用于应用程序为了提供可编程性而暴露内部,也用于发布API的标准方法。Windows的一些系统功能也通过COM接口来暴露,还有存在许多第三方厂商的控件是以可重用部件的方式来出售。

       然而,COM方法现在有明显的衰落趋势。在这个模型中,开发者要负责运行时操作的每一个细节,必须非常仔细的遵守复杂的互操作协议,以便组件能够正确的工作。因为使协议执行正确难度很大,所以代码不但繁杂,而且容易造成BUG。

       大部分的COM复杂性都可以通过为组件开发者提供共享底层服务来消除,就像操作系统为所有使用计算机资源的程序提供共享底层服务一样(例如,内存垃圾回收,就是一种可以从根本上减少组件之间所必须进行的协作操作的服务)。1997年,人们提出了一种为COM服务的运行时,它能为COM编程者提供一个类模型,以及通用运行时服务,既能提高生产力(程序员不再需要重复的编写同样的支持机制),也能带来更好的安全性,高效率和稳定性。这个运行时最初的名称是组件对象运行时(COR),在共享源代码CLI的一些函数名称中还能找到这个名字。

       微软公司最初开发COR的目的只是希望能为COM提供一个运行时,但是微软公司没有局限与此,而是决定完成一个通用的虚拟执行环境。这个努力最终成为了CLI标准。

共享类型系统和中间语言

       CLI中的类型在最低限度上是由字段和方法构成的,但是这些字段和方法自身又是如何定义的呢?CLI标准定义了一个与处理器无关的中间语言,用于描述程序,也定义了一个通用类型系统来为这种中间语言提供基本数据类型。这两个事物一起组成了一个抽象计算模型。标准使用一些规则来修饰这个抽象模型,这些规则描述了抽象模型如何才能转化成机器指令流和内存引用;这些转化过程的设计十分高效,能够识别和准确的描述许多不同编程语言的语义。中间语言,中间语言类型,以及转换的规则,组成了一个具有普遍意义的,用于描述程序的语言无关的方法。

       CLI规范中定义的中间语言叫做通用中间语言(CIL)。它包含一个与任何现存的计算机硬件结构均无关的丰富的操作码集合,用于驱动一个易于理解的抽象堆栈机。同样的,通用类型系统(CTS)定义了一个包含标准的跨语言互操作性的类型的基本集合。为了充分实现这种语言无关的世界的好处,高级编译器需要理解CIL指令集和它所匹配的数据类型的集合。如果没有这个协议,那么不同的语言就必须选择不同的映射方式;例如,C#中的int类型的长度有多大?它和Visual Basic中的Integer类型有什么关系?它和C++中的long类型完全相同吗?通过将指令集和这些类型进行匹配,这些选择将变得相当简单,当然,关于具体应该使用哪个指令和类型的选择是由编译器决定的,但是,一个良好的规范的出现意味着使得这些选择变得相当地直接和简单。通过使用这种方法,结果代码可以和其它语言编写的代码和框架进行互操作,从而导致了更加高效的重用。第3章详细描述了CLI类型系统,而第5章描述了CIL,以及它是如何转换成本地指令的。

 

基于类型的可移植打包单元:装配件

       CLI利用它的类型系统和抽象计算模型,实现了这样的理想:人们可以利用由不同人员在不同时间编写的软件组件,通过校验,装载这些组件,来使用它们一起创建应用程序。在CLI中,单独的组件可以打包到称为装配件的单元中,装配件可以在执行引擎中按需动态装载,既可以从本地磁盘中装载,也可以从网络上装载,甚至还可以在程序的控制下动态的创建。

       装配件为CLI定义了组件模型的语义。类型不能存在于装配件的外部。反过来说,装配件是将类型装载到CLI中的唯一的机制。装配件又是由一个或多个模块(模块是驻留信息的打包子单元),以及一大块名为装配件清单的描述装配件的元数据组成的。虽然装配件也能由多个模块组成,不过一般一个装配件都只包含一个模块。

       为了保证装配件不会在被编译和被装载的时候被篡改,每一个装配件通过一个秘钥对和整个装配件的一个哈希表来进行签名,这个签名可以被放在装配件清单中。这个签名被执行引擎所信任,并且可以保证装配件不会被篡改,以及有危险的装配件不会被装载。如果在运行时从装配件生成的哈希表和装配件清单中包含的哈希表不匹配,运行时会拒绝装载装配件,并且在潜在的危险代码有机会做任何事情之前抛出一个异常。

在许多方面,装配件对于CLI的意义,就像共享库或DLL对于操作系统的意义一样:它们都是绑定和识别属于同一个部分的代码的方法。感谢CLI中建立的可靠的对元数据和符号的绑定方法,这使得每一个组件都可以在独立于它的邻居的情况下被装载,翻译以及执行,即使它们之间相互依赖,也不会互相干扰。这是至关紧要的,因为平台,应用程序,库,以及硬件都会随着时间的变化而改变。基于组件建立的解决方案应该在这些组件变化时继续正常工作。我们将在第3章和第4章讨论装配件。

 

组件隔离:应用程序域和远程调用

       以使组件能够一起工作,并保护组件不受其它组件中的恶意代码或bug危害的方式装载各组件的能力,与在组件内部将代码组织在一起的能力一样重要。操作系统常常通过建立保护地址空间,以及提供连接保护地址空间和其它地址空间的通讯机制的方法来获得独立性;地址空间提供保护边界,而通讯机制为协作提供通道。在CLI中有相似的隔离执行代码的概念,它由应用程序域和对远程调用的支持组成。

       程序集总是在一个应用程序域的上下文中被装载的,因此类型就被它们的应用程序域限制了范围,例如,程序集中定义的静态变量在应用程序域中分配空间和存储。如果同一个程序集在三个不同的域中被加载,会为这个程序集中类型的数据分配三个不同的拷贝。在本质上,应用程序域是”轻量级的地址空间”,对于在各个应用程序域之间传递数据,CLI执行和操作系统在不同的地址空间之间传递数据所执行的相同的限制。希望跨越域边界进行通讯的类型,必须使用特殊的通讯通道,并按照特定的规则来进行操作。

       被称为远程调用的技术,可以用来在不同的物理计算机(计算机上可能运行不同的操作系统,具有不同的处理器)上运行的应用程序域之间进行通讯。就像经常一样,远程调用机制常常用于分隔位于同一个机器中同一个进程的域中的各组件。希望参与到远程调用中的组件要么必须是可序列化的,这样它们就能在域之间传递,要么必须继承自System.MarshalByRefObject类型,这样它们可以使用负担传递工作的代理对象进行通讯。在第4章中会讲述应用程序域,远程调用,以及装载的详细内容。

 

为灵活的版本装载服务的命名规则

       因为所有的类型和类型的代码都存在于程序集中,因此必须要有一组定义明确的描述当执行引擎需要程序集中的类型时如何查找和使用程序集的规则。程序集的名称由一个元素的标准集合组成,包括程序集的基础名称,一个版本号,一个地区文化(为全球化服务),以及一个代表发布这个程序集的发布者的公钥的哈希表。组合名称保证了由各程序集创建的软件会优雅的适应版本的变化。编译时,每个程序集都会包含在编译时它所依赖的其它程序集的组合名称的引用,并记住每个这些程序集的版本信息。这样,当装载时,程序集会非常明确的要求它所依赖的程序集的某个特定(或语义一致)的版本。用来满足这些需求的绑定策略可以通过对配置的设置来改变,但是绑定策略是不可能被忽略的。

       通常可以在下列两个地方之一找到程序集:在一个被成为全局程序集缓存(GAC),作用于整个机器的缓存,或者一个基于URL的查找路径。GAC是每个机器上的程序集的有效的数据库,每一个程序集都由它的四部分名称来唯一的识别。GAC可以是一个文件系统的目录,但也可以不是,CLI的运行必须能够在GAC中存放同一个程序集的不同版本,并且能够跟踪这些不同版本。查找路径基本上是一个URL(通常是文件系统的目录)的集合,当需要装载一个程序集时,会搜索这些路径。第4章会详细讲述装载的过程,以及装载是如何实现的。

 

JIT编译和类型安全

       CLI描述的执行模型意味着编译高级类型的工作应该和将这些类型描述转换为基于特定处理器的代码和内存结构的工作分离开来。这种分离为计算模型带来了很多重要的优势,例如能够在出现了新的操作系统和处理器后,很容易的调整代码来适应,以及能够独立的定义来自各个不同来源的组件的版本。这种分离也带来了新的挑战。例如,因为所有的类型都由CIL和CTS来描述,所以,所有的类型都必须在它们能够被使用前翻译成机器码和内存结构;本质上,整个应用程序总是必须在能够运行前被重新编译,这可能会是一个非常昂贵的方法。

       为了分次消耗将CIL转化成机器指令的代价(即包括装载所花费的时间,也包括所需要的内存),基于CLI的应用程序的类型的装载方式很独特,直到软件的运行需要它们时才会被装载,一个类型一旦被装载,它的各个方法会直到软件的执行需要它们时才被翻译。这种延期的装载和代码生成被称作即时(JIT)编译。CLI并不是一定要求最后时刻的JIT编译,但是延迟装载和编译总是会发生在一个应用程序的生命周期的某些点上,来将CIL转化为机器码。可以想象的一种情况就是,软件的安装程序就可以执行编译。第5章将讲述为了符合CLI规范JIT编译需要实现的方法。

       之所以要在CLI执行模型中要建立JIT编译的最重要的原因并不明显。在执行引擎自己的装载器和编译器的控制下进行从抽象组件到可运行的机器码的转换,是使得执行引擎在运行时保持控制,以及高效的运行代码的原因,即使是在c++编写的代码和一个托管语言编写的代码之间进行来回调用,执行引擎也能很好的工作。传统的编译,链接,和装载的过程,在CLI中依然存在,但是,就像我们看到的一样,每个工具链上的元素必须大量使用复杂的技术(例如缓存),因为延迟的使用导致了较高的运行时代价。这些较高的代价是值得忍受的,因为延迟也使得能够对运行的组件的行为进行全面控制。因为CLI的执行是基于对类型的逐渐加载,以及所有的类型都是使用平台无关的中间语言定义的,所以CLI执行引擎在它运行的过程中是在不断的编译和添加新的行为的。因为CIL被设计成可校验的,和类型安全的,编译成机器码的过程是在具有特权的执行引擎的控制下执行的,所以可以在允许一个新类型运行之前校验类型安全性。安全策略也能够在CIL被转换成机器码时检查和应用,这就意味着安全检查可以直接插入到代码中,在方法被执行时以系统的名义来执行。简而言之,通过使用延迟加载,校验和直到运行时才对组件进行编译这些技术,CLI可以加强可靠的托管执行。

 

托管执行

       类型装载是导致CLI的工具链在运行时十分忙碌的关键所在。看看装载进程的部分工作,CLI需要编译,装配,链接,验证可执行文件的格式和程序的元数据,校验类型安全,最终甚至是管理运行时的资源,例如内存和处理器周期,在它的控制下代表组件运行。将所有这些阶段联系在一起的任务导致CLI包括了名称绑定,内存分布,编译和打补丁,分隔,同步化,以及符号解析的基础结构。因为总是希望这些元素的执行延迟到尽可能最后的时刻,所以执行引擎可以享受对装载和执行策略,内存组织,生成代码,以及代码和底层平台、操作系统的交互方式的高度可靠的控制。

       延迟编译,链接,和装载为跨越目标平台和跨越版本变化提供了更好的可移植性。通过延迟排序和排列的决定,延迟地址和偏移量的计算,延迟处理器指令的选择,延迟调用的转化,当然,还有延迟链接到平台的自身服务上,程序集可以变得更加的向前兼容。由定义良好的元数据和策略驱动的延迟过程,是非常有活力的。

       翻译元数据的执行引擎是可信赖的系统代码,因此,通过后期装载,安全性和稳定性也得到了增强。每个程序集都包含一个与它相关的许可权限的集合,定义了允许这个程序集执行什么操作。当这个程序集中的代码企图执行一个敏感的操作时(例如企图读或写一个文件,或者企图使用网络),CLI会查看调用堆栈,检查它来判断是否当前范围内的所有代码都有合适的权限――如果堆栈上的代码没有合适的权限,操作就会被拒绝,一个异常会被抛出(异常是另一个能够在组件之间进行简单的交互的机制;CLI的设计不仅支持在执行引擎内广泛范围内的异常语义,也紧密的集成了来自底层平台的异常信号)。第6章和第7章将详细描述托管执行。

 

      使用元数据来实现数据驱动的可扩展性

       CLI组件是自描述的。一个CLI组件包含了它内部的每个成员的定义,而这些定义信息在运行时中受保证的有效性是帮助虚拟执行具有高度可适应性的一个因素。每个类型,每个方法,每个字段,每个单一方法调用上的每个单一的参数必须是被充分描述的,而这个描述信息必须存储在程序集内部。因为CLI将各种链接操作延迟到必需执行的最后时刻,那些希望通过使用元数据来操作组件和创建新组件的工具和程序获得了极大的灵活性。建立在CLI之上的代码可以使用与CLI所使用的相同类型的技巧,对于工具软件和运行时服务来说,这简直就是天降横财。

       为了获得类型的信息,CLI程序员可以使用执行引擎提供的反射服务。反射提供了在运行时检查编译时信息的能力。例如,对于一个托管组件,开发者可以获得类型的结构信息,包括它的构造器,字段,方法,属性,事件,接口,以及继承关系。可能更重要的是,开发者可以使用名为定制属性的功能将自己的元数据添加到组件的描述信息中。

      通过反射不仅可以获得编译时信息,而且可以操作运行中的实例。开发者可以使用反射来进入类型内部,获得它们的结构信息,并基于这个结构信息操作类型的内部信息。对于方法来说,是一样的;开发者可以在运行时动态的调用方法。这种元数据驱动风格的编程能力,以及如何才能实现它的问题,我们会在第3章接触到,并且在第8章会详细的进行讲述。

 

CLI的共享源码实现:Roter

       2001年夏天,雷德蒙公司的一小组开发人员宣布了一个微软公司少有的开发计划:一个免费使用的软件产品,包括可修改,可重新发布的源代码。这个叫做SharedSource CLI(SSCLI,人们也亲切的称呼它的代码名称,“Rotor”)的产品,包括一个实现了全部功能的CLI执行引擎,一个C#编译器,基础的编程库,和许多相关的开发工具。这个产品在商业的.Net框架周边静悄悄的发展,代表了微软的开发者工具策略的一个重要方面,实际上,SSCLI要实现三个目的:检验CLI规范的可移植性,帮助人们学习和理解微软的商业CLR产品,以及长期促进学院对于CLI的兴趣。最重要的是,SSCLI要符合ECMA标准,以便任何希望了解和实现这个标准的人可以将SSCLI作为一个指南。

       虽然名义上SSCLI是这本书的主题,但实际上CLI标准才是本书的核心。SSCLI帮助我们阐明CLI是怎样一件如此令人感兴趣的工作,以及为什么。这个产品本身包含了巨大数量的代码,因此,它可以为在开发工具或系统设计领域工作的研究者和试验者提供重要的帮助,对于那些教授计算机科学的老师也是一样。本书尝试通过提供远多于CLI理论的知识,来辅助分析和解释基本代码的规则,以便为这些人提供一个针对这些代码的高级指南。CLI标准会变得越来越重要,而完全理解它的最好方法,就是浏览,创建,观察和分析一个运行中的实例。

       而Rotor向我们演示了创建一个可移植,编程语言无关的CLI规范的实际版本的一种方法,当然,它不是唯一的方法。在写作本书的时候,就存在着其它的CLI规范的实现方式,包括微软公司的两种(商业.Net框架和名为“精简框架”的运行在微型设备上的版本),以及第三方厂商的两种开源的实现,一种来自Ximian公司(名为Mono),另一种来自DotGNU项目(名为Portable.NET――可移植.NET)。Rotor自身提供了大大超过标准的各种附加开发工具和功能。为了说明在这个产品中包含了那些内容,图1-3使用一张图来说明微软商业产品(.NET CLR),CLI和C#标准,以及Roter之间的区别。

       如图所示,SSCLI是CLI标准的一个超集,而微软的商业产品又是SSCLI的一个超集。

       Rotor是一个由许多人历经多年开发的巨大数量的代码的集合,因此,它是一个复杂和

文体上的变量(stylistically variable???),就软件的大小来说,它可以与那些最大的着名开源产品相比,例如Xfree86,Mozilla,以及OpenOffice。和这些产品一样,如果希望开始了解这些巨大数量的代码,可能会令人望而却步。本书将帮助您较为容易的完成这个任务,让我们从这个产品自身的简短历程开始吧。

       SSCLI是使用C++和C#,以及少数处理特定处理器细节的汇编组合编码开发的,这个产品通过一个三阶段的过程开发完成。首先,一个基于特定平台的C++编译器被用来开发一个平台适配层(PAL),这是一个用于将操作系统的各API的差别隐藏在一个单一抽象编程集合后面的库。之后,一系列创建SSCLI所必需的开发工具(包括C#编译器)在PAL库上被创建和链接。最后,使用这些工具和PAL库来开发产品的其它部分。

 

       表1-1列出了一些在浏览SSCLI源码时有用的子目录,SSCLI源码位于本书的随书光盘中(也可以从http://msdn.microsoft.com/net/sscli处下载)

表1-1。产品中重要的子目录,以及各目录的内容。

子目录                                     内容

/build                                  包括创建好的可执行文件和库文件

/clr/src                               许多包含核心内容子目录的主目录

/bcl                                           基础类库,用C#编写

/csharp                               一个C#编译器,用C#编写

/classlibnative                      C++编写的开发库

/debug                                实现托管调试

/dlls/mscorsn                             强名称加密代码

/fjit                                     SSCLI的JIT编译器

/fusion                                定位版本文件的代码

/ilasm                                 一个CIL汇编器

/ildasm                               一个CIL反汇编器

/inc                                           共享的包含文件

/md                                    元数据工具

/toolbox/caspol                    caspol安全工具的源码

/tools                                  许多工具程序的主目录

/clix                                   SSCLI托管执行的启动程序

/gac                                   gacutil缓存工具的源码

/peverify                             peverify CIL校验工具

/sos                                    SOS调试外挂库

/strongname                        序列号代码签名工具

/vm                                    CLI执行引擎

/docs                                  文档

/fx/src                                附加的托管库的主目录

/net/system/net                    网络功能库

/regex/system/text                      正则表达式库

/jscript                                一个可以编译成CIL代码的完整Jscript编译器,使用 C#编写 (一个托管的托管代码编译器!)

/managedlibraries/remoting   对bcl目录中的基础类库附加的远程调用支持

/pal                                           PAL在多个特定操作系统上的实现

/palrt                                  支持SSCLI运行的低级API ,但不针对某个特定的操作系统

/samples                                    使用CLI的例子程序

/tests                                  扩展测试和测试基础结构

/tools                                  用来创建SSCLI产品的工具

 

       这些子目录可以被分成四个概念明显不同的部分,如下所示:

  • CLI执行引擎
  • 包装和扩展了执行引擎的组件框架
  • 用于从一个操作系统移植到另一个操作系统的可移植层(PAL)
  • 工具,测试,编译器,文档,以及为和托管代码一起工作服务的工具。

 

让我们依次钻研这些部分,关注哪里能够找到它们的实现方法。

 

CLI执行引擎

执行引擎是CLI的核心。它包括组件模型,以及运行时服务,例如异常处理,自动的堆和栈的管理。在许多方面,它简直是一个大巫师(???);它就是当我们谈论“运行时”和“虚拟执行环境”时所指的代码。JIT编译,内存管理,程序集和类的装载,类型解析,元数据的解析,堆栈遍历,以及其它的基础机制都是在这里实现的。在sscli/clr/src目录和四个子目录vm,fjit,md,和fusion中可以找到包含了大部分执行引擎功能的代码。

       如图1-4所示,执行引擎由一系列可装载的动态库组成,而不是一个单一的可执行文件。Clix程序启动器(或者任何希望使用执行引擎服务的程序)装载主要的共享库,sscoree,来在进程中创建一个CLI的实例,然后为这个实例提供一个需要执行的启动程序集。因此,在执行引擎中是没有main函数的,它是以打包的形式来被其它程序寄宿的。执行引擎依赖大量的其它共享库,包括一些不封闭的库,因为它们是需要被替代代的,例如crypto代码必须装载和创建位于mscorsn中的签名汇编,以及一些会在许多不同的地方潜在的发挥作用的库,例如PAL,可以在rotor_pal目录和rotor_palrt目录中找到它。最后,那些不常需要的代码也被打包在分别装载的库中,例如mscordbc,这个库实现调试器功能的支持。

 

CLI中的开发库

       CLI的共享框架不仅包含标准的,底层的功能,例如元数据,通用中间语言,以及通用类型系统,也包括高级的,面向生产的类库。表1-2通过列举有用途的各方面简单的总结了这些库的内容

表1-2. CLI标准库所包含的高级元素。

类别                                        功能

生产库                                    文本格式化,正则表达式,集合,时间,日期,文件和网络IO,配置,诊断,全球化,独立存储,XML

执行引擎库                            分隔的域,异步调用,堆栈遍历,堆栈跟踪,垃圾回收,句柄,环境,线程,异常,基于监控的同步,安全性,校验,反射,序列化,与机器码的互操作

类型相关的库                         简单类型,值类型,委托,字符串,数组

扩展的数值库                         Decimal数值, 双精度和单精度浮点数,数学计算

编程语言支持            编译器服务,定制元数据属性,资源回收

 

       这些库提供了一种使用底层操作系统功能的接口,不过是通过一种与CLI的服务和规范相适应的方式来使用的,这样通过它们的一致性和高质量,就提高了程序员的生产力。

       这些API也为其它一些不太重要的方面服务:它们通过暴露编程服务和规则促进了组件集成,通过编程服务和规则的使用促进了组件的卫生学。将组件开发者必须完成的薄记工作降低到最小的服务,或将复杂的组件间管理协议的需要降到最小的那些服务,使组件之间的继承更加平滑和安全(并且减少了需要编写的代码)。一个组件越不依赖于其它组件,这个组件必须为其它组件作的工作就越少,这个应用程序因此也就越可能远离BUG,易读和健壮。我们意识到基于组件的软件真正的含义,必需将组件创建为依赖一个以这些原则为核心设计的环境中的托管执行。

       有人可能会将CLI库想象成C运行时库的另一个时髦版本。其实CLI库并没有试图为所有的程序员提供所有的功能;而是提供一组几乎每一个程序员都会用到的核心组件。因为sscli/clr/src/bcl目录下的基础库,是任何CLI实现所必须具有的部分,所以它们形成了可移植的应用程序实现的基础。在sscli/fx,sscli/clr/src/classlibnative,和sscli/managedlibraries目录下的附加库,要么是可选的标准库,要么就是sscli中的部分。目前,Sscli中所有的库都可以在微软的商业.Net框架中找到。

       如果浏览各个程序库,就会发现,除了sscli/docs目录下有专门说明Rotor产品和它的功能的文档之外,还有一个单独可下载的文件(也包含在本书的随书光盘中),其中包含了类库的文档。该文档来自微软的.Net框架SDK中使用的文档,不过它已经经过了修改,并转化成简单的HTML文件了。

 

平台适配器层

       PAL是一个很有趣的软件,虽然人们并不太关注它,但它十分有用。当然,任何包含大量代码的适配器或驱动层都有一个显着特点,就是可以运行在许多操作系统平台上,PAL     的首要目的是将实现从各种操作系统的细节中分离开来。Sscli中的选择十分明确:因为它是从特定的Win32代码启动的,因此PAL被设计为表达Win32的API的一个子集(可以在sscli/pal/rotor_pal.h中找到)。这个实现绝对不会完整,因为它只需要提供CLI实际会使用的那些调用。不要企图将PAL用作一个通用的win32模拟层,因为它是不完整的。

       当然,将Rotor移植到新平台的工作应该从PAL开始,因为用来创建Rotor的工具依赖于基于它们的操作系统和资源的PAL。可以查看sscli/pal/unix目录,来了解包含了什么内容。这里包含了大量必须完成的重要工作:提供通用异常处理机制,通用线程,共享的句柄管理,IO,同步,调试等等。特定的主机进程,例如web服务器或者数据库,很可能会需要它们自己相似的运行时。PAL的语义需要考虑这些问题。因为上面这些情况,以及PAL定义了如何使用操作系统资源的方法,所以对于很多人来说,理解各种各样的PAL的实现会十分重要和有用。

       除了PAL之外,名为sscli/palrt/src目录下包含了一个Win32 API的实现库,SSCLI需要这个库,但这个库的执行并不依赖于操作系统。这个库也包括了少量PAL的API。它确实是一个各种功能的大杂烩,不过有趣的是,其中包含了decimal算法,一些微软的COM组件模型的简单实现,数组处理,内存管理,以及各种其它有用的函数。

       PAL最有趣的方法一定就是执行引擎控制了。SSCLI的设计要求能够在原始进程中和机器码进行协作。这意味着需要捕捉许多操作系统调用,以便给执行引擎提供机会,来维护使用运行时系统所需的薄记信息,例如垃圾回收或者安全系统。这是PAL层一个关键的用处,SSCLI实现是以PAL所表达的抽象方式创建的,如果没有PAL,它就不能保持独立,安全和控制。例如,线程和异常处理都是在PAL中实现的,二者对于运行时的执行引擎都很关键,因为它使用异常帧来跟踪托管模块,使用线程堆栈来存储包括自身许多服务状态的扩散结构。PAL这方面的细节会在第6章中详细讨论,而PAL的自身设计将是第9章的主题。

 

工具,编译器,测试,文档和其它有用的东西

       Rotor中相当一部分代码是由用于创建,测试,和使用它的CLI实现的框架结构组成的。我们刚讨论过的PAL层,就是这样的代码。在这个产品的各个地方都可以发现各种附加的开发工具,有用的东西,以及测试程序。这些工具属于管理开发和创建产品的广泛范围。

       就管理开发来说,Rotor产品中的许多工具会让任何有过使用微软.Net框架经验的程序员感到很熟悉,因为这两个实现共享它们基础的功能集,例如链接器,汇编器,以及反汇编器。sscli/clr/src, sscli/clr/src/tools, and sscli/clr/src/toolbox目录包括了这些功能的子目录,以及只用于使用SSCLI来开发和运行托管代码的工具,例如clix.exe。程序员应该查看sscli/docs目录下的文档来了解是否某个特性是由Rotor版本的工具和它的.Net框架的副本所共享的,不是所有的特性都是共享的。

       用于自我生成Rotor的创建系统位于sscli/tools目录下。这些工具在PAL上创建,用于跟踪依赖关系,驱动创建进程,汇编库和执行文件,一旦被创建,装配库和可执行文件将被放到sscli/build目录下。Rotor中的依赖令人费解,因为它是十分巨大的项目,因此这些工具十分重要。可以查看sscli/docs/buildtools目录来了解如何使用它们,以及当开发者在修改代码时如何与它们交互。

一旦SSCLI被创建,就可以使用sscli/tests目录下的测试程序来测试它了。特别要说明的是位于sscli/tests/palsuite目录下的PAL测试程序,可以用它来校验新的PAL实现,或者将它校验一个现存的PAL中的改变,在sscli/tests/bvt目录下的开发者创建校验测试(BVT),可以用来检查在执行引擎中完成的工作。还有为其它领域(例如基础类库)服务的测试,例如:基础类库。大部分测试,包括BVTs,使用了sscli/tests/harness目录下的测试工具,文档位于sscli/docs/testing_overview.html中。

Rotor相关的文档和技术资料位于sscli/docs目录下,这个目录包含了对于浏览源码,修改代码,以及理解CLI结构和创建SSCLI时特定实现的选择很有用的信息。该目录下也包括一个PAL的详细说明,对于需要将Rotor移植到新平台上的人会非常有用。花费一些时间来浏览这个目录是很值得的。

 

本书范围

       本书关注的重点在于在SSCLI中是如何实现CLI组件模型和它底层的执行引擎的。也简洁的讲述了将这样一个机制放置在操作系统上的需要,以及通用移植的事项。而关于编译器,语言和框架,以及非面向组件的使用CLI的方法的论述就比较少了,不过这些内容可以在众多的讲述.Net框架和CLI的书中找到。书的大小和范围,以及作者希望看到本书出版的事实,决定了本书最关注的内容。

       还需要声明一点:本书中大量来自sscli源码的C++例子程序大部分都被清除了,用进程中的伪代码来代替。这样做的目的是移去那些会混淆真正代码的丑陋的宏定义,错误处理以及断言,以使代码更加可读。如果您计划添加和修改sscli的源码,你应该意识到那些必须保留的变量,以及采用与SSCLI开发者相同的编程风格和错误处理方法。附录D对这些方面做了简短的描述。

 

摘要

       CLI是第一个从头设计,可以被许多不同的编程语言共享的虚拟执行环境。平台提供者,框架创建者,以及程序员不必被迫关注所有的语言特性,而只需利用创建基于组件的计算工作的优势,例如,异常,垃圾回收,反射,代码访问安全,数据驱动的可扩展性。使用CLI,可以很容易的将已经存在的代码合并到基于组件的编程工作中,这就导致了可增长的互操作性和共享的框架。

       CLI用于打包,描述和部署组件的标准格式与操作系统和实现语言均无关。这是很重要的,因为这种格式形成了CLI数据驱动的结构的基础。数据驱动机制提高了程序员的生产力,因为它使得不同的程序,库,和工具能够无缝的进行交互,并随着时间的推移而发展。一个数据驱动的组件模型就如同今天的技术所认为的那样,是将来的保证。

       虚拟指令集和类型系统描绘了CLI的虚拟执行模型,展示了圣杯诱人的光芒:软件可以在任何地方运行,CLI设计当然预见到了一个各种不同的实现和不同版本的标准可以在许多平台上并存运行的世界。然而在这个世界中,利用CLI对互操作性的优秀支持,每一个实现都可能暴露唯一的框架,服务,功能和工具,或者增大了基础容量的语言特性。这将导致类似C语言的开发,在这种开发中,很少会有重要的应用程序是独立的在标准运行时上创建的。取而代之的是,应用程序会明智的将平台相关的各库的标准功能,和为跨平台使用特别设计的库组合起来。许多重要的CLI程序会将平台相关的组件和为跨平台使用特别设计的第三方组件组合起来形成标准的组件。

       人们开发出CLI的语言无关特性,数据驱动的结构,以及它的虚拟执行模型,以创建一个组件之间能够高效的协作,而不需牺牲安全性和封装性的舞台。元数据伸展的链创建了一个能够意识到组件的行为,在运行它们之间能够将安全错误注入它们的代码中的环境。CLI执行模型中的每一个阶段都包括了从前一阶段接受数据,转换和增强它,然后将它传递给下一个阶段的工作。本书描述了这些阶段的完整链,以及它们基于实现的执行引擎,从最初的启动序列开始,直到最后它的托管资源被释放为止。


daidaoke2001的专栏 2014-07-12 16:10:30

[新一篇] 大河之旁必有大城

[舊一篇] 過好精力充沛的每一天——讀《精力管理》有感
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表