Shared Source CLI Essentials

>>>  文章華國詩禮傳家—精彩書評選  >>> 簡體     傳統

This concise guide offers a road map for anyone wishing to navigate, understand, or alter the Microsoft Shared Source CLI ("Rotor") code. Written by members of the core team that designed the .NET Framework, this book is for anyone who wants a deeper understanding of what goes on under the hood of the .NET runtime and the ECMA CLI. Microsoft's Shared Source CLI (code-named "Rotor") is the publicly available implementation of the ECMA Common Language Infrastructure (CLI) and the ECMA C# language specification. Loaded with three million lines of source code, it presents a wealth of programming language technology that targets developers interested in the internal workings of the Microsoft .NET Framework, academics working with advanced compiler technology, and people developing their own CLI implementations. The CLI, at its heart, is an approach to building software that enables code from many independent sources to co-exist and interoperate safely. The book is a companion guide to Rotor's code. It provides a road map for anyone wishing to navigate, understand, or alter the Shared Source CLI code. This book illustrates the design principles used in the CLI standard and discusses the complexities involved when building virtual machines. Included with the book is a CD-ROM that contains all the source code and files. After introducing the CLI, its core concepts, and the Shared Source CLI implementation, Shared Source CLI Essentials covers these topics: the CLI type system; component packaging and assemblies; type loading and JIT Compilation; managed code and the execution engine; garbage collection and memory management; and the Platform Adaptation Layer (PAL) - a portability layer for Win32, Mac OS X, and FreeBSD.

  • CLI組件模型介紹

21世紀的程序員有很多煩惱。

首先,優秀的軟件比以前復雜多了。僅僅提供一個基于終端的簡單命令提示符或一個字符用戶界面已經不能被用戶接收了;現在的用戶需要包括各種優秀虛擬功能的豐富的圖形化用戶界面。軟件相關的數據,已經很少有結構適合存儲在本地系統的普通文件中的了;現在的情況是,計算機用戶已經十分依賴軟件所提供的查詢和報表的功能,而為了滿足用戶的這些需求,常常必須使用關系數據庫。而不斷變化的業務情況要求對長期存儲的數據進行的經常的修改也要求必須使用關系數據庫。從前單機環境就能夠滿足應用程序的部署需要,在這種環境中是通過文件或剪貼板來實現數據共享的;而現在這個星球上的大部分計算機都聯入了網絡,因此在這些計算機上部署的軟件不但必須要具有網絡功能,還必須能夠適應不斷變化的網絡環境。簡而言之,軟件開發早已不再是技術高手單槍匹馬就能夠完成任務的職業了,它已經變成了一種基于相當復雜的底層框架的集體行為了。

現在的程序員已經不能享受使用貼近處理器的底層工具(例如匯編或C編譯器)來從頭完成一個完整的軟件工程的奢侈了。也很少有人有時間或耐心來編寫中間層的框架了,即使是象HTTP協議的實現或者完成一個XML解析器這樣簡單的工作都很少有人來作,而具有調節底層框架來使系統達到所需的性能和質量的技能的人就更少了。現在軟件開發最主要的重心放在了可重用代碼和可重用組件上。操作系統加上少量的開發庫這種方式已經不能滿足軟件開發的需要。因此,不管你喜不喜歡,今天的程序員都必須依賴多個不同來源的代碼來支持他的應用程序,而且這些代碼要能夠正確可靠的協同工作。

為了適應當前軟件開發模式的變化趨勢,出現了一種新的軟件開發方法學――基于組件的軟件開發,它采用組合多個獨立的代碼模塊的方式來創建應用程序。通過組合多個來源的組件,可以快速和高效的創建應用程序。然而,這門技術對編程工具和軟件開發過程提出了新的需求。例如,因為依賴于其它由不被信任或不知名的開發者開發的組件,所以程序執行時的嚴格控制和在運行時對代碼的驗證成為軟件正常運行最基本的要求。在我們這個遍布網路連接的時代,基于組件的復雜軟件常常不需要客戶端的操作就能動態更新,而有時這些更新可能是惡意的。如果詢問那些計算機病毒的受害者保護她的機器和數據的干凈是否必要,或者和一個不熟練的計算機使用者談論那些他們所經歷的,由于安裝和卸載應用程序而導致系統莫名其妙的不穩定的情況,您就會發現基于組件的軟件造成的問題幾乎和它帶來的好處一樣之多。

多年來,對基于組件的軟件開發的商業宣傳和所預期的高效,被如何安全的將不同來源的組件組合起來工作的復雜性所抵消了。然而,最近10年中,我們看到了寄宿托管組件的虛擬執行環境在商業上的成功。托管組件是可以獨立開發和部署的簡單軟件部件,不過它們可以在應用程序中安全的共存。托管組件的”托管”,是指它們需要一個虛擬執行環境來提供運行時和執行服務。為了滿足組件的需要,這些環境著重致力于提供一個重點在于保證組件之間的安全合作和協作的結構良好的模型,而不只是簡單的暴露它下層的處理器和操作系統的物理資源。

如圖1-1所抽象描述的那樣,虛擬執行環境和托管組件為三個不同的軟件群體提供了很多好處:程序員,開發編程工具和編程庫的人,以及那些管理在虛擬執行環境中運行的軟件的管理員。對于那些使用托管組件開發復雜應用軟件的程序員來說,開發工具和編程工具庫的出現減少了花費在集成各組件和管理組件間通訊的任務上的時間,提高了生產率。對于工具軟件開發者來說(例如編譯器開發者),支持框架和一個清晰,說明詳細的虛擬機的出現可以減少花費在框架和解決互操作性上的時間,而留出更多的時間來開發工具軟件。(high-definition, carefully specified virtual machine??)最后,管理員和計算機用戶可以從使用單一的運行時框架和打包模型中獲益,并因此更好的控制計算機,所有這些都和特定的處理器和操作系統無關。

 

圖1-1     當寄宿在一個虛擬執行環境中時,各組件可以安全的進行協作。

 

