生成、打包、部署和管理应用程序及类型

作者:追风剑情 发布于:2022-3-24 10:02 分类:C#

本章内容:
● .NET Framework 部署目标
● 将类型生成到模块中
● 元数据概述
● 将模块合并成程序集
● 程序集版本资源信息
● 语言文化
● 简单应用程序部署(私有部署的程序集)
● 简单管理控制(配置)

在解释如何为 Microsoft .NET Framework 开发程序之前,首先讨论一下生成、打包和部署 应用程序及其类型的步骤。本章重点解释如何生成仅供自己的应用程序使用的程序集。第 3 章“共享程序集和强命名程序集”将讨论更高级的概念,包括如何生成和使用程序集, 使其中包含的类型能由多个应用程序共享。这两章会谈及管理员能采取什么方式来影响应 用程序及其类型的执行。

当今的应用程序都由多个类型构成,这些类型通常是由你和 Microsoft 创建的。除此之外, 作为一个新兴产业,组件厂商们也纷纷着手构建一些专用类型,并将其出售给各大公司, 以缩短软件项目的开发时间。开发这些类型时,如果使用的语言是面向 CLR 的,这些类型 就能无缝地共同工作。换言之,用一种语言写的类型可以将另一个类型作为自己的基类使 用,不用关心基类用什么语言开发。

一、.NET Framework部署目标

Windows 多年来一直因为不稳定和过于复杂而口碑不佳。不管对它的评价对不对,之所以 造成这种状况,要归咎于几方面的原因。首先,所有应用程序都使用来自 Microsoft 或其他厂商的动态链接库(Dynamie-Link Library,DLL)。由于应用程序要执行多个厂商的代码, 所以任何一段代码的开发人员都不能百分之百保证别人以什么方式使用这段代码。虽然这 种交互可能造成各种各样的麻烦,但实际一般不会出太大的问题,因为应用程序在部署前 会进行严格测试和调试。

但对于用户,当一家公司决定更新其软件产品的代码,并将新文件发送给他们时,就可能 出问题。新文件理论上应该向后兼容以前的文件,但谁能对此保证呢?事实上,一家厂商 更新代码时,经常都不可能重新测试和调试之前发布的所有应用程序,无法保证自己的更 改不会造成不希望的结果。

很多人都可能遭遇过这样的问题:安装新应用程序时,它可能莫名其妙破坏了另一个已经 安装好的应用程序。这就是所谓的“DLL hell”。这种不稳定会对普通计算机用户带来不 小的困扰。最终结果是用户必须慎重考虑是否安装新软件。就我个人来说,有一些重要的 应用程序是平时经常都要用到的。为了避免对它们产生不好的影响,我不会冒险去“尝鲜”。

造成 Windows 口碑不佳的第二个原因是安装的复杂性。如今,大多数应用程序在安装时都 会影响到系统的全部组件。例如,安装一个应用程序会将文件复制到多个目录,更新注册 表设置,并在桌面和“开始”菜单上安装快捷方式。问题是,应用程序不是一个孤立的实 体。应用程序备份不易,因为必须复制应用程序的全部文件以及注册表中的相关部分。除 此之外,也不能轻松地将应用程序从一台机器移动到另一台机器。只有再次运行安装程序, 才能确保所有文件和注册表设置的正确性。最后,即使卸载或移除了应用程序,也免不了 担心它的一部分内容仍潜伏在我们的机器中。

第三个原因涉及安全性。应用程序安装时会带来各种文件,其中许多是由不同的公司开发 的。此外,Web 应用程序经常会悄悄下载一些代码(比如 ActiveX 控件),用户根本注意不 到自己的机器上安装了这些代码。如今,这种代码能够执行任何操作,包括删除文件或者 发送电子邮件。用户完全有理由害怕安装新的应用程序,因为它们可能造成各种各样的危 害。考虑到用户的感受,安全性必须集成到系统中,使用户能够明确允许或禁止各个公司 开发的代码访问自己的系统资源。

