二、生成、打包、部署和管理應用程式及類型
2.1 .NET Framework部署目标
Windows多年來一直因為不穩定和過于複雜而口碑不佳。存在所謂”DLL hell“、安裝的複雜性等繁瑣的問題,而.NET Framework 正在嘗試徹底解決DLL hell的問題,也在很大程度上解決了應用程式狀态在使用者硬碟中四處分散的問題。 >和COM不同,類型不再需要系統資料庫中的設定。……像Microsoft SQL Server這樣的宿主應用程式隻能将少許權限授予代碼,而本地安裝的(自寄宿)應用程式可獲得完全信任(全部權限)
2.2 将類型生成到子產品中
System.Console是Microsoft實作好的類型,用于實作這個類型的各個方法的IL代碼存儲在MSCorLib.dll
public sealed class Program{
public static void Main(){
System.Console.WriteLine("Hi");
}
}
對于上述代碼,由于引用了Console類的WriteLine方法,要順利通過編譯,必須向C#編譯器提供一組程式集,使他能解析對外部類型的引用。是以需要添加r:MSCorLib.dll(此處”r“意為reference)開關指令,完整編譯指令行應如下:
但由于其他指令均為預設指令,本例中的編譯指令行可以簡化為
如果不想C#編譯器自動引用MSCorLib.dll程式集,可以使用/nostdlib開關。
2.2-1生成三種應用程式的編譯器開關
- 生成控制台使用者界面(Console User Interface, CUI)應用程式使用/t:exe開關;
- 生成圖形使用者界面(Graphical User Interface, GUI)應用程式使用/t:winexe開關;
- 生成Windows Store應用程式使用/t:appcontainerexe開關;
2.2-2響應檔案
編譯時可以指定包含編譯器設定指令的響應檔案,例如:假定響應檔案MyProject.rsp包含以下文本
/out:MyProject.exe
/target:winexe
為了讓CSC.exe使用該響應檔案,可以像下面這樣調用它
C#支援多個相應檔案,其先後順序服從就近原則,優先級為控制台指令>本地>全局。.NET Framework具有一個預設的全局CSC.rsp檔案,在運作CSC.exe進行編譯時會自動調用,全局CSC.rsp檔案中列出了所有的程式集,就不必使用C#的/reference開關顯式引用這些程式集,這會對編譯速度有一些影響,但不會影響最終的程式集檔案,以及執行性能,開發者也可以自己為全局CSC.rsp添加指令開關,但這可能為在其他機器上重制編譯過程帶來麻煩。
另外,指定/noconfig開關後,編譯器将忽略本地和全局CSC.rsp檔案。
2.3 中繼資料概述
再來回顧一下托管子產品的檔案結構,托管PE檔案由四部分構成,它們分别為:PE32(+)頭,CLR頭,中繼資料以及IL,接下來将展開談中繼資料的内部結構和作用
- PE32(+)頭是所有windows程式的标準資訊頭,詳情可參見
- CLR頭是一個小的資訊塊,是托管子產品特有的,包含生成時所面向的版本号、一些标志、和一個MethodDef token用來指定子產品的入口方法,最後,CLR頭還包含子產品内部的一些中繼資料表的大小的偏移量
- 中繼資料是由三種表構成的二進制資料塊,這三種表分别為定義表(definiton talbe)、引用表(reference table)和清單表(mainfest table)。
表1 常用的中繼資料定義表
中繼資料定義表名稱 | 說明 |
---|---|
ModuleDef | 總是包含對子產品進行辨別的一個記錄項,這個記錄項包含子產品檔案名和擴充名(不含路徑),以及子產品版本ID(為編譯器建立的GUID)。這樣可以在保留原始名稱記錄的前提下自由重命名檔案,但強烈反對重命名檔案,因為可能妨礙CLR在運作時正确定位程式集。 |
TypeDef | 子產品定義的每個類型在這個表中都有一個記錄項,包含類型的名稱、基類、标志(public/private etc.)以及一些索引,這些索引指向MethodDef中屬于該類型的方法、FieldDef表中該類的字段、PropertyDef表中該類型的屬性以及EventDef表中該類型的時間。 |
MetodDef | 子產品定義的每個方法在這個表中都有一個記錄項(包括入口方法)。每個記錄項都包含方法的名稱、标志、簽名以及方法的IL代碼在子產品中的偏移量(通俗地說,位置)。每個記錄項還引用了ParamDef表中的一個記錄項,後者包括與方法參數有關的更多資訊。 |
FieldDef | 子產品定義的每一個字段在這個表中都有一個記錄項。每個記錄項都包括标志、類型和名稱。 |
ParamDef | 子產品定義的每個參數在這個表中都有一個記錄項。每個記錄項包含标志(in/out/retval等)、類型和名稱。 |
PropertyDef | 子產品定義的每個屬性在這個表中都有一個記錄項。每個記錄項都包含标志、類型和名稱。 |
EventDef | 子產品定義的每個事件在這個表中都有一個記錄項。每個記錄項都包含标志和名稱。 |
表1:代碼中定義的任何東西都将在上表中的某個表建立一個記錄項。
表2 常用的引用中繼資料表
引用中繼資料表名稱 | 說明 |
---|---|
AssemblyRef | 子產品中引用的每個程式集在這個表中都有一個記錄項。每個及錄像都包含綁定(bind) ① 該程式集所需的資訊:程式集名稱(不包含路徑和擴充名)、版本号、語言文化及公鑰Token(根據釋出者的公鑰生成一個小的哈希值,辨別了所引用程式集的釋出者)。每個記錄項還包含一些标志和一個哈希值。該哈希值本應作為所引用程式集的二進制資料校驗和來使用。但是目前CLR完全忽略該哈希值,未來的CLR可能同樣如此。 |
ModuleRef | 實作該子產品所引用的類型的每個PE子產品在這個表中都有一個記錄項。每個記錄項都包含子產品的檔案名和擴充名(不含路徑),如果存在别的子產品實作了你需要的類型,這個表的作用便是同哪些類型建立綁定關系 |
TypeRef | 子產品引用的每一個類型在這個表中都有一個記錄項。每個記錄項都包括子產品的檔案名和一個引用(指向該類型的位置)如果類型在另一個類型中實作,引用指向一個TypeRef記錄項。如果類型在同一個子產品中實作,引用指向一個ModuleDef記錄項。如果類型在調用程式集内的另一個子產品中實作,引用指向一個ModuleDef記錄項。如果類型在不同程式集中實作,引用指向一個AssemblyRef記錄項 |
MemberRef | 子產品引用的每個成員(字段和方法,以及屬性方法和事件方法)在這個表中都有一個記錄項。每個記錄項都包含成員的名稱和簽名,并指向對成員進行定義的那個類型的TypeRef記錄項 |
①譯者注:bind在文檔中有時譯為“聯編”,binder有時譯為”聯程式設計式“,這裡譯為“綁定”和“綁定器”
2.4 将子產品合并成程式集
程式集(Assembly)是一個或多個類型定義檔案及資源檔案的集合。在程式集的所有檔案中,有一個檔案容納了清單(Manifest),如上一節一開始所述,清單也是中繼資料的組成部分之一,表中主要包含作為程式集組成部分的那些檔案的名稱。此外還描述程式集的版本、語言文化、釋出者、公開導出類型以及構成程式集的所有檔案。
CLR操作的是程式集,對于程式集,有以下幾點重要特性:
- 程式集定義了可重用的類型。
- 程式集用一個版本号标記。
- 程式集可以關聯安全資訊。
對于一個程式集來說,除了包含清單中繼資料表的檔案,程式集中的其他檔案獨立時不具備以上特點
Microsoft為何考慮要引入程式集這一概念?這是因為使用程式集,可重用類型的邏輯表示和實體表示就可以分開。實體上,可以将常用的類型放在一個檔案中,不常用的程式放在另一些檔案中,隻在使用時加載,但是在邏輯上,這些程式仍然被組織于同一程式集中,不需要編寫額外的代碼顯式進行連結。
提示:總之,程式集是進行重用、版本控制和應用安全性設定的基本單元。
表3 清單中繼資料表
清單中繼資料表名稱 | 說明 |
---|---|
AssemblyDef | 如果子產品辨別的是程式集,這個中繼資料表就包含單一記錄項來列出程式集名稱(不包含路徑和擴充名)、版本(major,minor,build和revision)、語言文化、标志、雜湊演算法以及釋出者公鑰(可為null) |
FileDef | 作為程式集一部分的每個PE檔案和資源檔案在這個表中都有一個記錄項(清單本身所在的檔案除外,該檔案在AssemblyDef表的單一記錄項中列出)。在每個記錄項中,都包含檔案名和擴充名(不含路徑)、哈希值和一些标志。如果程式集隻包含他自己的檔案 ① ,FileDef将無記錄 |
ManifestResourceDef | 作為程式集一部分的每個資源在這個表中都有一個記錄項。記錄項中包含資源名稱、一些标志(如果程式集外部可見,就為public,否則為private)以及FileDef表的一個索引(指出資源或流包含在哪個檔案中)。如果資源不是獨立檔案(比如.jpg或者.gif檔案),那麼資源就是包含在PE檔案中的流。對于嵌入資源,記錄項還包含一個偏移量,指出資源流在PE檔案中的起始位置 |
ExportedTypesDef | 從程式集的所有PE子產品中導出的每個public類型在這個表中都有一個記錄項。記錄項中包含類型名稱、FileDef表的一個索引(指出類型由程式集的哪個檔案實作)以及TypeDef表的一個索引。注意,為了節省空間,從清單所在檔案導出的類型不再重複,因為可以通過中繼資料的TypeDef表擷取類型資訊 |
①譯者注:所謂“如果程式集隻包含他自己的檔案“,是指程式集隻包含他的主子產品,不包含其他非主子產品和資源檔案。
指定以下任何指令行開關,C#編譯器都會生成程式集: /t: exe, /t: winexe, /t: appcontainerexe, /t: library 或者/t: winmdobj。這些開關會訓示編譯器生成含有清單中繼資料表的PE檔案。
除了這些開關,C#編譯器還支援/t: module開關。這個開關訓示編譯器生成一個不包含清單中繼資料表的PE檔案。這樣生成的肯定是一個DLL PE檔案。CLR要想通路其中的任何類型,必須先将該檔案添加到一個程式集中。使用/t: module開關時,C#編譯器預設為輸出檔案使用.netmodule擴充名。
遺憾的是,不能直接從Microsoft Visual studio內建開發環境中建立多檔案程式集,隻能用指令行工具建立多檔案程式集。
可以通過C#編譯器,AL連接配接器等方法生成多子產品程式集,下面将展開介紹
2.4-1通過C#編譯器生成程式集
如果用C#編譯器生成含清單的PE檔案,可以使用/addmodule開關。假定有如下兩個源代碼檔案:
- RUT.cs, 其中包含不常用類型
- FUT.cs, 其中包含常用類型
下面将不常用類型編譯到一個單獨子產品,這樣一來如果程式集的使用者永遠不使用不常用類型,就不需要部署這個子產品。
上述指令行造成C#編譯器建立名為RUT.netmodule的檔案。這是一個标準的DLL PE檔案,但是CLR不能但單獨加載它。
接着編譯常用類型子產品事實上由于該子產品現在代表整個程式集,是以将輸出的檔案名改為MultiFileLibrary.dll
由于指定了.t: library開關,是以生成的是含有清單中繼資料表的DLL PE檔案。/addmodule:RUT.netmodule 開關告訴編譯器RUT.netmodule檔案是程式集的一部分,進而将其添加到FileDef清單中繼資料表,并将RUT.netmodule的公開導出類型添加到ExportedTypesDef清單源資料表。
編譯器最終建立如圖2所示的兩個檔案,清單在右邊的檔案中。
MultilFileLibrary.dll除了和RUT.netmodule一樣包括一些描述自身類型、方法、字段等的定義中繼資料表外,還包含額外的清單中繼資料表,這使MultiFileLibrary.dll(聯合RUT.netmodule)成為了程式集。清單中繼資料表描述了程式集的所有檔案(MultiFileLibrary.dll本身和RUT.netmodule)。清單中繼資料表還包含從MultiFuileLibraty.dll和RUT.netmodule導出的所有公共類型
以下供參考。中繼資料token試一個4位元組的值。其中高位位元組指明token的類型(0x01=TypeRef, 0x02=TypeDef, 0x23=AssemblyRef, 0x26=File(檔案定義), 0x27=ExportedType)更多可參見 .NET Framework SDK包含的 CORHdr.h 檔案中的CorTokenType枚舉類型。
在生成新程式集的時候,所引用的程式集中的所有檔案都必須存在。
但在運作時,隻有被調用的方法确實引用了未加載程式集中的類型時,才會加載程式。換言之,為了運作程式,并不要求被引用的程式集的所有檔案都存在。
2.4-2 使用程式集連結器生成程式集
除了使用C#編譯器,還可以使用”程式集連結器“實用程式AL.exe來建立程式集。如果程式集要求包含由不同編譯器生成的子產品(而這些編譯器不支援與C#編譯器的/addmodule開關等家的幾種機制),或者生成時不清楚程式集的打包要求,程式集連接配接器就顯得相當有用。還可以用AL.exe來生成隻含資源的程式集,也就是所謂的附屬程式集,他們通常用于本地化,本章稍後會讨論附屬程式集的問題。
AL.exe能生成EXE檔案,或者生成隻包含清單的DLL PE檔案。
為了了解AL.exe的工作原理,讓我們改變一下MultiFileLibrary.dll程式的內建方式:
csc /t:module RUT.cs
csc /t:module FUT.cs
al /out:MultiFileLibrary.dll /t:library FUT.netmodule RUT.netmodule
圖3展示了執行這些指令後生成的檔案。
程式集連結器不能将多個檔案合并成一個檔案。
2.4-3為程式集添加資源檔案
用AL.exe建立程式集時,可用/enbed[resource]開關将檔案作為資源添加到程式集。該開關擷取任意檔案,并将檔案内容嵌入最終的PE檔案。也可用/Link[resource]開關擷取資源檔案,但隻指出資源包含在程式集的哪個檔案,并不嵌入到PE檔案中;該資源檔案獨立,并必須與程式集檔案一同被打包部署
相似的,C#編譯器用/resource開關将資源嵌入PE檔案,用/linkresource開關添加記錄項引用資源檔案。以上開關均會修改ManifestResourceDef清單表添加記錄項,外部引用的開關還會修改FileDef表以指出資源封包件。
2.4-4 使用VS IDE将程式集添加到項目中
一個項目所需的程式集,除了顯式的在代碼中引用命名空間外,還要在項目引用管理器中引用,為此請打開解決方案資料總管,右擊想添加引用的項目,選擇“添加引用”打開“引用管理器”對話框,如圖4所示
其中的COM選項允許從托管代碼中通路一個非托管COM伺服器。這是通過Visual Studio自動生成的一個托管代理類實作的。
2.5 程式集版本資源資訊
AL.exe或CSC.exe生成PE檔案程式集時,還會在PE檔案中嵌入标準的Win32版本資源。
在應用程式代碼中調用System.Diagnostics.FileVersionInfo的靜态方法GetVersionInfo并傳遞程式集路徑作為參數可以擷取并檢查這些資訊。
生成程式集時,這些特性在源代碼中應用于assembly級别。
Visual Studio建立C#項目時會在一個Properties檔案夾中自動建立AssemblyInfo.cs檔案。可直接打開該檔案并修改自己的程式集特有資訊。
以下為上圖中由IDE自動生成的AssemblyInfo.cs檔案中的代碼片段,該代碼片段定義了程式集資訊,右側的詳細資訊視窗所來自的程式集便由此段代碼所屬項目生成。
// 有關程式集的一般資訊由以下
// 控制。更改這些特性值可修改
// 與程式集關聯的資訊。
[assembly: AssemblyTitle("LentilToolbox")]
[assembly: AssemblyDescription("Licensed under the MIT license")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LentilToolbox")]
[assembly: AssemblyCopyright("Copyright © 2016 Lentil Sun")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
//将 ComVisible 設定為 false 将使此程式集中的類型
//對 COM 元件不可見。 如果需要從 COM 通路此程式集中的類型,
//請将此類型的 ComVisible 特性設定為 true。
[assembly: ComVisible(false)]
// 如果此項目向 COM 公開,則下列 GUID 用于類型庫的 ID
[assembly: Guid("ac315d57-80ca-4e7a-b55c-064b94547552")]
// 程式集的版本資訊由下列四個值組成:
//
// 主版本
// 次版本
// 生成号
// 修訂号
//
//可以指定所有這些值,也可以使用“生成号”和“修訂号”的預設值,
// 方法是按如下所示使用“*”: :
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.0.2")]
[assembly: AssemblyFileVersion("1.1.0.2")]
windows資料總管的屬性對話框顯然遺漏了一些特性值。最遺憾的是沒有顯示AssemblyVersion這個特性的值,因為CLR加載程式集時會使用這個值。
表4 版本資源字段和對應的AL.exe開關/定制特性
版本資源 | AL.exe開關 | 定制特性/說明 |
---|---|---|
FILEVERSION | /fileversion | System.Reflection.AssemblyFileVersionAttribute |
PRODUCTVERSION | /productversion | System.Reflection.AssemblyInformationalVersionAttribbute |
FILEFLAGS | (無) | 總是設為VS_FFI_FILEFLAGSMASK(在WinVer.h中定義為0x0000003F) |
FILEOS | (無) | 總是0 |
FILEOS | (無) | 目前總是VOS_WINDOWS32 |
FILETYPE | /target | 如果指定了/target:exe或target:winexe,就設為VFT_APP;如果指定了/target:library,就設為 VFT_DLL |
FILESUBTYPE | (無) | 總是設為VFT2_UNKNOWN(該字段對VFT_APP和VFT_DLL無意義) |
AssemblyVersion | /Version | System.Reflection.AssemblyVersionAttribute |
Comments | /description | System.Reflection.AssemblyDescriptionAttribute |
CompanyName | /Company | System.Reflection.AssemblyCompanyAttrbute |
FileDescription | /title | System.Reflection.AssemblyTitleAttribute |
FileVersion | /version | System.Reflection.AssemblyFileVersionAttribute |
InternalName | /out | 設定為指定的輸出檔案的名稱(無擴充名) |
LegalCopyright | /copyright | System.Reflection.AssemblyCopyrighhtAttrubute |
LegalTrademarks | /trademark | System.Reflection.AssemblyTrademarkAttribute |
OriginalFilename | /out | 設為輸出檔案的名稱(無路徑) |
PrivateBuild | (無) | 總是空白 |
ProductName | /product | System.Reflection.AssemblyProductAttribute |
ProductVersion | /Productversion | System.Reflection.AssemblyInformationalVersionAttribute |
2.5-1 版本号
上表指出可向程式集應用幾個版本号,所有這些版本号都具有相同的格式如下
表5 版本号格式
- | major(主版本号) | minor(次版本号) | build(内部版本号) | revision(修訂号) |
---|---|---|---|---|
示例 | 2 | 5 | 719 | 2 |
注意:程式集有三個版本号,每個版本号都有不同的用途:
- AssemblyFileVersion:這個版本号存儲在Win32版本資源中供使用者參考,CLR既不檢查,也不關心,這個版本号的作用是說明該程式集的版本。
- AssemblyInformationalVersion:同上,這個版本号存儲在Win32版本資源中供使用者參考,CLR既不檢查,也不關心,這個版本号作用是說明使用該程式集的産品的版本。
- AssemblyVersion:存儲在AssemblyDef清單中繼資料表中,CLR在綁定到強命名程式集時會用到它。這個版本号很重要,它唯一性地辨別了程式集。
2.6 語言文化
除了版本号,語言文化(culture) ① 也作為其身份辨別的一部分。例如,可能有一個程式集限定德語使用者,另一個限定英語使用者。語言文化标準記錄于RFC1766,表6展示了一些例子
①譯者注:文檔翻譯為“區域性”(部落客:面向使用者的界面大多直譯為語言吧)
表6 程式集語言文化标記的例子
主标記 | 副标記 | 語言文化 |
---|---|---|
zh | (無) | 中文 |
zh | Hans | 中文(簡體) |
zh | Hant | 中文(繁體) |
en | (無) | 英文 |
en | GB | 英國英語 |
en | US | 美國英語 |
未指定具體語言文化的程式內建為語言文化中性(Culture neutral)。
如果應用程式包含語言文化特有的資源,Microsoft強烈建議專門建立一個程式集來包含代碼和應用程式的預設(或附加)資源。生成該程式集時不要指定具體的語言文化,其他程式集通過引用該程式集來建立和操縱他的公開類型。
标記了語言文化的程式集稱為附屬程式集(satellite assembly)
一般不要生成引用了附屬程式集的程式集。換言之,程式集的AssemblyRef記錄項隻應引用語言文化中性的程式集。要通路附屬程式集中的類型或成員,應使用第23章“程式集加載和反射”介紹的反射技術。
2.7 簡單應用程式部署(私有部署的程式集)
Windows Store應用對程式集的打包有一套很嚴格的規則,Visual Studio會将應用程式所必要的程式集打包成一個.appx檔案。該檔案要麼上傳到Windows Store,要麼side-load到機器。使用者安裝應用時,其中包含的所有程式集都進入一個目錄。CLR從該目錄加載程式集
對于非Windows Store的應用,程式打包的方式沒有限制。可以使用.cab檔案(從Internet下載下傳時使用,旨在壓縮檔案并縮短下載下傳時間)。還可以将程式打包成一個MSI檔案,以便由Windows Installer服務(MSIExec.exe)使用。也可以使用Visual Studio内建機制釋出應用程式,具體做法是打開項目屬性頁并點選“釋出”标簽。這個MSI檔案還能安裝必備元件,以及利用ClickOnce技術,應用程式還能自動檢查更新,并在使用者機器上安裝更新。
第三章将讨論如何部署可以由多個應用程式通路的共享程式集。