CLI虛擬執行環境

       ECMA通用語言框架是一個虛擬執行環境的標準規范。它描述了一個數據驅動的架構,在其中,語言無關的數據塊以自裝配件,類型安全的軟件系統的形式被激活。驅動這個過程的數據,叫做元數據,開發工具用它來描述軟件的行為,以及軟件在內存中運行的特性。CLI執行引擎利用元數據來達到將各個來源的組件安全裝載到一起的目的。各CLI組件在嚴格控制和監督下共存,然而它們也能夠互相訪問,可以直接訪問哪些需要共享的資源。CLI是一個良好的平衡了可控性和靈活性的模型。

       ECMA,歐洲計算機廠商聯合會,是一個已有多年歷史的標準化團體。除了發布它自己的標準之外,ECMA也和ISO(國際標準化組織)有著密切的關系,基于這種關系,CLI標準已經被認可為符合ISO/IEC 23271:2003標準,之后根據一篇科技報告,而成為ISO:IEC 23272:2003標準,C#標準也被認可為符合ISO/IEC 23271:2003標準,并最終成為了ISO/IEC 23270:2003標準。

      

CLI標準可以在本書前言所提到的web站點中找到,它也包含在本書的隨書光盤中。它由5大部分組成,外加上它的開發庫的相關文檔。在CLI被標準化的時候,一種名為C#的編程語言也由于共同的努力而被標準化了。C#利用了CLI的大部分特性,并且它是一種易學的面向對象語言,因此我們選擇它來完成本書中的絕大部分例子小程序。在形式上,C#和CLI標準是各自獨立的(雖然C#標準確實引用了CLI標準),但實際上,這二者是糾纏在一起的,并且許多人認為C#是開發CLI組件的標準語言。

       CLI中的虛擬執行是在它的執行引擎的控制下發生的,執行引擎通過在運行時解釋描述組件的元數據來寄宿組件,對于那些不是基于組件的代碼也是一樣。采用這種方式運行的代碼常被稱作托管代碼,它們是使用可生成與CLI兼容的可執行程序的工具和編程語言來創建的。一個被詳細說明的事件鏈被用來從稱為裝配件的包裝工廠中裝載元數據,并將元數據轉化為適合在一個機器的處理器和操作系統上運行的可執行代碼。圖1-2演示了這個事件鏈的一個簡化版本,它將成為本書其它部分的基礎。CLI標準的第一部分也詳細描述了這個事件鏈(標準第一部分中的第8節描述了通用類型系統,第11節描述了虛擬執行系統,這些都是非常好的背景資料)。

 

       圖1-2   CLI裝載序列中的每一步都由前一階段計算獲得的元數據注釋驅動,

 

       在某些方面,CLI執行引擎很象一個操作系統,因為它也是一段為在它的控制下執行的代碼提供服務(例如裝載,隔離和調度)和管理資源(例如內存和IO)的特權代碼。還有,在CLI和操作系統中,服務都是即可以明確的被程序所請求,也可以作為執行模型環境的一部分而發揮作用。(環境服務是指在執行環境中持續運行的服務。它們非常重要,因為它們定義了一個系統的運行時計算模型的大部分內容)(Ambient services????)

       在其它方面,CLI很象傳統的編譯器,鏈接器和裝載器的工具鏈,因為它也處理內存布局,編譯和符號解析的工作。CLI標準不但極力詳細描述了托管軟件如何工作,還詳細描述了非托管軟件如何才能和托管軟件安全的共存,以便能夠無縫的共享計算資源和職責。CLI中系統和工具框架的結合使它成為創建基于組件的軟件的唯一強有力的新技術。

 

CLI標準的基礎概念

       在CLI標準和執行模型的背后是一系列的核心思想。這些核心思想是以能夠幫助開發者們組織和整理他們的代碼的抽象或具體的技術的形式包含在CLI設計理念中的。讓我們通過一系列的設計原則來思考這些技術。

  • 使用統一的類型系統來暴露所有的可編程項。
  • 將類型打包成完全自描述的可移植單元。
  • 在運行時采用能夠將各類型相互獨立的方式裝載類型,但是要能共享資源。
  • 使用考慮了版本、特定文化的差異(例如日歷或字符編碼),以及管理策略的靈活的綁定機制解決運行時類型間的依賴問題。
  • 采用能夠校驗類型安全的方式來描述類型的行為,但不要要求所有的程序都是類型安全的。
  • 采用能夠延遲到最后時刻的方式來處理針對特定處理器的任務,例如內存分配和編譯。但也要考慮較早執行這些任務的工具。
  • 在一個能夠提供運行時策略的責任和執行的特權執行引擎的控制下執行代碼。
  • 將運行時服務設計成由可擴展的元數據格式來驅動,這樣它們會容易適應新的情況和將來的變化。

在這里我們會談到一些最重要的概念,然后在整本書的過程中再次詳細的說明這些概念。

 

