CLR基础之一---认识CLR [《CLR via C#》读书笔记]
《CLR via C#》读书笔记
-
什么是CLR
CLR的基本概念
通用语言运行平台(Common Language Runtime,简称CLR)是微软为他们的.Net虚拟机所选用的名称。这是通用语言架构(简称CLI)的微软实现版本,它定义了一个代码运行的环境。CLR运行一种称为“通用中间语言”的字节码,这个是微软的通用中间语言实现版本。 CLR运行在微软的视窗操作系统上。查看通用语言架构可以找到该规格的实现版本列表。其中有一些版本是运行在非Windows的操作系统中。(维基百科CLR)
以上定义至少包含以下几点信息:
- CLR是一个类似于JVM的虚拟机,为微软的.Net产品提供运行环境。
- CLR上实际运行的并不是我们通常所用的编程语言(例如C#、VB等),而是一种字节码形态的“中间语言”。这意味着只要能将代码编译成这种特定的“中间语言”(MSIL),任何语言的产品都能运行在CLR上。
- CLR通常被运行在Windows系统上,但是也有一些非Windows的版本。这意味着.Net也很容易实现“跨平台”。(至于为什么大家的印象中.Net的跨平台性不如Java,更多的是微软商业战略导致的)
CLR和.Net Framework的关系
.NET框架 (.NET Framework) 是由微软开发,一个致力于敏捷软件开发(Agile software development)、快速应用开发(Rapid application development)、平台无关性和网络透明化的软件开发平台。.NET框架是以一种采用系统虚拟机运行的编程平台,以通用语言运行库(Common Language Runtime)为基础,支持多种语言(C#、VB.NET、C++、Python等)的开发。(维基百科.Net Framework)
由此可见,.Net Framework是一个支持多种开发语言的开发平台,而这种多语言支持的特性又要以CLR为基础。CLR是一个.Net产品的运行环境。公共语言运行时(Common Language Runtime)和 .Net Framework 类库(FCL)是.Net Framework的两个主要组成部分。
CLR>=CLR+CLI+FCL。
目前有哪些语言支持CLR
微软已经为多种语言开发了基于CLR的编译器,这些语言包括:C++/CLI、C#、Visual Basic、F#、Iron Python、 Iron Ruby和IL。除此之外,其他的一些公司和大学等机构也位一些语言开发了基于CLR的编译器,例如Ada、APL、Caml、COBOL、Eiffel、Forth、Fortran、Haskell、Lexicon、LISP、LOGO、Lua、Mercury、ML、Mondrian、Oberon、Pascal、Perl、PHP、Prolog、RPG、Scheme、Smaltak、Tcl/Tk。
CLR为不同的编程语言提供了统一的运行平台,在很大程度上对上层开发者屏蔽了语言之间才特性差异。对于CLR来说,不同语言的编译器(Compiler)就相当于一个这种语言的代码审查者(Checker),所做的工作就是检查源码语法是否正确,然后将源码编译成CLR所需要的中间语言(IL)。所以编程语言对于CLR是透明的,也就是说CLR只知道IL的存在,而不知道IL是由哪种语言编译而来。
CLR的这种“公共语言”的特性使得“多语言混合编程”成为可能,让APL开发人员可以使用自己熟悉的语言和语法来开发基于.Net的项目。当然,更重要的是,这种特性允许用不同的语言来开发同一个项目的不同模块,比如在一个项目中用Visual Basic开发UI、用APL开发财务相关的模块,而与数学计算有关的模块使用F#,充分利用这些语言的特性,将会得到意想不到的效果。
什么是CLS
正如上面所说,CLR集成了这些所有的语言,使得一种编程语言可以使用另一种编程语言创建的对象。语言集成是一个宏伟的目标,然而,一个不得不面对的事实是不同的编程语言之间存在着极大的差异,比如有些语言不支持操作符重载、有些语言不支持可选参数等等。为了保证用一种面向CLR的编程语言创建的对象能够安全的被其他面向CLR的语言来访问,最好的办法就是在用这种语言编程的时候只是用那些所有面向CLR的语言都支持的特性。为此,微软专门定义了一个“公共语言规范(Common Language Specification)”,即CLS。CLS定义了一个最小的功能集,任何面向CLR的编译器生成的类型想要兼容其他“符合CLS,面向CLR”的组件,都必须支持这个最小功能集。
CLR/CTS 支持的功能比 CLS 定义的子集多得多。如果不关心语言之间的互操作性,可以开发一套功能非 常丰富的类型,它们仅受你选用的那种语言的功能集的限制。具体地说,在开发类型和方法的时候,如果 希望它们对外“可见”,能够从符合 CLS 的任何一种编程语言中访问,就必须遵守由 CLS 定义的规则。注意, 假如代码只是从定义(这些代码的)程序集的内部访问,CLS 规则就不适用了。
到目前为止,除了IL汇编语言支持CLR所有的功能特性之外,其他大多数面向CLR的语言都只提供了CLR部分的功能。也就是说这些语言的功能都是CLR功能特性的一个子集,但为了实现与其他语言的互操作性,他们的功能都是CLS的一个超集(但不一定是同一个超集)。
想了解更多关于CLS的信息,请点击这里。
如果想要写出遵循CLS的代码,必须要保证一下几个部分的代码遵循CLS。
- 公共类(public class)的定义
- 公共类中公共成员的定义,以及派生类(family访问)可以访问的成员的定义。
- 公共类中公共方法的参数和返回类型,以及派生类可以访问的方法的参数和返回类型。
小提示:在您的私有类的定义中、在公共类上私有方法的定义中以及在局部变量中使用的功能不必遵循 CLS 规则。 您还可以在实现您的类的代码中使用任何想要的语言功能并让它仍是一个符合 CLS 的组件。
注意:交错数组(jagged arrays)符合 CLS。 在 .NET Framework 1.0 版中,C# 编译器错误地将其报告为不符合。
可以使用 CLSCompliantAttribute 将成程序集、模块、类型和成员标记为符合CLS或者不符合CLS。 未标记为符合 CLS 的程序集将被认为是不符合 CLS 的。 如果没有 CLS 特性应用于某个类型,则认为该类型与在其中定义该类型的程序集具有相同的 CLS 遵从性。 同样,如果没有 CLS 特性应用于某个成员,则认为该成员与定义该成员的类型具有相同的 CLS 遵从性。 如果程序元素中包含的元素未标记为符合 CLS,则不能将此程序元素标记为符合 CLS。
以下代码示例了CLSCompliantAttribute的使用:
using System; // 告诉编译器要检查代码是否符合CLS [assembly: CLSCompliant(true)] namespace CLS_CS { public class CsClass { // 警告: “CLS_CS.CsClass.Abc()”的返回类型不符合 CLS // UInt32不符合CLS。 public UInt32 Abc() { return 0; } // 警告:仅大小写不同的标识符“CLS_CS.CsClass.abc()” // abc和Abc是两个的方法名称仅仅是大小写不同。 public void abc() { } //没有警告:私有方法不会促发CLSCompliant规则 private UInt32 ABC() { return 0; } } }
在编译这段代码的时候,由于使用了[assembly: CLSCompliant(true)]特性,编译器就会检查代码是否符合CLS,同时对于不符合CLS的部分给出警告。
事实上,只要满足一下两个条件,即使程序集、模块或者类型的某些部分不符合CLS,这些程序集、模块或者类型也可是是符合CLS的:
- 如果元素标记为符合CLS,,则必须使用 CLSCompliantAttribute(同时其参数应设为 false)对不符合 CLS 的部分进行标记。
- 必须为每个不符合CLS的成员提供相对应的符合CLS的替换成员。
对于第一个条件,只要在上面的代码中相应的地方使用[CLSCompliant(false)],就可以让编译器不再产生这些警告,生成的还是一个符合CLS的程序集。
using System; // 告诉编译器要检查代码是否符合CLS [assembly: CLSCompliant(true)] namespace CLS_CS { public class CsClass { // 没有警告 // UInt32不符合CLS。 [ CLSCompliant(false)] public UInt32 Abc() { return 0; } // 没有警告 // abc和Abc是两个的方法名称仅仅是大小写不同。 [CLSCompliant(false)] public void abc() { } //没有警告:私有方法不会促发CLSCompliant规则 private UInt32 ABC() { return 0; } } }
但是,这样一来,其他语言的模块就不能很方便的访问这些成员了。在必要情况下,需要使用第二个条件,即为不符合CLS的成员提相应的符合CLS的替换成员。
为了方便开发,有些编译器已经为我们做好了类似的工作,即在编译阶段已把某些不符合CLS的功能编译成了符合CLS的特性。这里以操作符重载为例,前面已经说过,有些面向CLR的语言是不支持操作符重载的,因此操作符重载并不是CLS的一部分。那么编译器是如何处理操作符重载的呢。 以下代码简单的示例了操作符“<”和“>”的重载:
using System; // 告诉编译器要检查代码是否符合CLS [assembly: CLSCompliant(true)] namespace CLS_CS { public class Operatorcs { // 重载操作符 public static Boolean operator >(Operatorcs t1, Operatorcs t2) { return true; } // >和 <的重载必须要成对出现 public static Boolean operator <(Operatorcs t1, Operatorcs t2) { return true; } } }
将这段代码编译成程序集,然后用IL反编译工具查看生成的IL代码。这里使用的是ILDasm.exe,当然,你也可以使用一些其他的工具,例如.Net Reflector、ILSpy等。
可以看到,编译后的IL代码并没有任何操作符“>”和"<"的信息,同时,生成了两个名为“opGreaterThan”和“opLessThan”的静态方法,而且这两个方法的返回值和签名都和源码中操作符重载的方法一致,由此,可以推测编译器将操作符重载“翻译”成了对应的静态成员。
而事实正是如此,虽然操作符重载不是CLS的一部分,但是所有面向CLR、符合CLS的语言都会支持类型、成员、属性、字段这些特性。编译器利用这一点用符合CLS的成员方法代替不符合CLS的操作符重载,巧妙的避开了CLS的规则校验,这也是为什么这段代码虽然也使用了[assembly: CLSCompliant(true)] 但没有报出“不符合CLS”的警告的原因。
什么是CTS
需要记住的是CLR所有功能的实现都是基于类型的。一个类型将功能提供给一个应用程序或者另一个类型来使用。通过类型,用一种编程语言写的代码能与用另一种语言写的代码沟通。由于类型是CLR的根本,微软专门为如何定义、使用和管理类型定义了一个正式的规范-- 通用类型系统(Common Type System),即CTS。
CTS对类型的定义和行为特性给出了规范,这些特性包括但不仅限于以下几点:
- 类成员(Field、Method、Property、Event)
- 访问可见性级别(private、family、family and assembly 、assembly、family or assembly 、public)
- 类型的继承
- 接口
- 虚方法
- 对象的生命周期
同时,所有引用类型都必须继承自System.Object的规则也是在CTS中定义的。
一般来说,CLS主要提供了一下功能:
-
- 建立一个支持跨语言集成、类型安全和高性能代码执行的框架环境。
- 提供一个支持完整实现多种编程语言的面向对象的模型。
- 定义各语言必须遵守的规则,有助于确保用不同语言编写的对象能够交互作用。
- 提供包含应用程序开发中使用的基元数据类型(如Boolean、Byte、Char、Int32 和 UInt64)的库。
-
CLR是如何工作的
借用维基百科上的一副图来描述CLR的运行流程:
从源代码到应用程序执行CLR主要做了以下工作:
-
将源代码编译成托管模块
托管模块是一个标准的 32 位 Microsoft Windows 可移植执行体(PE32)文件,或者是一个标准的 64 位 Windows 可移植执行体(PE32+)文件,它们都需要 CLR 才能执行。一个托管模块主要包含一下几部分信息:
-
PE32货PE32+头
这里描述了当前托管模块的基本信息,包括运行的Windows版本、文件类型、生成时间等。对于包含本地 CPU代码的模块,这个头包含了与本地 CPU 代码有关的信息。
-
CLR头
包含使这个模块成为一个托管模块的信息(可由 CLR 和一些实用程序 进行解释)。头中包含了需要的 CLR 版本,一些 flag,托管模块入口方 法(Main 方法)的 MethodDef 元数据 token,以及模块的元数据、资 源、强名称、一些 flag 以及其他不太重要的数据项的位置/大小
-
元数据
每个托管模块都包含元数据表。主要有两种类型的表:一种类型的表 描述源代码中定义的类型和成员,另一种类型的表描述源代码引用的 类型和成员
-
IL (中间语言)代码
编译器编译源代码时生成的代码。在运行时, CLR 将 IL 编译成本地 CPU 指令
-
-
将托管模块合并成程序集
CLR 实际不和模块一起工作,而是和程序集一起工作的。可以从以下两点对程序集有一个初步的认识:
- 首先,程序集是一个或多个模块/资源文件的逻辑性分组。
- 其次,程序集是重用、安全性以及版本控制的最小单元。
-
加载CLR
在这一步,Windows 检查好 EXE 文件头,决定是创建 32 位、 64 位还是 WoW64 进程之后,会在进程的地址空间中 加载 MSCorEE.dll 的 x86,x64 或 IA64 版本。如果是 Windows 的 x86 版本,MSCorEE.dll 的 x86 版本在 C:\Windows\System32 目录中。如果是 Windows 的 x64 或 IA64 版本,MSCorEE.dll 的 x86 版本在 C:\Windows\SysWow64 目录中,64 位版本(x64 或者 IA64)则在 C:\Windows\System32 目录中(为了向后 兼容)。然后,进程的主线程调用 MSCorEE.dll 中定义的一个方法。这个方法初始化 CLR,加载 EXE 程序集, 然后调用其入口方法(Main)。随即,托管的应用程序将启动并运行。
-
执行程序集中的代码
到目前为止,源代码已经被编译成二进制的IL并且包含在程序集中,而且被CLR加载。但是,直接执行运算的CPU来说二进制的IL还是太高级了,而且不同的CPU支持的指令集也有所差异。因此,CLR在这里还需要对已经编译好的IL再次编译,针对CPU版本生成可以直接运行的CPU指令,这个过程是由JIT(Just In Time)编译器完成的,可以称作“即时编译”。
当第一次执行某个函数时,MSCorEE.dll 的JITCompiler函数会从程序集的元数据中获取该方法和方法的IL,并且分配一块内存地址,然后将IL编译成的本地代码放入这块内存,然后执行内存中的本地代码。
当再次执行这个函数的时候,由于内存中已经存在JIT编译好的本地代码,因此不需要再次进行JIT过程,可以直接执行内存中的本地代码。 可以预知的结果是,这种情况下应用程序启动后第一次调用某个模块所花费的时间要比以后调用这个模块要稍微多一些。
现在,通过本地代码生成技术,已经可以在编译阶段就根据计算机的的实际环境生成本地代码,这样以来就可以在运行时节省JIT编译的时间,提高程序的启动效率。这看起来是一个不错的功能,但是实际上运用的不是很广泛,主要是有一下限制:
- 编译时生成的本地代码太过于依赖本地环境,一旦环境有变化 (包括操作系统更新、.Net Framework版本更新、CPU更换等),以前生成的本地代码都不再适用。
- 编译时生成的本地代码必须要和程序集保持同步。
- 编译时生成的本地代码不能像运行时JIT编译那样根据运行时的情况对代码进行优化。
-
-
CL有哪些功能
这里借用Jeffrey Richter的一段原话:
At first, I thought that the .NET Framework was an abstraction layer over the Win32 API and COM. As I invested more and more of my time into it, however, I realized that it was much bigger. In a way, it is its own operating system. It has its own memory manager, its own security system, its own file loader, its own error handling mechanism, its own application isolation boundaries (AppDomains), its own threading models, and more.
虽然原文中Jeffrey Richter说的是.Net Framework,但很显然这些功能都是.Net Framework的核心组件CLR来提供的,正是CLR使.Net Framework并不是Win32 API和COM的一个抽象层,而是有了自己的"操作系统"(Jeffrey Richter的意思应该是在一定程度上,虚拟机也可以认为是一个小型的操作系统)。总结起来,CLR主要提供了一下功能:
- 基类库支持 (Base Class Library Support)
- 内存管理 (Memory Management)
- 线程管理 (Thread Management)
- 垃圾回收 (Garbage Collection)
- 安全性 (Security)
- 类型检查 (Type Checker)
- 异常处理 (Exception Manager)
- 即使编译 (JIT)
本系列主要围绕以上功能对CLR进行探讨。
参考资料: