CLR对组件代码的打包、部署和查找有自己一整套的概念和技术。这些概念和技术与COM、Java和Win32存在着根本上的差别。如果进一步认识CLR加载器,就能够很好地理解它们之间的差异。不过,我们必须先知道代码和元数据是如何打包的。
模块定义
CLR程序存在模块(module)中。一个CLR模块是一个字节流,通常作为一个文件存储在本地的文件系统中或者Web服务器上。
如图2.1所示,CLR模块采用Windows NT的PE/COFF可执行文件格式的扩展版。不过,CLR对PE/COFF文件格式进行了很大的扩展,而不是简单的沿袭。同时,CLR模块也是有效的Win32模块,可以通过LoadLibrary系统调用进行加载。不过,CLR模块用到的PE/COFF的功能极少。准确地说,CLR模块的大部分内容是作为不透明的数据,存放在PE/COFF文件的.text部分。
CLR模块包含代码、元数据和资源。代码一般以公共中间语言[common intermediate language(CIL)]的格式存放(尽管代码也可能被存为特定处理器的机器指令)。模块的元数据描述了模块中定义的类型,包含名字、继承关系、方法签名和依赖信息等。模块的资源由静态的只读数据组成,例如,字符串、位图,以及其他没有被存储为可执行代码的部分。
PE:Portable Exectuable,译为可移植可执行文件。COFF:Common Object File Format,公共对象文件格式。当CLR编译器对C#、VB.NET等源程序编译后产生 MSIL(中间语言)和元数据。元数据描述代码中的类型,包括每种类型的定义、每种类型的成员的签名、代码引用的成员和运行库在执行时使用的其他数据。MSIL 和元数据包含在一个可移植可执行 (PE) 文件中,此文件基于并扩展过去用于可执行内容的已公布的 Microsoft PE 和公共对象文件格式 (COFF)。这种文件格式包含 MSIL 或本机代码以及元数据,使得操作系统能够识别公共语言运行库映像。文件中的元数据以及 MSIL 的存在使代码能够描述自身,这意味着不再需要类型库或接口定义语言 (IDL)。
图2.1: CLR模块格式
CLR模块使用的文件格式具有较好的文档性,开发人员很少也会遇到未加工的格式。因此,即使对于急于求成的开发人员,一般也能够使用CLR提供的两个实用部件中的一个,用来进行可编程地生成模块。IMetaDataEmit接口是低级的COM接口,可以用来由经典的C++编程生成模块元数据。System.Reflection.Emit命名空间是高级的类库,用来由任何CLR正式语言(例如,C#、VB.NET)编程生成元数据和CIL。CodeDOM则工作在更高级的抽象层面上,不必知道和理解CIL。然而,对绝大多数开发人员来说,他们只是需要在开发时生成代码,而不是运行时,对此CLR编译器完全能够胜任。
C#编译器(CSC.EXE)、VB.NET编译器(VBC.EXE)和C++编译器(CL.EXE)都能够将源代码翻译成CLR模块。各个编译器通过命令行开关控制产生的模块种类。如表2.1所示:
C# / VB.NET |
C++ |
直接可加载的? |
从Shell中可直接运行? |
可访问控制台? |
/t:exe |
/CLR |
是 |
是 |
总是 |
/t:winexe |
/CLR /link
/subsystem:windows |
是 |
是 |
从不 |
/t:library |
/CLR /LD |
是 |
否 |
依赖主机(Host-dependent) |
/t:module |
/CLR:NOASSEMBLY
/LD |
否 |
否 |
依赖主机(Host-dependent) |
表2.1 模块输出选项
有4个可能的选项。在C#和VB.NET中,通过/target命令行开关(或者其快捷形式/t)选择目标文件的种类。C++编译器可以使用多个开关的组合;不过,通过/CLR开关,强制C++编译器生成CLR兼容的模块。下面所用到的C#和VB.NET开关,将采用它们的快捷形式。
选项/t:module产生“未加工的(raw)”模块,其文件扩展名默认为.netmodule。这种格式的模块不能独立地部署,CLR也不能直接加载它们。准确地说,开发人员必须在部署前,将这些“未加工的”模块与成型的组件(被称为程序集)进行关联。相比之下,用/t:library选项编译产生的模块,能够包含附加的元数据,允许开发人员将其作为独立代码进行部署。选项/t:library编译产生的模块,其文件扩展名默认为.DLL。
用/t:library编译产生的模块能被CLR直接加载,但不能从命令外壳或Windows资源管理器中作为可执行程序启动。如果要产生可执行程序,你必须采用/t:exe或者/t:winexe选项。这两个选项均产生扩展名为.EXE的文件,唯一的差别是:前者假定为控制台UI子系统使用,后者则假定为GUI子系统。如果没有指定/t选项,默认为/t:exe。
不管是使用/t:exe还是/t:winexe选项产生的模块,都必须定义一个初始入口点(initial entry point)。初始入口点是程序启动时CLR将自动执行的方法。程序员必须将这个方法声明为static,并且,在C#或VB.NET中,还必须命名为Main。程序员能够将入口点方法声明为无返回值,或者返回int型值。他们也可以将其声明为无参数形式,或者接受一个字符串数组的参数,它包含从外壳程序输入的命令行参数。下面是C#中Main方法的四种合法的实现。
static void Main() { }
static void Main(string[] argv) { }
static void Main() { return 0; }
static void Main(string[] argv) { return 0; }
对应的VB.NET代码为:
shared sub Main() : end sub
shared sub Main(argv as string()) : end sub
shared function Main() : return 0 : end function
shared function Main(argv as string())
return 0
end function
注意,这些方法并不是必须声明为public。不过,程序员只能在类型定义中声明Main方法,尽管类型名称并不重要。
下面是最小的C#程序,只是向控制台打印字符串“Hello,world”。
class myapp {
static void Main() {
System.Console.WriteLine("Hello, World");
}
}
在这个例子中,只有一个类,其中包含一个名为Main的静态方法。如果源文件包含多个类型,都有名为Main的静态方法,那么,C#或VB.NET编译可能无所适从(甚至导致错误)。为了解决这种二义性,程序员可以采用/main命令行开关,告诉C#或VB.NET编译器哪个类型将用作程序的初始入口点。
悥蠀(