類型

       CLI將世界分為各種類型,程序員們也使用類型來組織他們編寫的代碼中的結構和行為。組件模型對類型的描述常常是非常簡單的:一個類型描述了包含數據的字段和屬性,還包含描述它的行為的方法和事件(所有這些都會在第3章中詳細討論)。狀態和行為既可以在實例級別中存在(在這種形式中各個組件共享結構,但各個組件不相同),也可以在類型級別中存在(在這種形式中一個獨立邊界內的所有實例共享一個單一的數據拷貝或方法調度的信息)。最后,組件模型支持標準的面向對象結構,例如繼承,基于接口的多態,以及構造器。

       對于執行引擎,程序員和其它類型來說,類型的結構采用元數據來表示,是很有用的。元數據之所以非常重要,是因為它使得來自于不同人員,地方和平臺的類型能夠和平的共存,并保持獨立。缺省情況下,CLI只在需要用到某個類型時才會加載它;鏈接只有在需要的時候才會被求值,確定地址和編譯。一個類型中所有對其它類型的引用都是符號,這就意味著它們是以名稱的形式存在,在運行時再確定地址,而不是預先計算好地址和偏移量。通過使用符號引用,可以建立成熟的版本機制,各類型將來的獨立版本可以在執行引擎的綁定邏輯中實現。

       使用經典面向對象的單繼承語法,一個類型可以從另一個類型繼承結構和行為。在子類的定義中會包含它的基類的所有方法和字段,一個子類的實例可以代替它的基類的實例。雖然類型可能只能有一個基類,但它們可以附加實現任意數目的接口。所有的類型都直接,或通過它們的父類的繼承關系,間接的繼承自基類型System.Object。

       CLI組件模型向程序員暴露了兩個更高級的結構:屬性和事件,從而擴大了字段和方法的概念。屬性允許類型暴露數據,數據的值可通過任意的代碼來獲取和設置,而不是通過直接的內存訪問。從管道的角度來看,屬性絕對是語法上的甜頭,因為它們在內部表現為方法的形式,但是從語義學的角度來看,屬性是一個類型的元數據最好的元素,能夠導致更一致的API和更好的開發工具。

       類型使用事件在執行期間通知外部的觀察者它們感興趣的事件(例如,通知數據已經可用,或者內部狀態的改變)。為了使外部觀察者能夠注冊它們感興趣的事件,CLI委托將執行一個回調所必需的信息包裝起來。當注冊一個事件回調時,程序員可以創建兩種委托之一:一種是一個封裝了指向類型的一個靜態方法的指針的靜態委托;另一種是一個將對象的引用和一個方法(對象會在這個方法上被回調)聯系起來的實例委托。委托一般以參數的形式傳遞給事件注冊方法;當類型需要觸發一個事件時,它只是對那些在類型上注冊的委托執行一下回調。

       類型使用字段來存儲數據,使用方法來表示行為,從最低限度來說,它是一種采用上述方式來組織編程模塊的分層方法。而在這之上,簡潔性,完整性,模型,屬性,事件,以及其它構造方式提供了附加的結構,可以使用這些結構來創建共享程序庫和不同于CLI的運行時服務。

       COMCLI

       長期以來追求重用的軟件設計者認為標準化的組件打包和運行時互操作性是最基礎的。就像早期使用的穿孔打卡機就是計算函數的可重用的庫所顯示的那樣。微軟公司開發組件對象模型(COM)的原因是為了實現統一打包和細粒度的互操作性這兩個目的。結果,使用“基于接口”的方法來進行二進制組件打包已經成功地被無數的軟件開發者用來部署他們的API和將分散的代碼模塊化。與CLI不同,COM是一個幾乎完全基于共享規則的組件模型,而不是一個共享的執行引擎。各COM組件共享底層的運行時基礎結構,并在每一個組件的基礎上進行功能協作。這個方法可以是非常有用的,尤其適合于那些計算機資源非常有限的環境,在這些環境中程序員必須壓榨每一個字節的性能。或者是環境中存在數量巨大的代碼需要暴露組件界面。

       只有在使用COM的共享規則后,組件之間的細粒度二進制互操作性才在Windows操作系統上的軟件運行中普遍起來。它廣泛而成功的作為一種方法來使用于應用程序為了提供可編程性而暴露內部,也用于發布API的標準方法。Windows的一些系統功能也通過COM接口來暴露,還有存在許多第三方廠商的控件是以可重用部件的方式來出售。

       然而,COM方法現在有明顯的衰落趨勢。在這個模型中,開發者要負責運行時操作的每一個細節,必須非常仔細的遵守復雜的互操作協議,以便組件能夠正確的工作。因為使協議執行正確難度很大,所以代碼不但繁雜,而且容易造成BUG。

       大部分的COM復雜性都可以通過為組件開發者提供共享底層服務來消除,就像操作系統為所有使用計算機資源的程序提供共享底層服務一樣(例如,內存垃圾回收,就是一種可以從根本上減少組件之間所必須進行的協作操作的服務)。1997年,人們提出了一種為COM服務的運行時,它能為COM編程者提供一個類模型,以及通用運行時服務,既能提高生產力(程序員不再需要重復的編寫同樣的支持機制),也能帶來更好的安全性,高效率和穩定性。這個運行時最初的名稱是組件對象運行時(COR),在共享源代碼CLI的一些函數名稱中還能找到這個名字。

       微軟公司最初開發COR的目的只是希望能為COM提供一個運行時,但是微軟公司沒有局限與此,而是決定完成一個通用的虛擬執行環境。這個努力最終成為了CLI標準。

共享類型系統和中間語言

       CLI中的類型在最低限度上是由字段和方法構成的,但是這些字段和方法自身又是如何定義的呢?CLI標準定義了一個與處理器無關的中間語言,用于描述程序,也定義了一個通用類型系統來為這種中間語言提供基本數據類型。這兩個事物一起組成了一個抽象計算模型。標準使用一些規則來修飾這個抽象模型,這些規則描述了抽象模型如何才能轉化成機器指令流和內存引用;這些轉化過程的設計十分高效,能夠識別和準確的描述許多不同編程語言的語義。中間語言,中間語言類型,以及轉換的規則,組成了一個具有普遍意義的,用于描述程序的語言無關的方法。

       CLI規范中定義的中間語言叫做通用中間語言(CIL)。它包含一個與任何現存的計算機硬件結構均無關的豐富的操作碼集合,用于驅動一個易于理解的抽象堆棧機。同樣的,通用類型系統(CTS)定義了一個包含標準的跨語言互操作性的類型的基本集合。為了充分實現這種語言無關的世界的好處,高級編譯器需要理解CIL指令集和它所匹配的數據類型的集合。如果沒有這個協議,那么不同的語言就必須選擇不同的映射方式;例如,C#中的int類型的長度有多大?它和Visual Basic中的Integer類型有什么關系?它和C++中的long類型完全相同嗎?通過將指令集和這些類型進行匹配,這些選擇將變得相當簡單,當然,關于具體應該使用哪個指令和類型的選擇是由編譯器決定的,但是,一個良好的規范的出現意味著使得這些選擇變得相當地直接和簡單。通過使用這種方法,結果代碼可以和其它語言編寫的代碼和框架進行互操作,從而導致了更加高效的重用。第3章詳細描述了CLI類型系統,而第5章描述了CIL,以及它是如何轉換成本地指令的。

 