阅读本章和下一章可以知道,.NET Framework 正在尝试彻底解决 DLL hell 的问题。另 外,.NET Framework 还在很大程度上解决了应用程序状态在用户硬盘中四处分散的问题。 例如,和 COM 不同,类型不再需要注册表中的设置。但遗憾的是,应用程序还是需要快 捷方式。安全性方面,NET Framework 包含称为“代码访问安全性”(Code Aceess Security) 的安全模型。Windows 安全性基于用户身份,而代码访问安全性允许宿主设置权限,控制 代码,而本地安装的(自寄宿)应用程序可获得完全信任(全部权限)。以后会讲到,.NET 加载的组件能做的事情。像 Microsoft SQL Server 这样的宿主应用程序只能将少许权限授予 Framework 允许用户灵活地控制哪些东西能够安装,哪些东西能够运行。他们对自己机器 的控制上升到一个前所未有的高度。

二、将类型生成到模块中

本节讨论如何将包含多个类型的源代码文件转变为可以部署的文件。先看下面这个简单的 应用程序:

public sealed class Program {
   public static void Main() {
      System.Console.WriteLine("Hi");
   }
}

该应用程序定义了 Program 类型,其中有名为 Main 的 public static 方法。Main中引用了 另一个类型 System.Console。System.Console 是 Microsoft 实现好的类型,用于实现这个类 型的各个方法的IL代码存储在 MSCorLib.dll 文件中。总之,应用程序定义了一个类型,还 使用了其他公司提供的类型。

为了生成这个示例应用程序,请将上述代码放到一个源代码文件中(假定为 Program.cs),然 后在命令行执行以下命令:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs
这个命令行指示 C#编译器生成名为 Program.exe 的可执行文件(/out:Program.exe)。生成的 文件是 Win32 控制台应用程序类型(/t[arget]:exe)。

C#编译器处理源文件时,发现代码引用了 System.Console 类型的 WriteLine 方法。此时, 编译器要核实该类型确实存在,它确实有 WriteLine 方法,而且传递的实参与方法形参匹 配。由于该类型在 C#源代码中没有定义,所以要顺利通过编译,必须向 C#编译器提供一 组程序集,使它能解析对外部类型的引用。在上述命令行中,我添加了 /r[eference]:MSCorLib.dll 开关,告诉编译器在 MSCorLib.dll 程序集中查找外部类型。

MSCorLib.dll 是特殊文件,它包含所有核心类型,包括 Byte,Char,String,Int32 等等。 事实上,由于这些类型使用得如此频繁,以至于 C#编译器会自动引用 MSCorLib.dll程序集。 换言之,命令行其实可以简化成下面这样(省略/r 开关):
csc.exe /out:Program.exe /t:exe Program.cs
此外,由于/out:Program.exe 和 /t:exe 开关是 C#编详器的默认设定,所以能继续简化成以 下形式:
csc.exe Program.cs
如果因为某个原因不想 C#编译器自动引用 MSCorLib.ll程序集,可以使用 /nostdlib 开关。 Microsoft 生成 MSCorLib.dll程序集自身时便使用了这个开关。例如,用以下命令行编译 Program.cs 会报错,因为它使用的 System.Console 类型是在 MSCorLib.odll 中定义的:
csc.exe /out:Program.exe /t:exe /nostdlib Program.cs
现在更深入地思考一下C#编译器生成的 Program.exe 文件。这个文件到底是什么?首先,

它是标准 PE(可移植执行体,Portable Executable)文件。这意味着运行 32 位或 64 位 Windows 的计算机能加载它,并能通过它执行某些操作。Windows 支持三种应用程序。生成控制台 用户界面(Console User Interface,CUD)应用程序使用 /t:exe 开关;生成图形用户界面 (Graphical User Interface,GUI)应用程序使用 t:winexe 开关;生成 Windows Store 应用使用 /t:appcontainerexe 开关。

三、响应文件

结束对编译器开关的讨论之前,让我们花点时间了解一下响应文件。响应文件是包含一组 编译器命令行开关的文本文件。执行 CSC.exe 时,编译器打开响应文件,并使用其中包含 的所有开关,感觉就像是这些开关直接在命令行上传递给 CSC.exe。要告诉编译器使用响 应文件,在命令行中,请在@符号之后指定响应文件的名称。例如,假定响应文件 MyProject.rsp 包含以下文本:
/out:MyProject.exe
/target:winexe
为了让 CSC.exe 使用这些设置,可以像下面这样调用它:
csc.exe @MyProject.rsp CodeFilel.cs CodeFile2.cs
这就告诉了 C#编译器输出文件的名称和要创建哪种类型的应用程序。可以看出,响应文件 能带来一些便利,不必每次编译项目时都手动指定命令行参数。

