《CLR via C#》學習筆記【2】

二、生成、打包、部署和管理應用程序及類型

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)開關命令,完整編譯命令行應如下:

 csc.exe /out:Program.exe/t:exe/r:MSCorLib.dll Program.cs 

但由於其他命令均爲默認命令,本例中的編譯命令行可以簡化爲

csc.exe Program.cs 

如果不想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使用該響應文件,可以像下面這樣調用它

csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs

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, 其中包含常用類型

下面將不常用類型編譯到一個單獨模塊,這樣一來如果程序集的用戶永遠不使用不常用類型,就不需要部署這個模塊。

csc /t:module RUT.cs

上述命令行造成C#編譯器創建名爲RUT.netmodule的文件。這是一個標準的DLL PE文件,但是CLR不能但單獨加載它。
接着編譯常用類型模塊事實上由於該模塊現在代表整個程序集,所以將輸出的文件名改爲MultiFileLibrary.dll

csc /out:NultiFileLibray.dll /t:library /addmodule:RUT.netmodule FUT.cs

由於指定了.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所示
Visual Studio的引用管理器
其中的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技術,應用程序還能自動檢查更新,並在用戶機器上安裝更新。
第三章將討論如何部署可以由多個應用程序訪問的共享程序集。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章