基于類型的可移植打包單元:裝配件

       CLI利用它的類型系統和抽象計算模型,實現了這樣的理想:人們可以利用由不同人員在不同時間編寫的軟件組件,通過校驗,裝載這些組件,來使用它們一起創建應用程序。在CLI中,單獨的組件可以打包到稱為裝配件的單元中,裝配件可以在執行引擎中按需動態裝載,既可以從本地磁盤中裝載,也可以從網絡上裝載,甚至還可以在程序的控制下動態的創建。

       裝配件為CLI定義了組件模型的語義。類型不能存在于裝配件的外部。反過來說,裝配件是將類型裝載到CLI中的唯一的機制。裝配件又是由一個或多個模塊(模塊是駐留信息的打包子單元),以及一大塊名為裝配件清單的描述裝配件的元數據組成的。雖然裝配件也能由多個模塊組成,不過一般一個裝配件都只包含一個模塊。

       為了保證裝配件不會在被編譯和被裝載的時候被篡改,每一個裝配件通過一個秘鑰對和整個裝配件的一個哈希表來進行簽名,這個簽名可以被放在裝配件清單中。這個簽名被執行引擎所信任,并且可以保證裝配件不會被篡改,以及有危險的裝配件不會被裝載。如果在運行時從裝配件生成的哈希表和裝配件清單中包含的哈希表不匹配,運行時會拒絕裝載裝配件,并且在潛在的危險代碼有機會做任何事情之前拋出一個異常。

在許多方面,裝配件對于CLI的意義,就像共享庫或DLL對于操作系統的意義一樣:它們都是綁定和識別屬于同一個部分的代碼的方法。感謝CLI中建立的可靠的對元數據和符號的綁定方法,這使得每一個組件都可以在獨立于它的鄰居的情況下被裝載,翻譯以及執行,即使它們之間相互依賴,也不會互相干擾。這是至關緊要的,因為平臺,應用程序,庫,以及硬件都會隨著時間的變化而改變。基于組件建立的解決方案應該在這些組件變化時繼續正常工作。我們將在第3章和第4章討論裝配件。

 

組件隔離:應用程序域和遠程調用

       以使組件能夠一起工作,并保護組件不受其它組件中的惡意代碼或bug危害的方式裝載各組件的能力,與在組件內部將代碼組織在一起的能力一樣重要。操作系統常常通過建立保護地址空間,以及提供連接保護地址空間和其它地址空間的通訊機制的方法來獲得獨立性;地址空間提供保護邊界,而通訊機制為協作提供通道。在CLI中有相似的隔離執行代碼的概念,它由應用程序域和對遠程調用的支持組成。

       程序集總是在一個應用程序域的上下文中被裝載的,因此類型就被它們的應用程序域限制了范圍,例如,程序集中定義的靜態變量在應用程序域中分配空間和存儲。如果同一個程序集在三個不同的域中被加載,會為這個程序集中類型的數據分配三個不同的拷貝。在本質上,應用程序域是”輕量級的地址空間”,對于在各個應用程序域之間傳遞數據,CLI執行和操作系統在不同的地址空間之間傳遞數據所執行的相同的限制。希望跨越域邊界進行通訊的類型,必須使用特殊的通訊通道,并按照特定的規則來進行操作。

       被稱為遠程調用的技術,可以用來在不同的物理計算機(計算機上可能運行不同的操作系統,具有不同的處理器)上運行的應用程序域之間進行通訊。就像經常一樣,遠程調用機制常常用于分隔位于同一個機器中同一個進程的域中的各組件。希望參與到遠程調用中的組件要么必須是可序列化的,這樣它們就能在域之間傳遞,要么必須繼承自System.MarshalByRefObject類型,這樣它們可以使用負担傳遞工作的代理對象進行通訊。在第4章中會講述應用程序域,遠程調用,以及裝載的詳細內容。

 

為靈活的版本裝載服務的命名規則

       因為所有的類型和類型的代碼都存在于程序集中,因此必須要有一組定義明確的描述當執行引擎需要程序集中的類型時如何查找和使用程序集的規則。程序集的名稱由一個元素的標準集合組成,包括程序集的基礎名稱,一個版本號,一個地區文化(為全球化服務),以及一個代表發布這個程序集的發布者的公鑰的哈希表。組合名稱保證了由各程序集創建的軟件會優雅的適應版本的變化。編譯時,每個程序集都會包含在編譯時它所依賴的其它程序集的組合名稱的引用,并記住每個這些程序集的版本信息。這樣,當裝載時,程序集會非常明確的要求它所依賴的程序集的某個特定(或語義一致)的版本。用來滿足這些需求的綁定策略可以通過對配置的設置來改變,但是綁定策略是不可能被忽略的。

       通常可以在下列兩個地方之一找到程序集:在一個被成為全局程序集緩存(GAC),作用于整個機器的緩存,或者一個基于URL的查找路徑。GAC是每個機器上的程序集的有效的數據庫,每一個程序集都由它的四部分名稱來唯一的識別。GAC可以是一個文件系統的目錄,但也可以不是,CLI的運行必須能夠在GAC中存放同一個程序集的不同版本,并且能夠跟蹤這些不同版本。查找路徑基本上是一個URL(通常是文件系統的目錄)的集合,當需要裝載一個程序集時,會搜索這些路徑。第4章會詳細講述裝載的過程,以及裝載是如何實現的。

 