C#编译器支持多个响应文件。除了在命令行上显式指定的文件,编译器还会自动查找名为 CSC.rsp 的文件。CSC.exe 运行时,会在 CSC.exe 所在的目录查找全局 CSC.rsp 文件。想应 用于自己所有项目的设置应放到其中。编译器汇总并使用所有响应文件中的设置。本地和 全局响应文件中的某个设置发生冲突,将以本地设置为准。类似地,命令行上显式指定的 设置将覆盖本地响应文件中的设置。

.NET Framework 安装时会在%SystemRoot%\Microsoft.NET\Framework(64)\vX.X.X 目录中安 装默认全局 CSC.rsp 文件(X.XX 是你安装的.NET Framework 的版本号)。
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework
这个文件的最新版 本包含以下开关:

由于全局 CSC.rsp 文件引用了列出的所有程序集,所以不必使用 C#编译器的/reference 开 关显式引用这些程序集。这个响应文件为开发人员带来了极大的方便,因为可以直接使用 Microsoft 发布的各个程序集中定义的类型和命名空间,不必每次编译时都指定/reference 编译器开关。

引用所有这些程序集对编译器的速度有一点影响。但是,如果源代码没有引用上述任何程 序集定义的类型或成员,就不会影响最终的程序集文件,也不会影响程序的执行性能。

当然,要进一步简化操作,还可在全局 CSC.rsp 文件中添加自己的开关。但这样一来,在 其他机器上重现代码的生成环境就比较困难了:在每台用于生成的机器上,都必须以相同 方式更新 CSC.rsp。另外,指定 /noconfig 命令行开关,编译器将忽略本地和全局 CSC.rsp 文件。

四、元数据概述

现在,我们知道了创建的是什么类型的 PE 文件。但是,Program.exe 文件中到底有什么? 托管 PE 文件由 4 部分构成:PE32(+)头、CLR 头、元数据以及IL。PE32(+)头是 Windows 要求的标准信息。CLR 头是一个小的信息块,是需要 CLR 的模块(托管模块)特有的。这个 头包含模块生成时所面向的 CLR 的 major(主)和 minor(次)版本号:一些标志(flag):一个 MethodDef token(稍后详述),该 token 指定了模块的入口方法(前提是该模块是 CUI、GUI 或 Windows Store执行体);一个可选的强名称数字签名(将在第3章讨论)。最后,CLR头 还包含模块内部的一些元数据表的大小和偏移量。可以查看 CorHdr.h 头文件定义的 IMAGE_COR20_HEADER 来了解 CLR 头的具体格式。

元数据是由几个表构成的二进制数据块。有三种表,分别是定义表(definition table)、引用表(reference table)和清单表(manifest table)。表 2-1 总结了模块元数据块中常用的定义表。

常用的元数据定义表
元数据定义表名称 描述
ModuleDef 总是包含对模块进行标识的一个记录项。该记录项包含模块文件名和扩展名(不含路径),以及模块版本 ID(形式为编译器创建的 GUID)。这样可在保留原始名称记录的前提下自由重命名文件。但强烈反对重命名文件,因为可能妨碍 CLR 在运行时正确定位程序集
TypeDef 模块定义的每个类型在这个表中都有一个记录项。每个记录项都包含类型的名称、基类型、一些标志(public, private 等)以及一些索引,这些索引指向 MethodDef表中该类型的方法、FieldDef表中该类型的字段、PropertyDef表中该类型的属性以及 EventDef 表中该类型的事件
MethodDef 模块定义的每个方法在这个表中都有一个记录项。每个记录项都包含方法的名称、一些标志(private,public, virtual,abstract,static,final 等)、签名以及方法的IL代码在模块中的偏移量。每个记录项还引用了 ParamDef 表中的一个记录项,后者包括与方法参数有关的更多信息
FieldDef 模块定义的每个字段在这个表中都有一个记录项。每个记录项都包含标志(private, public 等)、类型和名称
ParamDef 模块定义的每个参数在这个表中都有一个记录项。每个记录项都包含标志(in,out,retval等)、类型和名称
PropertyDef 模块定义的每个属性在这个表中都有一个记录项。每个记录项都包含标志、类型和名称
EventDef 模块定义的每个事件在这个表中都有一个记录项。每个记录项都包含标志和名称

编译器编译源代码时,代码定义的任何东西都导致在表 2-1 列出的某个表中创建一个记录 项。此外,编译器还会检测源代码引用的类型、字段、方法、属性和事件,并创建相应 的元数据表记录项。在创建的元数据中包含一组引用表,它们记录了所引用的内容。表 2-2 总结了常用的引用元数据表。

