OSKit作為一套開發操作系統的工具,其最大的特色是在操作系統的整體設計中采用了COM的思想,將操作系統中各個不同功能的部分設計成獨立的COM模塊,使得操作系統的開發者能很方便地按照COM的規范開發出符合自己需要的功能模塊,將OSKIT中的相應模塊替換,但卻能繼續使用OSKIT中的其它模塊。而且對每個COM模塊內部的接口也可以進行改寫,添加其未實現的功能,刪除不必要的功能,最終實現真正滿足自己要求的操作系統。這些OSKIT特性的實現無一不是有賴於COM的機制。因此,本章對COM中的基本概念和COM規范進行簡單的介紹。
2.1 COM的基本概念
由於COM是由組件技術發展而來,因此在介紹COM機制之前有必要先對組件進行必要的說明,然後再對COM的基本概念進行說明。
2.1.1 組件技術
在計算機軟件發展的早期,一個應用系統往往是一個單獨的應用程序。應用越復雜,程序就越龐大,系統開發的難度也就越大。而且,一旦系統的某個版本完成後,在下個版本出來之前,應用程序不會再有所改變。而對於龐大的程序來講,更新版本的周期很長,在兩個版本之間,如果由於操作系統發生了變化,或者硬件平台有了變化,則應用系統就很難適應這種變化。所以這類單體應用程序已經不能滿足計算機軟硬件的發展需要。
從軟件模型角度來考慮,一個很自然的想法就是把一個龐大的應用程序分成多個模塊,每個模塊保持一定的功能獨立性,在協同工作時,通過相互之間的接口完成實際的任務。我們把每一個這樣的模塊稱為組件,一個設計良好的應用系統往往被切分成一些組件,這些組件可以單獨開發,單獨編譯,甚至單獨調試和測試。當所有的組件開發完成後,把它們組合在一起就得到了完整的應用系統。當系統的軟硬件環境發生變化或者用戶的需求有所更改時,並不需要對所有的組件進行修改,而只需對受影響的組件進行修改,然後重新組合得到新的升級軟件。這種組件化程序設計技術,不同於傳統的結構化程序設計技術,也不同於現在被廣泛采用的面向對象程序設計技術。可以說,組件化程序設計位於這兩者之上,它更注重於系統的全局,要求從系統的全方位進行考察。
2.1.2 什麼是COM
COM,即組件對象模型,是一種以組件為發布單元的對象模型,這種模型使各軟件組件可以用一種統一的方式進行交互。COM既提供了組件之間進行交互的規范,也提供了實現交互的環境,因為組件對象之間交互的規范不依賴於任何特定的語言,所以COM也可以是不同語言協作開發的一種標准。COM不僅僅提供了組件之間的接口標准,它還引入了面向對象的思想。在COM規范中,把對象稱為COM對象。組件模型為COM對象提供了活動的空間,COM對象以接口的方式提供服務,下圖表明了COM組件、COM對象和COM接口三者之間的關系。
一個組件程序可以包含多個COM對象,而且每個COM對象可以實現多個接口。當另外的組件或普通程序(即組件的客戶程序)調用組件的功能時,它首先創建一個COM對象或者通過該對象所實現的COM接口調用它所提供的服務。當所有的服務結束後,如果客戶程序不再需要該COM對象,那麼應該釋放掉對象所占有的資源,包括對象自身。
2.1.3 COM結構
前面已經提到過,COM為組件和應用程序之間進行通信提供了統一的標准,它為組件程序提供了一個面向對象的活動環境。
COM標准包括規范和實現兩大部分,規范部分定義了組件和組件之間通信的機制,這些規范不依賴於任何特定的語言和操作系統,只要按照該規范,任何語言都可以使用。在這裡主要討論COM規范,至於其在OSKIT中的實現,在後面的章節中討論。
COM主要是由對象和接口兩部分組成。對象是某個類(class)的一個實例;而類則是一組相關的數據和功能組合在一起的一個定義。使用對象的應用(或另一個對象)稱為客戶,有時也稱為對象的客戶。接口是一組邏輯上相關的函數集合,其函數稱為接口成員函數。按照習慣,接口名常以"I"為前綴,例如"IUNKNOWN".對象通過接口和成員函數為客戶提供各種形式的服務。
2.1.4客戶/服務器模型
可以很容易看出,對象和客戶之間的相互作用是建立在客戶/服務期模型基礎上的,客戶/服務器模型的一個很大的優點是穩定性好,而穩定性正是COM模型的目標,尤其對於跨進程的程序通信,穩定性更會帶來性能上的高可靠性。
客戶/服務器模型是一種發展比較成功的軟件模型,因為這種模型有以下一些優勢:
穩定性好、可靠性高。客戶/服務器模型簡化了應用,把任務進行分離,客戶和服務器各司其職,共同完成任務。
軟件的可擴展性好。一個服務器進程可以為多個客戶提供服務,客戶也可以連接到不同的服務器上,這種模型的連接非常靈活。
可以很容易看出,對象和客戶之間的相互作用是建立在客戶/服務器模型的基礎上的,客戶/服務器模型的一個很大優點是穩定性好,而穩定性正是COM模型的目標,尤其對於跨進程的程序通信,穩定性更會帶來性能上的高可靠性。
然而,COM不僅僅是一種簡單的客戶/服務器模型,有時客戶也可以反過來提供服務,或者服務方本身也需要其他對象的一些功能,在這些情況下,一個對象可能既是服務器也是客戶。COM能夠有效地處理這種情況。
2.2 COM對象
在COM規范中,並沒有對COM對象進行嚴格的定義,但COM提供的是面向對象的組件模型,COM組件提供給客戶的是以對象形式封裝起來的實體。客戶程序與COM組件程序進行交互的實體是COM對象,它並不關心組件模塊的名稱和位置(即位置透明性),但它必須知道自己在與那個COM對象進行交互。
類似於C++語言中類(class)的概念,COM對象也包括屬性(也包括狀態)和方法(也稱為操作),對象的狀態反映了對象的存在,也是區別於其他對象的要素;而對象所提供的方法就是對象提供給外界的接口,客戶必須通過接口才能獲得對象的服務。對於COM對象來說,接口是它與外界交互的唯一途徑,因此,封裝特性是COM對象的基本特征。
COM對象可以由多種語言來實現,例如C++,JAVA,C(正如在OSKIT中)。如果用C++來實現COM對象,則很自然可以用類(class)來定義COM對象,類的每個實例代表一個COM對象,類的數據成員可用於反映對象的屬性,而接口自然可以定義成類的成員函數。但在非面向對象語言,例如C語言中,對象的概念可能變成一個邏輯概念,如果兩個對象同時存在,則在接口實現中必須明確知道所進行的操作是針對哪個對象的,這個過程可由COM接口的定義來保證。
2.2.1 COM對象的標識-CLSID
前面已經說過,COM組件的位置對客戶來說是透明的,因為客戶並不直接去訪問COM組件,客戶程序通過一個全局標識符進行對象的創建和初始化工作。如果從標識符的可讀性來考慮,使用字符串是最簡單的方法,但這樣做會增加名字沖突的可能性,這樣組件的唯一性就很難保證,所以不能采用這種方法。而如果按照TCP/IP網絡協議標識計算機采用的IP地址標識方法,那麼每個組件對象都應該分配一個整數,該整數唯一標識了組件對象。問題在於,為了保證唯一性,必須有一個專門的權威機構為COM組件分配整數標識符,對於COM組件的開發和使用,顯然不能滿足實際需要。
使用定長位數的整數來標識組件對象是合理的,為了在沒有中心機構管理的情況下保證唯一性,COM規范采用了128位全局唯一標識符GUID,這是一個隨機數,並不需要專門機構進行分配和管理。因為GUID是隨機數,所以並不絕對保證唯一性,但發生標識符相重的可能性非常小。從理論上講,如果一台機器每秒產生10 000 000個GUID,則可以保證(概率意義上),3240年不重復。
下面是一個GUID的例子:
{54BF6567--1007--11D1--B0AA--444553540000}。
在C/C++語言中可以用這樣的結構來描述:
typedef struct_GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
Byte Data4[8];
} GUID;
於是前面的GUID例子可以定義為
extern "c" const GUID CLSID_MYCLASID =
{ 0x54bf6567,0x1007,0x11d1,{0xb0,0xaa,0x44,0x45,0x53,0x54,0x00,0x00}};
CLSID是用來標識COM對象的GUID,因此,CLSID在結構定義上GUID一致。GUID並不是專門用來定義COM對象標識符的,它也用於定義其它實體的標識符,比如接口標識符。
2.3 COM接口
因為COM對象的客戶與對象的服務之間通過接口進行交互,所以組件之間接口的定義至關重要。COM規范的核心內容是關於接口的定義。
2.3.1 接口的定義和標識
從技術上講,接口是包含了一組函數的數據結構,通過這組數據結構,客戶代碼可以調用組件對象的功能。接口定義了一組成員函數,這組成員函數是組件對象暴露出來的所有信息,客戶程序利用這些函數獲得組件對象的服務。
客戶程序用一個指向接口數據結構的指針來調用接口成員函數。如圖所示,接口指針實際上又指向另一個指針,這第二個指針指向一組函數,稱為接口函數表,接口函數表中每一項為4個字節長的函數指針,每個函數指針與對象的具體實現連接起來。通過這種方式,客戶只要獲得了接口指針,就可以調用到對象的實際功能。
通常,接口函數表被稱為虛函數表(virtual function table,簡稱vtable),在OSKIT中稱為函數動態派遣表。對於一個接口來說,它的虛函數表是確定的,因此接口的成員函數個數是不變的,而且成員函數的先後順序也是不變的;對於每個成員函數來說,其參數和返回值也是確定的。在一個接口的定義中,所有這些信息都必須在二進制一級確定,不管什麼語言,只要能支持這樣的內存結構描述,就可以定義接口。此外,成員函數除了參數類型是確定的,還要使用同樣的調用習慣。客戶程序在調用成員函數之前,必須先把參數壓入棧中,然後再進入成員函數中,成員函數依次把參數從棧中取出來,在函數返回之前或返回後,必須恢復棧的當前位置,才能保證函數正常運行。由於OSKIT是用C語言來實現的,因此客戶程序只需要包含接口聲明的頭文件,就可以調用COM對象的接口,而組件程序必須提供具體的實現過程,也就是說,如果一個COM對象實現了這個接口,則它提供的接口指針所指向的結構中,每個成員必須是有效的函數指針。
因為接口被用於組件程序和客戶程序的通信橋梁,所以接口應該具有不變性,一個COM對象可以支持多個接口。為了讓客戶程序標識每個接口,類似於COM對象的標識方法,COM接口也使用全局唯一標識符,它被稱為接口標識符(IID,interface identifier)。
例如:
extern "c" const IID IID_Iunknown =
{ 0x00000000,0x0000,0x0000, { 0xc0,0x00,0x00,0x00, 0x00, 0x00, 0x00,0x46} };
如果客戶程序要使用一個COM對象的某個接口,則它必須知道該接口的IID和接口所能提供的方法(即接口成員函數)。
2.3.2 接口的一些特點
* 二進制特性
接口規范並不建立在任何編程語言的基礎上,而是規定了二進制一級的標准。任何語言只要有足夠的數據表達能力,它就可以對接口進行描述,從而可以用於與組件程序有關的開發。
* 接口不變性
接口是組件客戶程序和組件對象之間的橋梁,接口如果經常發生變化,則客戶程序和組件程序也要跟著變化,這對於系統的開發非常不利,也不符合組件化程序設計的思想。因此,接口應該保持不變,只要客戶程序和組件程序都按照既定的接口設計進行開發,則可以保證在兩者獨立開發結束後,它們的協作運行能力能達到預期的效果。
* 繼承性
COM接口具有不變性,但接口也需要發展。類似與C++中的繼承性,接口也可以繼承發展。與C++中的繼承不同,接口繼承只是說明繼承,即派生的接口只繼承了基接口的成員函數說明,並沒有繼承基接口的實現,因為接口定義不包括函數實現部分,而且接口繼承只允許單繼承,不允許多重繼承。根據COM規范,所有的接口都必須從Iunknown接口派生,而且一般的接口都是直接派生於Iunknown接口。
* 多態性-運行過程中的多態性
多態性是面向對象系統的重要特征,COM對象也具有多態性,其多態性通過COM接口體現。多態性時客戶程序可以用統一的方法處理不同的對象,甚至是不同類型的對象,只要它們實現了同樣的接口。如果幾個不同的COM對象實現了同一個接口,則客戶程序可以用同樣的代碼調用這些COM對象。因為COM規范允許一個對象實現多個接口,因此,COM對象的多態性可以在每個接口上得到體現。
2.4 Iunknown接口
COM規范說明,COM定義的每一個接口都必須從Iunknown繼承過來,其原因在於Iunknown接口提供了兩個非常重要的特征:生存期控制和接口查詢。客戶程序只能通過接口與COM對象進行通信,需要控制對象的存在與否。如果客戶還要繼續對對象進行操作,則它必須保證對象能一直存在於內存中;如果客戶對對象的操作已經完成,而且不再需要該對象,則它必須及時地把對象釋放掉,以提高資源的利用率。Iunknown引入了"引用計數"(reference counting)方法,可以有效地控制對象的生存周期。
另一方面,如果一個COM對象實現了多個COM接口,在初始時刻,客戶程序不大可能得到該對象所有的接口指針,它只會擁有一個接口指針。Iunknown使用了"接口查詢"(QueryInterface)的方法來完成接口之間的跳轉。
Iunknown包含了三個成員函數:QueryInterface、Addref和Release。函數QueryInterface用於查詢COM對象的其它接口指針,函數Addref和Release用於對引用計數進行操作。
2.4.1 引用計數
COM采用了"引用計數"技術來解決內存管理的問題,COM對象通過引用計數來決定是否繼續生存下去,對於一個COM對象來說,只要有任一個邏輯模塊還需要使用它,那麼它就必須駐留在內存中,不能釋放自己。因此,每一個COM對象都記錄了一個稱為"引用計數"的數值,該數值的含義是有多少個有效指針在引用該COM對象。當客戶得到了一個指向該對象的接口指針時,引用計數增1;當客戶用完了該接口指針後,引用計數減1。當引用計數減到0時,COM對象就應該把它自己從內存中清除掉。當客戶程序對一個接口指針進行了復制,則引用計數也應該相應增加。Iunknown的接口成員函數Addref和Release分別完成引用計數的加1和減1操作。通過引用計數,COM對象的客戶程序可以通過接口指針很好地控制對象的生存期。
2.4.2 實現引用計數
按照COM規范,一個COM組件可以實現多個COM對象,而且每個COM對象又可以支持多個COM接口。因此可以實現在COM組件一級,COM對象一級,甚至於對象的每個接口一級設置引用計數。
如果在組件一級設置引用計數,那麼可以控制組件模塊的生存周期,但不能控制COM對象的生存周期。如果一個組件有兩個COM對象,則必須等到所有的COM對象都使用完以後,所有的COM對象才可以一起被釋放。這樣做降低了系統資源的利用率。
如果在接口一級設置引用計數,可以跟蹤客戶對象COM對象的使用情況。對於實現多個接口的對象,很有可能某些接口沒有被客戶使用,那麼這些接口相關的資源可以不被占用。但每當一個接口的引用計數減到0時,它必須給對象發出通知,對象在接到通知後,需要判斷是否所有的接口引用計數為0,若是,就把自己釋放,然後再進一步通知組件程序,組件程序接到通知後判斷是否所有的對象都已被清除,若是,則它可以被卸出內存。這個過程比較繁瑣,而且也需要占用一部分的時間。
從折中的角度出發,比較合理的方案是采用對象一級的引用計數以便控制對象和組件的生存周期。這樣使多個對象的組件程序可以有效地提高系統資源利用率。當一個對象被釋放掉以後,它必須通知組件程序,如果組件程序發現已經沒有對象存在了,則組件模塊應該可以從內存中卸出。因此,組件程序應該保持一份有效對象的記錄,可以用一個全局的對象計數值來控制組件的生存周期。
2.4.3 接口查詢
按照COM規范,一個COM對象可以實現多個接口,客戶程序可以在運行時刻對COM對象的接口進行詢問,只有對象實現了該接口,對象才能提供這樣的接口的服務。要實現接口查詢,就要使用Iunknown的成員函數QueryInterface。
QueryInterface函數的IDL(interface description language,接口描述語言)語言說明:
HRESULT QueryInterface([in] REFIID iid,[out] void **ppv);
函數的輸入參數iid為接口標識符IID,它是與GUID一樣的128位整數,用來標識一個COM對象所支持的接口。輸出參數ptv為查詢得到的結果接口指針,如果對象沒有實現iid所標識的接口,則輸出參數ptv指向空(NULL)。 當客戶創建了COM對象後,創建函數總會返回一個接口指針,因為所有的接口都繼承於Iunknown,所以,所有的接口都有QueryInterface成員函數,於是,在得到了初始的接口指針之後,可以通過它的QueryInterface函數獲得該對象支持的任何一個接口指針。
2.4.4 COM對象的接口原則
COM規范對接口的查詢給出了以下一些規則:
* 對於同一個對象的不同接口指針,查詢得到的IUnknown接口必須完全相同。也就是說,每個對象的Iunknown接口指針是唯一的,因此,對兩個接口指針,可以通過判斷其查詢到的Iunknown接口是否相等來判斷它們是否指向同一個對象。
* 接口對稱性。對一個接口查詢其自身總應該成功。
* 接口自反性。如果從一個接口指針查詢到另一個接口指針,則從第二個接口指針再查詢第一個接口指針必定成功。
* 接口傳遞性。如果從第一個接口指針查詢到第二個接口指針,從第二個接口指針可以查詢到第三個接口指針,則從第三個接口指針可以查詢到第一個接口指針。
* 時間無關性。如果一個接口在某一個時刻可以查詢到另一個接口指針,則以後任何時候再查詢相同的接口指針,一定可以查詢成功。
2.5 COM特性
COM規范所定義的組件模型,除了前面提到的面向對象的特性和客戶/服務器特性這兩個基本特性外,值得重點說明的就是COM規范的語言無關性,對進程的透明性和它的可重用機制。
2.5.1語言無關性
COM對象的定義不依賴於特定的語言,因此,編寫組件對象所使用的語言與編寫客戶程序的語言可以有所不同,只要它們都能生成符合COM規范的可執行代碼即可。COM標准與面相對象的編程語言不同,它所采用的是一種二進制代碼級的標准,而不是源代碼級的標准。因此,COM的語言無關性實際上為跨語言合作開發提供了統一標准。在OSKIT中,所有的COM對象都是用C語言寫成的,但只要依據COM規范,完全可以用C++改寫其中的某些模塊,然後單獨編譯該模塊,而可以繼續使用OSKIT中其它的原有模塊。
2.5.2 進程透明特性
COM所提供的服務組件對象在實現時有兩種進程模型,進程內對象和進程外對象。如果是進程內對象,則它在客戶進程空間運行;如果是進程外對象,則它運行在同一機器上的另一個進程空間,或者在遠程機器的進程空間中。雖然COM對象有不同的進程模型,但這種區別對於客戶機來說是透明的,因此客戶程序在使用組件對象時可以不管這種區別的存在,只要遵循COM規范即可。然而,在實現COM對象時,還是應該慎重選擇進程模型。進程內模型的優點是效率高,但組件不穩定會引起客戶進程崩潰,因此組件進程可能會危及客戶;進程外模型的優點是穩定性好,組件進程不會危及客戶程序,一個組件進程可以為多個客戶程序提供服務,但進程外組件開銷大,而且調用效率相對低一些。
實現這種進程透明性的關鍵在於COM庫,COM庫負責組件程序的定位,管理組件對象的創建和對象與客戶之間的通信。當客戶創建組件對象時,COM庫負責裝入組件模塊或者啟動組件進程。因此,客戶程序可以不管組件對象的進程模型,即使組件的進程模型發生了變化,客戶程序也不需要重新編譯。
2.5.3 可重用性
可重用性是任何對象模型的實現目標,尤其是大型系統,可重用性非常重要。而且,由於COM標准是建立在二進制代碼級的,因此COM對象的可重用性與一般的面向對象語言有所不同。
對於COM對象的客戶程序來說,它只是通過接口使用對象提供的服務,它並不知道對象內部的實現過程,因此,組件對象的重用性建立在組件對象的行為方式上,而不是具體的實現上,這是重用的關鍵。
COM用兩種方式實現對象的重用。假定有兩個COM對象,對象1希望能重用對象2的功能,對象1稱為外部對象,對象2稱為內部對象。
包容方式。對象1包含了對象2,當對象1需要用到對象2的功能時,它可以簡單地把實現交給對象2來完成,雖然對象1和對象2實現同樣的接口,但對象1在實現接口時實際上調用了對象2的實現。
聚合方式。對象1只需簡單地把對象2的接口遞交給客戶即可,對象1並沒有實現對象2的接口,但它把對象2的接口也暴露給客戶程序,而客戶程序並不知道內部對象2的存在。
對象重用是COM規范很重要的一個方面,它保證COM可用於構造大型的軟件系統,而且,它使復雜系統簡化為一些簡單的對象模塊,體現了面向對象的思想。