JIT編譯和類型安全

       CLI描述的執行模型意味著編譯高級類型的工作應該和將這些類型描述轉換為基于特定處理器的代碼和內存結構的工作分離開來。這種分離為計算模型帶來了很多重要的優勢,例如能夠在出現了新的操作系統和處理器后,很容易的調整代碼來適應,以及能夠獨立的定義來自各個不同來源的組件的版本。這種分離也帶來了新的挑戰。例如,因為所有的類型都由CIL和CTS來描述,所以,所有的類型都必須在它們能夠被使用前翻譯成機器碼和內存結構;本質上,整個應用程序總是必須在能夠運行前被重新編譯,這可能會是一個非常昂貴的方法。

       為了分次消耗將CIL轉化成機器指令的代價(即包括裝載所花費的時間,也包括所需要的內存),基于CLI的應用程序的類型的裝載方式很獨特,直到軟件的運行需要它們時才會被裝載,一個類型一旦被裝載,它的各個方法會直到軟件的執行需要它們時才被翻譯。這種延期的裝載和代碼生成被稱作即時(JIT)編譯。CLI并不是一定要求最后時刻的JIT編譯,但是延遲裝載和編譯總是會發生在一個應用程序的生命周期的某些點上,來將CIL轉化為機器碼。可以想象的一種情況就是,軟件的安裝程序就可以執行編譯。第5章將講述為了符合CLI規范JIT編譯需要實現的方法。

       之所以要在CLI執行模型中要建立JIT編譯的最重要的原因并不明顯。在執行引擎自己的裝載器和編譯器的控制下進行從抽象組件到可運行的機器碼的轉換,是使得執行引擎在運行時保持控制,以及高效的運行代碼的原因,即使是在c++編寫的代碼和一個托管語言編寫的代碼之間進行來回調用,執行引擎也能很好的工作。傳統的編譯,鏈接,和裝載的過程,在CLI中依然存在,但是,就像我們看到的一樣,每個工具鏈上的元素必須大量使用復雜的技術(例如緩存),因為延遲的使用導致了較高的運行時代價。這些較高的代價是值得忍受的,因為延遲也使得能夠對運行的組件的行為進行全面控制。因為CLI的執行是基于對類型的逐漸加載,以及所有的類型都是使用平臺無關的中間語言定義的,所以CLI執行引擎在它運行的過程中是在不斷的編譯和添加新的行為的。因為CIL被設計成可校驗的,和類型安全的,編譯成機器碼的過程是在具有特權的執行引擎的控制下執行的,所以可以在允許一個新類型運行之前校驗類型安全性。安全策略也能夠在CIL被轉換成機器碼時檢查和應用,這就意味著安全檢查可以直接插入到代碼中,在方法被執行時以系統的名義來執行。簡而言之,通過使用延遲加載,校驗和直到運行時才對組件進行編譯這些技術,CLI可以加強可靠的托管執行。

 

托管執行

       類型裝載是導致CLI的工具鏈在運行時十分忙碌的關鍵所在。看看裝載進程的部分工作,CLI需要編譯,裝配,鏈接,驗證可執行文件的格式和程序的元數據,校驗類型安全,最終甚至是管理運行時的資源,例如內存和處理器周期,在它的控制下代表組件運行。將所有這些階段聯系在一起的任務導致CLI包括了名稱綁定,內存分布,編譯和打補丁,分隔,同步化,以及符號解析的基礎結構。因為總是希望這些元素的執行延遲到盡可能最后的時刻,所以執行引擎可以享受對裝載和執行策略,內存組織,生成代碼,以及代碼和底層平臺、操作系統的交互方式的高度可靠的控制。

       延遲編譯,鏈接,和裝載為跨越目標平臺和跨越版本變化提供了更好的可移植性。通過延遲排序和排列的決定,延遲地址和偏移量的計算,延遲處理器指令的選擇,延遲調用的轉化,當然,還有延遲鏈接到平臺的自身服務上,程序集可以變得更加的向前兼容。由定義良好的元數據和策略驅動的延遲過程,是非常有活力的。

       翻譯元數據的執行引擎是可信賴的系統代碼,因此,通過后期裝載,安全性和穩定性也得到了增強。每個程序集都包含一個與它相關的許可權限的集合,定義了允許這個程序集執行什么操作。當這個程序集中的代碼企圖執行一個敏感的操作時(例如企圖讀或寫一個文件,或者企圖使用網絡),CLI會查看調用堆棧,檢查它來判斷是否當前范圍內的所有代碼都有合適的權限――如果堆棧上的代碼沒有合適的權限,操作就會被拒絕,一個異常會被拋出(異常是另一個能夠在組件之間進行簡單的交互的機制;CLI的設計不僅支持在執行引擎內廣泛范圍內的異常語義,也緊密的集成了來自底層平臺的異常信號)。第6章和第7章將詳細描述托管執行。

 

      使用元數據來實現數據驅動的可擴展性

       CLI組件是自描述的。一個CLI組件包含了它內部的每個成員的定義,而這些定義信息在運行時中受保證的有效性是幫助虛擬執行具有高度可適應性的一個因素。每個類型,每個方法,每個字段,每個單一方法調用上的每個單一的參數必須是被充分描述的,而這個描述信息必須存儲在程序集內部。因為CLI將各種鏈接操作延遲到必需執行的最后時刻,那些希望通過使用元數據來操作組件和創建新組件的工具和程序獲得了極大的靈活性。建立在CLI之上的代碼可以使用與CLI所使用的相同類型的技巧,對于工具軟件和運行時服務來說,這簡直就是天降橫財。

       為了獲得類型的信息,CLI程序員可以使用執行引擎提供的反射服務。反射提供了在運行時檢查編譯時信息的能力。例如,對于一個托管組件,開發者可以獲得類型的結構信息,包括它的構造器,字段,方法,屬性,事件,接口,以及繼承關系。可能更重要的是,開發者可以使用名為定制屬性的功能將自己的元數據添加到組件的描述信息中。

      通過反射不僅可以獲得編譯時信息,而且可以操作運行中的實例。開發者可以使用反射來進入類型內部,獲得它們的結構信息,并基于這個結構信息操作類型的內部信息。對于方法來說,是一樣的;開發者可以在運行時動態的調用方法。這種元數據驅動風格的編程能力,以及如何才能實現它的問題,我們會在第3章接觸到,并且在第8章會詳細的進行講述。

 