bind 在文档中有时翻译成“联编”,binder 有时翻译成“联编程序”。本书采用“绑定”和“绑定器”。一译注

常用的引用元数据表
引用元数据表名称 说明
AssemblyRef 模块引用的每个程序集在这个表中都有一个记录项。每个记录项都包含绑定该 程序集所需的信息:程序集名称(不含路径和扩展名)、版本号、语言文化(culture) 以及公钥 token(根据发布者的公钥生成的一个小的哈希值,标识了所引用程序集 的发布者)。每个记录项还包含一些标志(flag)和一个哈希值。该哈希值本应作为 所引用程序集的二进制数据的校验和来使用。但是,目前 CLR 完全忽略该哈希 值,未来的 CLR 可能同样如此
ModuleRef 实现该模块所引用的类型的每个 PE 模块在这个表中都有一个记录项。每个记录项都包含模块的文件名和扩展名(不含路径)。可能是别的模块实现了你需要的类型,这个表的作用便是建立同那些类型的绑定关系
TypeRef 模块引用的每个类型在这个表中都有一个记录项。每个记录项都包含类型的名称和一个引用(指向类型的位置)。如果类型在另一个类型中实现,引用指向一个TypeRef 记录项。如果类型在同一个模块中实现,引用指向一个 ModuleDef记录项。如果类型在调用程序集内的另一个模块中实现,引用指向一个ModuleRef记录项。如果类型在不同的程序集中实现,引用指向一个 AssemblyRef 记录项
MemberRef 模块引用的每个成员(字段和方法,以及属性方法和事件方法)在这个表中都有一个记录项。每个记录项都包含成员的名称和签名,并指向对成员进行定义的那个类型的 TypeRef 记录项

除了表 2-1 和表 2-2 所列的,还有其他许多定义表和引用表。但是,我的目的只是让你体 会一下编译器在生成的元数据中添加的各种信息。前面提到还有清单(manifest)元数据表, 它们将于本章稍后讨论。

可用多种工具检查托管 PE 文件中的元数据。我个人喜欢使用 ILDasm.exe,即IL Disassembler(IL 反汇编器)。要查看元数据表,请执行以下命令行:
ILDasm Program.exe

ILDasm.exe 将运行并加载 Program.exe 程序集。要采用一种美观的、容易阅读的方式查看 元数据,请选择“视图”|“元信息”|“显示!”菜单项(或直接按 Ctrl+M 组合键)。随后会 显示以下信息:

五、将模块合并成程序集

上一书讨论的 Program.exe 并非只是含有元数据的 PE 文件,它还是程序集(assembly)。程序 集是一个或多个类型定义文件及资源文件的集合。在程序集的所有文件中,有一个文件容 纳了清单(manifest)。清单也是一个元数据表集合,表中主要包含作为程序集组成部分的那 些文件的名称。此外,还描述了程序集的版本、语言文化、发布者、公开导出的类型以及 构成程序集的所有文件。

CLR操作的是程序集。换言之,CLR 总是首先加载包含“清单”元数据表的文件,再根据 “清单”来获取程序集中的其他文件的名称。下面列出了程序集的重要特点。
● 程序集定义了可重用的类型。
● 程序集用一个版本号标记。
● 程序集可以关联安全信息。
除了包含清单元数据表的文件,程序集其他单独的文件并不具备上述特点。

类型为了顺利地进行打包、版本控制、安全保护以及使用,必须放在作为程序集一部分的 模块中。程序集大多数时候只有一个文件,就像前面的 Program.exe 那样。然而,程序集还 可以由多个文件构成:一些是含有元数据的 PE 文件,另一些是.gif 或.jpg 这样的资源文件。 为便于理解,可将程序集视为一个逻辑 EXE 或 DLL。

Microsoft 为什么引入“程序集”的概念?这是因为使用程序集,可重用类型的逻辑表示与 物理表示就可以分开。例如,程序集可能包含多个类型。可以将常用类型放到一个文件中, 不常用类型放到另一个文件中。如果程序集要从 Internet 下载并部署,那么对于含有不常用 类型的文件,假如客户端永远不使用那些类型,该文件就永远不会下载到客户端。例如, 擅长制作 UI 控件的一家独立软件开发商(Independent Software Vendor,ISV)可选择在单独 的模块中实现 Active Accessibility 类型(以满足 Microsoft 徽标认证授权要求)。这样一来, 只有需要额外“无障碍访问”功能的用户才需要下载该模块。

为了配置应用程序去下载程序集文件,可在应用程序配置文件中指定 codeBase 元素(详见 第3 章)。在 codeBase 元素定义的 URL 所指向的位置,可找到程序集的所有文件。试图加 载程序集的一个文件时,CLR 获取 codeBase 元素的 URL,检查机器的下载缓存,判断文 件是否存在。如果是,直接加载文件。如果不是,CLR 去 URL 指向的位置将文件下载到 缓存。如果还是找不到文件,CLR 在运行时抛出 FileNotFoundException 异常。

① Microsoft Active Accessibility是一种基于 COM 的技术,能够为应用程序和 Active Accessibility 客户 端提供标准、一致的机制来交换信息。宗旨是帮助残障人士更有效地使用计算机。——译注

我想指出使用多文件程序集的三点理由。

● 不同的类型用不同的文件,使文件能以“增量”方式下载(就像前面在 Internet 下载的 例子中描述的那样)。另外,将类型划分到不同的文件中,可以对购买和安装的应用程 序进行部分或分批打包/部署。

● 可在程序集中添加资源或数据文件。例如,假定一个类型的作用是计算保险信息,需 要访问精算表才能完成计算。在这种情况下,不必在自己的源代码中嵌入精算表。相 反,可以使用一个工具(比如稍后要讨论的程序集链接器 AL.exe),使数据文件成为程 序集的一部分。顺便说一句,数据文件可为任意格式——包括文本文件,Microsoft Office Excel 电子表格文件以及Microsoft Office Word表格等——只要应用程序知道如何解析。

● 程序集包含的各个类型可以用不同的编程语言来实现。例如,一些类型可以用 C#实现, 一些用 Visual Basic 实现,其他则用其他语言实现。编译用 C#写的类型时,编译器会 生成一个模块。编译用 Visual Basic 写的类型时,编译器会生成另一个模块。然后可以 用工具将所有模块合并成单个程序集。其他开发人员在使用这个程序集时,只知道这 个程序集包含了一系列类型,根本不知道、也不用知道这些类型分别是用什么语言写 的。顺便说一句,如果愿意,可以对每个模块都运行ILDasm.exe,获得相应的 IL 源代 码文件。然后运行 ILAsm.exe,将所有 IL源代码文件都传给它。随后,ILAsm.exe 会 生成包含全部类型的单个文件。该技术的前提是源代码编译器能生成纯IL代码。

重要提示   总之,程序集是进行重用、版本控制和应用安全性设置的基本单元。它允许将类型和资源文件划分到单独的文件中。这样一来,无 论你自己,还是你的程序集的用户,都可以决定打包和部署哪些文件。 一旦CLR 加载含有清单的文件,就可确定在程序集的其他文件中,具体 是哪一些文件包含应用程序引用的类型和资源。程序集的用户(其他开发 人员)只需知道含有清单的那个文件的名称。这样一来,文件的具体划分 方式在程序集的用户那里就是完全透明的。你以后可以自由更改,不会 干扰应用程序的行为。
如果多个类型能共享相同的版本号和安全性设置,建议将所有这些类型 放到同一个文件中,而不是分散到多个文件中,更不要分散到多个程序 集中。这是出于对性能的考虑.每次加载文件或程序集,CLR和 Windows 都要花费一定的时间来查找、加载并初始化程序集。需要加载的文件/程 序集的数量越少,性能越好,因为加载较少的程序集有助于减小工作集 (working set),并缓解进程地址空间的碎片化。最后,nGen.exe 处理较大 的文件时可以进行更好的优化。

生成程序集要么选择现有的 PE 文件作为“清单”的宿主,要么创建单独的 PE 文件并只在 其中包含清单。表 2-3 展示了将托管模块转换成程序集的清单元数据表。

清单元数据表
清单元数据表名称 说明
AssemblyDef 如果模块标识的是程序集,这个元数据表就包含单一记录项来列出程序集名称(不含路径和扩展名)、版本(major, minor, build 和 revision)、语言文化(culture)、一些标志(flag)、哈希算法以及发布者公钥(可为 null)
FileDef