CLI的共享源碼實現:Roter

       2001年夏天,雷德蒙公司的一小組開發人員宣布了一個微軟公司少有的開發計劃:一個免費使用的軟件產品,包括可修改,可重新發布的源代碼。這個叫做SharedSource CLI(SSCLI,人們也親切的稱呼它的代碼名稱,“Rotor”)的產品,包括一個實現了全部功能的CLI執行引擎,一個C#編譯器,基礎的編程庫,和許多相關的開發工具。這個產品在商業的.Net框架周邊靜悄悄的發展,代表了微軟的開發者工具策略的一個重要方面,實際上,SSCLI要實現三個目的:檢驗CLI規范的可移植性,幫助人們學習和理解微軟的商業CLR產品,以及長期促進學院對于CLI的興趣。最重要的是,SSCLI要符合ECMA標準,以便任何希望了解和實現這個標準的人可以將SSCLI作為一個指南。

       雖然名義上SSCLI是這本書的主題,但實際上CLI標準才是本書的核心。SSCLI幫助我們闡明CLI是怎樣一件如此令人感興趣的工作,以及為什么。這個產品本身包含了巨大數量的代碼,因此,它可以為在開發工具或系統設計領域工作的研究者和試驗者提供重要的幫助,對于那些教授計算機科學的老師也是一樣。本書嘗試通過提供遠多于CLI理論的知識,來輔助分析和解釋基本代碼的規則,以便為這些人提供一個針對這些代碼的高級指南。CLI標準會變得越來越重要,而完全理解它的最好方法,就是瀏覽,創建,觀察和分析一個運行中的實例。

       而Rotor向我們演示了創建一個可移植,編程語言無關的CLI規范的實際版本的一種方法,當然,它不是唯一的方法。在寫作本書的時候,就存在著其它的CLI規范的實現方式,包括微軟公司的兩種(商業.Net框架和名為“精簡框架”的運行在微型設備上的版本),以及第三方廠商的兩種開源的實現,一種來自Ximian公司(名為Mono),另一種來自DotGNU項目(名為Portable.NET――可移植.NET)。Rotor自身提供了大大超過標準的各種附加開發工具和功能。為了說明在這個產品中包含了那些內容,圖1-3使用一張圖來說明微軟商業產品(.NET CLR),CLI和C#標準,以及Roter之間的區別。

       如圖所示,SSCLI是CLI標準的一個超集,而微軟的商業產品又是SSCLI的一個超集。

       Rotor是一個由許多人歷經多年開發的巨大數量的代碼的集合,因此,它是一個復雜和