作为程序集一部分的每个PE文件和资源文件在这个表中都有一个记录项(清单本身所在的文件除外,该文件在 AssemblyDef 表的单一记录项中列出)。在每个记录项中,都包含文件名和扩展名(不含路径),哈希值和一些标志(flags)。如果程序集只包含它自己的文件,FileDef 表将无记录

所谓“如果程序集只包含它自己的文件”,是指程序集只包含它的主模块,不包含其他非主模块和资源文件。1.2 节已经说过,程序集是一个抽象概念,是一个或者多个模块文件和资源文件组成的逻辑单元,其中必定含有且只有一个后缀为.exe 或者.dll 的主模块文件。——评注

ManifestResourceDef 作为程序集一部分的每个资源在这个表中都有一个记录项。记录项中包含资源名称、一些标志(如果在程序集外部可见,就为 public:否则为 private)以及 FileDef 表的一个索引(指出资源或流包含在哪个文件中)。如果资源不是独立文件(比如jpg 或者gif 文件),那么资源就是包含在 PE 文件中的流。对于嵌入资源,记录项还包含一个偏移量,指出资源流在 PE 文件中的起始位置
ExportedTypesDef 从程序集的所有 PE 模块中导出的每个 public 类型在这个表中都有一个记录项。记录项中包含类型名称、FileDef 表的一个索引(指出类型由程序集的哪个文件实现)以及 TypeDef 表的一个索引。注意,为节省空间,从清单所在文件导出的类型不再重复,因为可通过元数据的 TypeDef 表获取类型信息

由于有了清单的存在,程序集的用户不必关心程序集的划分细节。另外,清单也使程序集具 有了自描述性(self-describing)。另外,在包含清单的文件中,一些元数据信息描述了哪些文 件是程序集的一部分。但是,那些文件本身并不包含元数据来指出它们是程序集的一部分。

注意   包含清单的程序集文件还有一个 AssemblyRef 表。程序集全部文 件引用的每个程序集在这个表中都有一个记录项。这样一来,工具只需 打开程序集的清单,就可知道它引用的全部程序集,而不必打开程序集 的其他文件。同样地,AssemblyRef表的存在加强了程序集的自描述性。

指定以下任何命令行开关,C#编译器都会生成程序集:/t[arget]:exe/t[arget]:winexe/t[arget]:appcontainerexe/t[arget]:library 或者 /t[arget]:winmdobj。所有这些开关都会 造成编译器生成含有清单元数据表的 PE 文件。这些开关分别生成 CUI 执行体、GUI 执行 体,Windows Store 执行体、类库或者 WINMD 库。

若使用/t[arget]:winmdobj,生成的 .winmdobj 文件必须传给 WinMDExp.exe 工具进行处理,以便将程 序集的公共 CLR 类型作为 Windows Runtime 类型公开。WinMDExp.exe 工具根本不会碰IL代码。

除了这些开关,C#编译器还支持 /t[arget]:module 开关。这个开关指示编译器生成一个不包 含清单元数据表的 PE 文件。这样生成的肯定是一个 DLL PE 文件。CLR 要想访问其中的 任何类型,必须先将该文件添加到一个程序集中。使用 /t:module 开关时,C#编译器默认为 输出文件使用 .netmodule 扩展名。

重要提示   遗憾的是,不能直接从 Microsoft Visual Studio 集成开发环境 (IDE)中创建多文件程序集。只能用命令行工具创建多文件程序集。

可通过许多方式将模块添加到程序集。如果用 C#编译器生成含清单的 PE 文件,可以使用 /addmodule 开关。为了理解如何生成多文件程序集,假定有两个源代码文件。
• RUT.cs,其中包含不常用类型。
• FUT.cs,其中包含常用类型。
下面将不常用类型编译到一个单独的模块。这样一来,如果程序集的用户永远不使用不常 用类型,就不需要部署这个模块。
csc /t:module RUT.cs
上述命令行造成 C#编译器创建名为 RUT.netmodule 的文件。这是一个标准的 DLL PE 文件, 但是,CLR 不能单独加载它。

接着将常用类型编译到另一个模块。该模块将成为程序集清单的宿主,因为这些类型会经 常用到。事实上,由于该模块现在代表整个程序集,所以我将输出文件的名称改为MultiFileLibrary.dll,而不是默认的 FUT.dll。
csc /out:MultiFileLibrary.dll /t:library /addmodule:RUT.netmodule FUT.cs
上述命令行指示 C#编译器编译 FUT.cs 来生成 MultiFileLibrary.dll。由于指定了/t:library 开 关,所以生成的是含有清单元数据表的 DLL PE 文件。/addmodule:RUT.netmodule 开关告 诉编译器 RUT.netmodule 文件是程序集的一部分。具体地说,/addmodule 开关告诉编译器 将文件添加到 FileDef 清单元数据表,并将 RUT.netmodule 的公开导出类型添加到 ExportedTypesDef 清单元数据表。

编译器最终创建图 2-1 所示的两个文件。

11111.jpg

RUT.netmodule 文件包含编译 RUT.cs 所生成的 IL 代码。该文件还包含一些定义元数据表, 描述了 RUT.cs 定义的类型、方法、字段、属性、事件等。还包含一些引用元数据表,描述 了 RUT.cs 引用的类型、方法等。MultiFileLibrary.dll 是一个单独的文件。与 RUT.netmodule 相似,MultiFileLibrary.dll包含编译 FUT.cs 所生成的IL代码以及类似的定义与引用元数据 表。然而,MultiFileLibrary.dll 还包含额外的清单元数据表,这使 MultiFileLibrary.dll 成为 了程序集。清单元数据表描述了程序集的所有文件(MultiFileLibrary.dll 本身和 RUT.netmodule)。清单元数据表还包含从 MultiFileLibrary.dll 和 RUT.netmodule 导出的所有 公共类型。

注意   清单元数据表实际并不包含从清单所在的 PE 文件导出的类型。这 是一项优化措施,旨在减少 PE 文件中的清单信息量。因此,上述说法“清 单元数据表还包含从 MultiFileLibrary.dll和 RUT.netmodule 导出的所有公 共类型”并非百分之百准确。不过,这种说法确实精准地反映了清单在 逻辑意义上公开的内容。

生成 MultiFileLibrary.dll 程序集之后,接着可用 ILDasm.exe 检查元数据的清单表,验证程 序集文件确实包含了对 RUT.netmodule 文件的类型的引用。FileDef和 ExportedTypesDef元 数据表的内容如下所示。

可以看出,RUT.netmodule 文件已被视为程序集的一部分,它的 token 是 0x26000001。在 ExportedTypesDef 表中可以看到一个公开导出的类型,名为 ARarelyUsedType。该类型的 实现 token 是 0x26000001,表明类型的 IL 代码包含在 RUT.netmodule 文件中。

客户端代码必须使用 /r[eference]:MultiFileLibrary.dll 编译器开关生成,才能使用 MutiFileLibrary.dll 程序集的类型。该开关指示编译器在搜索外部类型时加载 MultiFileLibrary.dll 程序集以及 FileDef 表中列出的所有文件。要求程序集的所有文件都已 安装,而且能够访问。删除 RUT.netmodule 文件导致 C#编译器会报告以下错误:fatal error CS009:未能打开元数据文件“c:\MultiFileLibrary.dll”—“导入程序集“c:\MultiFileLibrary.dll” 的模块“RUT.netmodule”时出错-系统找不到指定的文件。”

注意   以下内容供技术宅参考。元数据 token是一个4字节的值。其中, 高位字节指明 token 的类型(0x01=TypeRef,0x02=TypeDef , 0x23=AssemblyRef,0x26=File(文件定义),0x27=ExportedType)。要获取 完整列表,请参见.NET Framework SDK 包含的 CorHdr.h 文件中的 CorTokenType 枚举类型。token 的三个低位字节指明对应的元数据表中 的行。例如,0x26000001 这个实现 token 引用的是 File 表的第一行。大 多数表的行从1 而不是 0 开始编号。TypeDef 表的行号实际从 2 开始。

这意味着为了生成新程序集,所引用的程序集中的所有文件都必须存在。

客户端代码执行时会调用方法。一个方法首次调用时,CLR 检测作为参数、返回值或者局 部变量而被方法引用的类型。然后,CLR 尝试加载所引用程序集中含有清单的文件。如果 要访问的类型恰好在这个文件中,CLR 会执行其内部登记工作,允许使用该类型。如果清 单指出被引用的类型在不同的文件中,CLR 会尝试加载需要的文件,同样执行内部登记, 并允许使用该类型。注意,CLR 并非一上来就加载所有可能用到的程序集。只有在调用的 方法确实引用了未加载程序集中的类型时,才会加载程序集。换言之,为了让应用程序运 行起来,并不要求被引用程序集的所有文件都存在。

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号