文體上的變量(stylistically variable???),就軟件的大小來說,它可以與那些最大的著名開源產品相比,例如Xfree86,Mozilla,以及OpenOffice。和這些產品一樣,如果希望開始了解這些巨大數量的代碼,可能會令人望而卻步。本書將幫助您較為容易的完成這個任務,讓我們從這個產品自身的簡短歷程開始吧。

       SSCLI是使用C++和C#,以及少數處理特定處理器細節的匯編組合編碼開發的,這個產品通過一個三階段的過程開發完成。首先,一個基于特定平臺的C++編譯器被用來開發一個平臺適配層(PAL),這是一個用于將操作系統的各API的差別隱藏在一個單一抽象編程集合后面的庫。之后,一系列創建SSCLI所必需的開發工具(包括C#編譯器)在PAL庫上被創建和鏈接。最后,使用這些工具和PAL庫來開發產品的其它部分。

 

       表1-1列出了一些在瀏覽SSCLI源碼時有用的子目錄,SSCLI源碼位于本書的隨書光盤中(也可以從http://msdn.microsoft.com/net/sscli處下載)

表1-1。產品中重要的子目錄,以及各目錄的內容。

子目錄                                     內容

/build                                  包括創建好的可執行文件和庫文件

/clr/src                               許多包含核心內容子目錄的主目錄

/bcl                                           基礎類庫,用C#編寫

/csharp                               一個C#編譯器,用C#編寫

/classlibnative                      C++編寫的開發庫

/debug                                實現托管調試

/dlls/mscorsn                             強名稱加密代碼

/fjit                                     SSCLI的JIT編譯器

/fusion                                定位版本文件的代碼

/ilasm                                 一個CIL匯編器

/ildasm                               一個CIL反匯編器

/inc                                           共享的包含文件

/md                                    元數據工具

/toolbox/caspol                    caspol安全工具的源碼

/tools                                  許多工具程序的主目錄

/clix                                   SSCLI托管執行的啟動程序

/gac                                   gacutil緩存工具的源碼

/peverify                             peverify CIL校驗工具

/sos                                    SOS調試外掛庫

/strongname                        序列號代碼簽名工具

/vm                                    CLI執行引擎

/docs                                  文檔

/fx/src                                附加的托管庫的主目錄

/net/system/net                    網絡功能庫

/regex/system/text                      正則表達式庫

/jscript                                一個可以編譯成CIL代碼的完整Jscript編譯器,使用 C#編寫 (一個托管的托管代碼編譯器!)

/managedlibraries/remoting   對bcl目錄中的基礎類庫附加的遠程調用支持

/pal                                           PAL在多個特定操作系統上的實現

/palrt                                  支持SSCLI運行的低級API ,但不針對某個特定的操作系統

/samples                                    使用CLI的例子程序

/tests                                  擴展測試和測試基礎結構

/tools                                  用來創建SSCLI產品的工具

 

       這些子目錄可以被分成四個概念明顯不同的部分,如下所示:

  • CLI執行引擎
  • 包裝和擴展了執行引擎的組件框架
  • 用于從一個操作系統移植到另一個操作系統的可移植層(PAL)
  • 工具,測試,編譯器,文檔,以及為和托管代碼一起工作服務的工具。

 

讓我們依次鉆研這些部分,關注哪里能夠找到它們的實現方法。

 

CLI執行引擎

執行引擎是CLI的核心。它包括組件模型,以及運行時服務,例如異常處理,自動的堆和棧的管理。在許多方面,它簡直是一個大巫師(???);它就是當我們談論“運行時”和“虛擬執行環境”時所指的代碼。JIT編譯,內存管理,程序集和類的裝載,類型解析,元數據的解析,堆棧遍歷,以及其它的基礎機制都是在這里實現的。在sscli/clr/src目錄和四個子目錄vm,fjit,md,和fusion中可以找到包含了大部分執行引擎功能的代碼。

       如圖1-4所示,執行引擎由一系列可裝載的動態庫組成,而不是一個單一的可執行文件。Clix程序啟動器(或者任何希望使用執行引擎服務的程序)裝載主要的共享庫,sscoree,來在進程中創建一個CLI的實例,然后為這個實例提供一個需要執行的啟動程序集。因此,在執行引擎中是沒有main函數的,它是以打包的形式來被其它程序寄宿的。執行引擎依賴大量的其它共享庫,包括一些不封閉的庫,因為它們是需要被替代代的,例如crypto代碼必須裝載和創建位于mscorsn中的簽名匯編,以及一些會在許多不同的地方潛在的發揮作用的庫,例如PAL,可以在rotor_pal目錄和rotor_palrt目錄中找到它。最后,那些不常需要的代碼也被打包在分別裝載的庫中,例如mscordbc,這個庫實現調試器功能的支持。

 

CLI中的開發庫

       CLI的共享框架不僅包含標準的,底層的功能,例如元數據,通用中間語言,以及通用類型系統,也包括高級的,面向生產的類庫。表1-2通過列舉有用途的各方面簡單的總結了這些庫的內容

表1-2. CLI標準庫所包含的高級元素。

類別                                        功能

生產庫                                    文本格式化,正則表達式,集合,時間,日期,文件和網絡IO,配置,診斷,全球化,獨立存儲,XML

執行引擎庫                            分隔的域,異步調用,堆棧遍歷,堆棧跟蹤,垃圾回收,句柄,環境,線程,異常,基于監控的同步,安全性,校驗,反射,序列化,與機器碼的互操作

類型相關的庫                         簡單類型,值類型,委托,字符串,數組

擴展的數值庫                         Decimal數值, 雙精度和單精度浮點數,數學計算

編程語言支持            編譯器服務,定制元數據屬性,資源回收

 

       這些庫提供了一種使用底層操作系統功能的接口,不過是通過一種與CLI的服務和規范相適應的方式來使用的,這樣通過它們的一致性和高質量,就提高了程序員的生產力。

       這些API也為其它一些不太重要的方面服務:它們通過暴露編程服務和規則促進了組件集成,通過編程服務和規則的使用促進了組件的衛生學。將組件開發者必須完成的薄記工作降低到最小的服務,或將復雜的組件間管理協議的需要降到最小的那些服務,使組件之間的繼承更加平滑和安全(并且減少了需要編寫的代碼)。一個組件越不依賴于其它組件,這個組件必須為其它組件作的工作就越少,這個應用程序因此也就越可能遠離BUG,易讀和健壯。我們意識到基于組件的軟件真正的含義,必需將組件創建為依賴一個以這些原則為核心設計的環境中的托管執行。

       有人可能會將CLI庫想象成C運行時庫的另一個時髦版本。其實CLI庫并沒有試圖為所有的程序員提供所有的功能;而是提供一組幾乎每一個程序員都會用到的核心組件。因為sscli/clr/src/bcl目錄下的基礎庫,是任何CLI實現所必須具有的部分,所以它們形成了可移植的應用程序實現的基礎。在sscli/fx,sscli/clr/src/classlibnative,和sscli/managedlibraries目錄下的附加庫,要么是可選的標準庫,要么就是sscli中的部分。目前,Sscli中所有的庫都可以在微軟的商業.Net框架中找到。

       如果瀏覽各個程序庫,就會發現,除了sscli/docs目錄下有專門說明Rotor產品和它的功能的文檔之外,還有一個單獨可下載的文件(也包含在本書的隨書光盤中),其中包含了類庫的文檔。該文檔來自微軟的.Net框架SDK中使用的文檔,不過它已經經過了修改,并轉化成簡單的HTML文件了。

 

平臺適配器層

       PAL是一個很有趣的軟件,雖然人們并不太關注它,但它十分有用。當然,任何包含大量代碼的適配器或驅動層都有一個顯著特點,就是可以運行在許多操作系統平臺上,PAL     的首要目的是將實現從各種操作系統的細節中分離開來。Sscli中的選擇十分明確:因為它是從特定的Win32代碼啟動的,因此PAL被設計為表達Win32的API的一個子集(可以在sscli/pal/rotor_pal.h中找到)。這個實現絕對不會完整,因為它只需要提供CLI實際會使用的那些調用。不要企圖將PAL用作一個通用的win32模擬層,因為它是不完整的。

       當然,將Rotor移植到新平臺的工作應該從PAL開始,因為用來創建Rotor的工具依賴于基于它們的操作系統和資源的PAL。可以查看sscli/pal/unix目錄,來了解包含了什么內容。這里包含了大量必須完成的重要工作:提供通用異常處理機制,通用線程,共享的句柄管理,IO,同步,調試等等。特定的主機進程,例如web服務器或者數據庫,很可能會需要它們自己相似的運行時。PAL的語義需要考慮這些問題。因為上面這些情況,以及PAL定義了如何使用操作系統資源的方法,所以對于很多人來說,理解各種各樣的PAL的實現會十分重要和有用。

       除了PAL之外,名為sscli/palrt/src目錄下包含了一個Win32 API的實現庫,SSCLI需要這個庫,但這個庫的執行并不依賴于操作系統。這個庫也包括了少量PAL的API。它確實是一個各種功能的大雜燴,不過有趣的是,其中包含了decimal算法,一些微軟的COM組件模型的簡單實現,數組處理,內存管理,以及各種其它有用的函數。

       PAL最有趣的方法一定就是執行引擎控制了。SSCLI的設計要求能夠在原始進程中和機器碼進行協作。這意味著需要捕捉許多操作系統調用,以便給執行引擎提供機會,來維護使用運行時系統所需的薄記信息,例如垃圾回收或者安全系統。這是PAL層一個關鍵的用處,SSCLI實現是以PAL所表達的抽象方式創建的,如果沒有PAL,它就不能保持獨立,安全和控制。例如,線程和異常處理都是在PAL中實現的,二者對于運行時的執行引擎都很關鍵,因為它使用異常幀來跟蹤托管模塊,使用線程堆棧來存儲包括自身許多服務狀態的擴散結構。PAL這方面的細節會在第6章中詳細討論,而PAL的自身設計將是第9章的主題。

 

工具,編譯器,測試,文檔和其它有用的東西

       Rotor中相當一部分代碼是由用于創建,測試,和使用它的CLI實現的框架結構組成的。我們剛討論過的PAL層,就是這樣的代碼。在這個產品的各個地方都可以發現各種附加的開發工具,有用的東西,以及測試程序。這些工具屬于管理開發和創建產品的廣泛范圍。

       就管理開發來說,Rotor產品中的許多工具會讓任何有過使用微軟.Net框架經驗的程序員感到很熟悉,因為這兩個實現共享它們基礎的功能集,例如鏈接器,匯編器,以及反匯編器。sscli/clr/src, sscli/clr/src/tools, and sscli/clr/src/toolbox目錄包括了這些功能的子目錄,以及只用于使用SSCLI來開發和運行托管代碼的工具,例如clix.exe。程序員應該查看sscli/docs目錄下的文檔來了解是否某個特性是由Rotor版本的工具和它的.Net框架的副本所共享的,不是所有的特性都是共享的。

       用于自我生成Rotor的創建系統位于sscli/tools目錄下。這些工具在PAL上創建,用于跟蹤依賴關系,驅動創建進程,匯編庫和執行文件,一旦被創建,裝配庫和可執行文件將被放到sscli/build目錄下。Rotor中的依賴令人費解,因為它是十分巨大的項目,因此這些工具十分重要。可以查看sscli/docs/buildtools目錄來了解如何使用它們,以及當開發者在修改代碼時如何與它們交互。

一旦SSCLI被創建,就可以使用sscli/tests目錄下的測試程序來測試它了。特別要說明的是位于sscli/tests/palsuite目錄下的PAL測試程序,可以用它來校驗新的PAL實現,或者將它校驗一個現存的PAL中的改變,在sscli/tests/bvt目錄下的開發者創建校驗測試(BVT),可以用來檢查在執行引擎中完成的工作。還有為其它領域(例如基礎類庫)服務的測試,例如:基礎類庫。大部分測試,包括BVTs,使用了sscli/tests/harness目錄下的測試工具,文檔位于sscli/docs/testing_overview.html中。

Rotor相關的文檔和技術資料位于sscli/docs目錄下,這個目錄包含了對于瀏覽源碼,修改代碼,以及理解CLI結構和創建SSCLI時特定實現的選擇很有用的信息。該目錄下也包括一個PAL的詳細說明,對于需要將Rotor移植到新平臺上的人會非常有用。花費一些時間來瀏覽這個目錄是很值得的。

 

本書范圍

       本書關注的重點在于在SSCLI中是如何實現CLI組件模型和它底層的執行引擎的。也簡潔的講述了將這樣一個機制放置在操作系統上的需要,以及通用移植的事項。而關于編譯器,語言和框架,以及非面向組件的使用CLI的方法的論述就比較少了,不過這些內容可以在眾多的講述.Net框架和CLI的書中找到。書的大小和范圍,以及作者希望看到本書出版的事實,決定了本書最關注的內容。

       還需要聲明一點:本書中大量來自sscli源碼的C++例子程序大部分都被清除了,用進程中的偽代碼來代替。這樣做的目的是移去那些會混淆真正代碼的丑陋的宏定義,錯誤處理以及斷言,以使代碼更加可讀。如果您計劃添加和修改sscli的源碼,你應該意識到那些必須保留的變量,以及采用與SSCLI開發者相同的編程風格和錯誤處理方法。附錄D對這些方面做了簡短的描述。

 

摘要

       CLI是第一個從頭設計,可以被許多不同的編程語言共享的虛擬執行環境。平臺提供者,框架創建者,以及程序員不必被迫關注所有的語言特性,而只需利用創建基于組件的計算工作的優勢,例如,異常,垃圾回收,反射,代碼訪問安全,數據驅動的可擴展性。使用CLI,可以很容易的將已經存在的代碼合并到基于組件的編程工作中,這就導致了可增長的互操作性和共享的框架。

       CLI用于打包,描述和部署組件的標準格式與操作系統和實現語言均無關。這是很重要的,因為這種格式形成了CLI數據驅動的結構的基礎。數據驅動機制提高了程序員的生產力,因為它使得不同的程序,庫,和工具能夠無縫的進行交互,并隨著時間的推移而發展。一個數據驅動的組件模型就如同今天的技術所認為的那樣,是將來的保證。

       虛擬指令集和類型系統描繪了CLI的虛擬執行模型,展示了圣杯誘人的光芒:軟件可以在任何地方運行,CLI設計當然預見到了一個各種不同的實現和不同版本的標準可以在許多平臺上并存運行的世界。然而在這個世界中,利用CLI對互操作性的優秀支持,每一個實現都可能暴露唯一的框架,服務,功能和工具,或者增大了基礎容量的語言特性。這將導致類似C語言的開發,在這種開發中,很少會有重要的應用程序是獨立的在標準運行時上創建的。取而代之的是,應用程序會明智的將平臺相關的各庫的標準功能,和為跨平臺使用特別設計的庫組合起來。許多重要的CLI程序會將平臺相關的組件和為跨平臺使用特別設計的第三方組件組合起來形成標準的組件。

       人們開發出CLI的語言無關特性,數據驅動的結構,以及它的虛擬執行模型,以創建一個組件之間能夠高效的協作,而不需犧牲安全性和封裝性的舞臺。元數據伸展的鏈創建了一個能夠意識到組件的行為,在運行它們之間能夠將安全錯誤注入它們的代碼中的環境。CLI執行模型中的每一個階段都包括了從前一階段接受數據,轉換和增強它,然后將它傳遞給下一個階段的工作。本書描述了這些階段的完整鏈,以及它們基于實現的執行引擎,從最初的啟動序列開始,直到最后它的托管資源被釋放為止。


daidaoke2001的專欄 2014-07-12 16:10:30

[新一篇] 大河之旁必有大城

[舊一篇] 過好精力充沛的每一天——讀《精力管理》有感
回頂部
寫評論


評論集


暫無評論。

稱謂:

内容:

驗證:


返回列表