1、面向對象系統的三個最基本的特性
封裝性、多態性、重用性。
2、COM特性的概述
COM對象的封裝特性是很徹底的,所有的對象狀態信息必須通過接口才能訪問;而COM的多態性完全通過接口體現出來,而且,COM分別在三個層次上體現了多態性:接口成員函數、單個接口、一組接口(對象類別即 implemented category)。而COM的重用性相對復雜。
3、重用性
所謂重用性是指,當一個程序單元能夠對其他的程序單元提供功能服務時,盡可能地重用原先程序單元的代碼,既可以在源代碼一級重用,也可以在可執行代碼一級重用。
C++語言的重用性位於源代碼一級,一個類可以繼承於另一個類,從而把父類的功能重用。但對於COM組件則情形有所不同,因為COM是建立在二進制代碼基礎上的標准,所以其重用性也必然建立於二進制代碼一級。
COM重用性是指一個COM對象如何重用已有的COM對象的功能,而不是重復實現老的功能服務。按照COM的標准,實現這種重用性有兩條途徑:包容和聚合。
4、包容和聚合
對象B調用對象A的相應成員函數實現ISomeInterface接口。因此,對象B的ISomeInterface接口提供的功能可以超過對象A的接口功能,返回結果也可以不一致。甚至,對象B的接口與對象A的接口不一定相同。一般來說,對象A的生存期包含在對象B的生存期之內。
在聚合模型中,被聚合的對象A雖然直接向對象B的客戶程序提供功能服務,但它的生存期仍受對象B控制,而且其他的一些行為也受對象B的控制,包括內部狀態初始化、獲取數據等等。
為了使聚合能夠順利實現,對象A必須能夠適應在被聚合的情況下進行特殊的處理,尤其是接口的QueryInterface成員函數,在被聚合情況下,當客戶請求它所不支持的接口或者IUknown接口時,它必須把控制交給外部對象,由外部對象決定客戶程序的請求結果。
聚合涉及到聚合對象和被聚合對象雙方的協作,並不是每個對象都能夠支持聚合特性,但聚合體現了組件軟件真正意義上的重用。而包容的重用性完全建立在客戶/服務器模型相對性的基礎上,實際上也就是客戶程序和組件程序的嵌套關系。這是包容和聚合本質的不同。
5、委托IUnknown和非委托IUnknown
對象創建函數CoCreateInstance的第二個參數pUnknownOuter用於解決聚合中IUnknown接口的問題。當其為NULL時表示正常使用,不為NULL時被聚合使用。內部對象實現兩個IUnknown 分別用於這兩種情況:委托IUnknown和非委托IUnknown(delegating unknown和nondelgating unknown)。
按照通常使用方式實現的IUnknown為非委托IUnknown,而委托IUnknown在不同的情況下有不同的行為:當對象被正常使用時,委托 IUnknown把調用傳遞給對象的非委托IUnknown;當對象被聚合使用時,委托IUnknown把調用傳遞到外部對象的IUnknown接口,即對象被創建時傳遞進來的pUnkownOuter參數,並且,這時外部對象通過非委托IUnknown對內部對象進行控制。委托IUnknown本身不進行任何操作。
因為C++類不支持同時實現兩個IUnknown,所以委托IUnknown和非委托IUnknown不能都使用IUnknown類,但我們可以定義一個新的類。因為COM不是通過類名來識別接口,而是通過vtable來調用接口成員函數。
6、COM接口調用的進程透明性
客戶程序創建COM對象具有進程透明特性,不管是進程內組件還是進程外組件,客戶程序可以使用一致的方法創建COM對象。對於進程內組件,無論是創建過程,還是客戶程序對接口函數的調用過程,都可以按照一般的同一進程內部函數調用的過程來理解組件和客戶之間的交互操作;但對於進程外組件,實際的情形要復雜得多,因為組件程序戶程序擁有不同的進程空間,所以,它們之間所有的交互過程都涉及到進程之間的通信過程。然而,COM客戶程序創建進程外組件程序成功後,它就得到了組件對象的一個接口指針,通過該指針間接調用組件對象的成員函數,如同調用本進程內的函數一樣,這正是COM所期望達到的透明效果。
7、進程外組件對象與客戶程序之間通信過程
接口指針所指的是本進程中的代理對象(proxy),客戶調用的是代理對象的成員函數,由代理對象通過跨進程的調用方法(LPC/RPC)與對象進程中的存根代碼(stub)通信,存根代碼再調用組件對象成員函數。函數返回的順序剛好相反。在這個交互過程中,可以看到,客戶仍然在調用同一進程內的組件對象,而組件對象也被同一進程內的客戶調用,從客戶和組件對象兩個角度絲毫感覺不到進程的邊界,所有跨進程的操作完全由代理對象和存根代碼包攬了。
8、列集(marshaling)與散集(unmarshaling)
列集是指客戶進程可以透明地調用另一進程中的對象成員函數的一種參數處理機制。
代理對象用列集手段處理成員函數的參數,通過列集處理後得到一個數據包(數據流),然後通過一種跨進程的數據傳輸方法,比如共享內存方法,甚至是網絡協議等,當數據包傳輸到對象進程後,存根代碼用散集(unmarshaling,列集的反過程)的方法把數據包參數解譯出來,再用這些參數去調用組件對象;當組件對象成員函數返回後,存根代碼又把返回值和輸出參數列集成新的數據包,並把數據包傳到客戶進程中,代理對象接收到數據包後,把數據包解譯出來再返回給客戶函數,從而完成一次調用。
9、連接
連接是指客戶進程與組件進程的一種依賴關系,簡單地說,客戶程序的一個有效接口指針就代表了一個連接。
連接是在函數調用的過程中產生的,最常使用的QueryInterface就是一個很好的例子。
連接是跨進程通信的基礎,新的連接本身也是在其他連接的調用過程中產生的。
10、不同參數的列集處理
32位整數只要把4字節的數據順序裝到數據包中或者從數據包中去出來即可;字符串或者結構類型的數據列集過程也可以按此方法處理。
對指針的列集處理過程是:列集時,把指針所指的數據裝到數據包中;散集時,在進程中分配一塊內存,把數據包中的數據拷貝到內存中,所得內存的地址即為散集的結果。
如果函數的參數中包含了指向接口的指針,則情形要復雜得多。接口的列集包含了代理對象和存根代碼的創建過程,實際上接口指針的列集過程也包括了連接的創建過程。
11、列集過程的兩種實現方式:
自定義列集法(custom marshaling),也稱為基本列集法(basic marshaling architecture)。其列集過程完全由對象自身控制,對象指定其代理對象的CLSID,代理對象控制了其所有接口的列集過程,包括接口參數的列集和散集,以及代理對象和存根代碼之間的跨進程通信過程。
標准列集法(standard marshaling),是由COM提供缺省的代理對象和存根代碼,因為列集過程涉及到操作系統的一些復雜特性的編程,如共享內存操作或其他跨進程數據傳輸機制,甚至通過網絡協議傳輸數據,所以COM提供了缺省的代理和存根代碼以及一套標准的列集方法,可以處理常用數據類型的列集和散集,包括指針類型和接口指針類型。
標准列集法的原理以及其列集過程與自定義列集法完全一致,事實上標准列集法是自定義列集法的一個特例。但兩者有一個基本的不同:自定義列集法其列集過程完全由對象自身控制,所以它以整個對象為列集單位,即對象指定的代理對象和存根代碼必須處理對象支持的所有接口;而標准列集法使用COM提供的標准代理對象和存根代碼,實際上該代理對象和存根代碼只是列集過程的管理器,因此,標准列集法是以接口為列集單位,COM提供的很多標准接口,其列集過程已經由COM 庫提供了,程序員只需要提供自定義接口的列集代碼即可。
12、標准列集的實現
COM已經提供了缺省的代理對象、存根管理器以及RPC通道,我們只需要實現每個接口的代理/存根模塊。一旦系統中安裝了某個接口的代理/存根程序並正確地進行了注冊,則代理管理器和存根管理器會在需要的時候自動加載接口代理和接口存根。因此,從實現的角度來講,我們的任務就是針對接口實現代理/存根程序。
代理/存根組件是一個DLL程序,除了實現接口代理和接口存根之外,還應該實現相應的類廠,代理/存根組件要求類廠支持IPSFactoryBuffer 接口,通過IPSFactoryBuffer::CreateProcy和IPSFactoryBuffer::CreateStub成員函數創建接口代理和接口存根對象。接口代理對象支持兩個接口:它本身提供列集特性的接口和IRpcProxyBuffer接口,其中IRpcProxyBuffer接口只有Connect和Disconnect成員函數,被代理管理器用於創建或取消它與RPC通道的連接;它本身提供列集特性的接口的成員函數接受客戶程序的調用,並把客戶的調用參數放到RPC通道中,然後調用RPC通道的SendReceive成員函數,函數返回後,把返回值和輸出參數解譯出來。這些操作是接口代理對象應該完成的。與此相對應,接口存根只要實現IRpcStubBuffer接口,除了存根管理器所調用的幾個與RPC通道連接的函數外,最主要的成員函數為IRpcStubBuffer::Invoke,RPC通道調用此函數以響應客戶進程的SendReceive調用,Invoke函數把客戶進程傳遞過來的參數解譯出來,然後調用組件對象的接口成員函數,並把返回結果或者輸出參數經過RPC通道傳回到客戶進程的RPC通道中。
接口代理對象和接口存根對象必須非常小心地處理接口成員函數的參數,尤其是一些指針或者結構參數,如果成員函數中包含接口指針類型,則還需要調用 CoMarshalInterface或者CoUnmarshalInterface函數,以便創建相應的存根或者代理對象。在代理和存根中對參數的處理必須嚴格一致,否則會發生不可預料的後果。如果客戶進程和組件進程在同一台機器上運行,則COM會根據注冊表中的接口信息,在兩個進程中使用相同的代理/存根程序,所以我們只要保證接口的代理/存根程序中對參數的列集和散集格式一致,參數傳遞就不會有問題;但如果客戶與組件程序在兩台機器上,則不能嚴格保證兩個進程會使用相同的代理/存根程序,那麼對參數的列集和散集最好使用統一的數據格式表示,以保證參數傳遞的正確性。
如果一個進程外組件實現了多個COM接口,那麼是否需要為每一個接口實現其代理/存根組件程序呢?在這些接口中,如果它是COM提供的標准接口,或者是 OLE標准接口,則COM或者OLE已經提供了其代理/存根程序,我們可以不管這些接口,直接使用即可;如果自定義的接口,則必須自己實現代理/存根程序,並注冊到系統中,然後才能真正使用這些接口。
13、自定義接口的代理/存根程序的實現
Microsoft提供了MIDL實用工具幫助我們建立自定義接口的代理/存根程序。首先我們使用IDL(接口描述語言)語言建立接口描述文件,然後運行 MIDL工具,它會根據接口描述文件生成一些C語言源代碼文件,用這些源代碼文件可以創建代理/存根組件程序。它為我們提供了接口代理/存根組件的一種標准實現方法。
用IDL描述接口與C++描述接口有一些相似之處,但IDL是一種平台無關的標准化描述語言。Win32 SDK提供了所有COM或者OLE標准接口的IDL描述,包括unknown.idl(定義了IUnknown接口),可以在Visual C++的include目錄下找到這些IDL文件。Microsoft的RPC開發包中包括運行程序MIDL.EXE和接口列集使用的 RpcProxy.h、Unknown.idl和wtypes.idl,以及RPC調用所需要的靜態連接庫和動態連接庫。
一般地,用MIDL程序可以產生實現代理/存根組件程序所需要的所有C語言源代碼文件:***.h為接口說明頭文件;***_p.c為接口代理和存根的實現文件;***_i.c為定義所有GUID描述符的文件;dlldata.c包含代理/存根程序的入口函數及類廠所需的數據結構。
運行NMAKE程序可以生成代理/存根組件程序,在集成開發環境中也可以生成。在集成開發環境中創建一個工程,並把MIDL生成的源代碼文件的DEF文件加入到工程中,並在編譯選項中加入REGISTER_PROXY_DLL,在連接選項中加入rpcrt4.lib、uuid.lib。
COM庫能夠提供代理管理器和存根管理器,並且MIDL又能夠自動生成自定義接口的代理和存根源代碼,但目前,COM庫還不能在運行過程中根據接口的描述自動生成接口代理和接口存根來處理自定義接口的列集過程。
14、MIDL創建自定義接口代理/存根組件程序的過程
(1)編寫接口的IDL文件;
(2)運行MIDL工具生成相關的源代碼文件;
(3)編寫DEF文件;
(4)編寫MAK文件;
(5)編譯連接得到接口/存根組件程序;
(6)運行regsvr32.exe注冊組件程序。
17、COM的安全性
安全性不是COM的主要目的,但既然COM是一種平台獨立的軟件模型,而且提供了跨進程甚至跨網絡的客戶/服務器軟件結構,則安全性是不可缺少的保護機制。建立一種適合各種操作系統的安全性機制是不可能的,所以COM規范也只是提供了安全性機制框架。在Windows平台上實現的COM版本基本上基於 Windows NT的鑒定服務(authentication service)機制。
18、Windows NT安全機制
Windows NT作為網絡操作系統,具有完全的保護機制,系統的所有資源都是受保護的,這些資源包括文件、外設、進程、線程,甚至同步對象、共享內存、注冊表中的鍵等等。所謂受保護是指這些資源與特定的訪問權限聯系在一起,當這些資源被訪問時,操作系統要對權限進行驗證,以便允許訪問或者禁止訪問。
19、RPC鑒定的5個層次
(1)無鑒定操作即正常的RPC調用;
(2)連接時進行鑒定;
(3)每一個接口調用時進行鑒定
(4)對每個請求進行鑒定,並對接收到的數據包進行完整性檢驗;
(5)進行所有的鑒定並對數據包加密。
20、COM提供了兩種類型的安全性
激活安全性(activation security),不同於激發安全性(launch security),包括COM對象如何被安全地啟動、客戶如何與對象建立連接,以及如何保護公共的資源,比如全局運行對象表、系統注冊表等。
調用安全性(call security),是指在已經建立連接的基礎上,客戶調用組件程序的安全保護問題。
21、激活安全性
SCM是COM庫中負責找到並啟動組件程序的組件。當客戶向COM庫請求創建新的COM對象或者連接已經運行的組件對象時,負責處理請求的正是SCM。因此,激活安全性也通過SCM實現。
激活安全性是進程一級的安全性,即進程中所有的對象和所有對象的成員函數共享的安全性,它分兩種情況:靜態安全性和動態安全性。當SCM接收到激活對象的請求時,它檢查注冊表中安全配置信息,以便滿足合法用戶的請求,這稱為靜態安全性檢查;另一種情況是,在程序運行過程中設置進程的安全性,這稱為動態安全性檢查。
Windows提供的工具DCOMCNFG.EXE(在控制面板中)可以對組件的安全性進行設置。
22、調用安全性
調用安全性的實現方法之一是使用IClientDecurity接口,方法二是使用COM提供的API函數。IClientDecurity是接口代理選擇實現的接口,它的三個主要成員函數:CopyProxy、QueryBlanket和SetBlanket。COM提供了幾個API函數封裝了接口 IClientDecurity的調用:CoQueryProxyBlanket、CoSetProxyBlanket和CoCopyProxy。
MIDL生成的接口代理對象實現了IClientDecurity接口,並且系統代理管理器也實現了IClientDecurity接口,所以並不需要自己實現IClientDecurity接口。
23、Win32線程和COM線程
Win32提供兩種線程:UI線程(user-interface thread,也稱為用戶界面線程)和輔助線程(worker thread)
對應於Win32的兩種線程,COM也有兩種線程類型:套間線程(apartment thread)(對應於UI線程)和自由線程(free thread)(對應於輔助線程)。
COM線程特性是針對特定的COM對象,而不是針對COM組件程序,所以在同一個COM組件中的不同對象可以運行在不同的線程類型上。
24、COM線程的使用
(1)進程內組件對象
如果一個COM對象運行在一個套間線程中,那麼此COM對象與UI線程中的窗口對象有很類似的特性。COM對象屬於創建此對象的套間線程所有。套間線程通過消息控制函數被自動同步,所以,運行在套間線程中的COM對象,不需要進行同步處理,但套間線程外的客戶的其他線程要訪問此線程只能通過代理/存根實現。
如果一個COM對象運行在一個自由線程中,那麼同一進程中的其他線程(即客戶線程)可以直接調用此對象成員函數,但對象成員函數必須進行同步處理,以保證其線程安全性。
(2)進程外組件對象
如果是進程外組件對象,則不管其運行在套間線程還是自由線程中,客戶調用必須跨進程,因此調用始終是間接進行的,所以列集對於進程外組件對象是必須的,而列集的結果是自動實現同步的,對象成員函數可以不處理同步。
套間線程中的對象被跨線程調用時,與跨進程調用有著類似的特性。所以套間線程有自己的COM庫初始化和終結過程調用。
25、列集和同步
COM對象的不同線程模型影響的主要是列集處理和同步處理。結果列集處理的調用總是通過代理和存根間接進行,因此,其效率也自然有所降低,但列集使所有對對象的調用通過消息循環中轉,所以調用被自動進行同步處理,某一時刻至多只能有一個調用在進行,所以COM對象不需要進行同步處理,也就是說COM對象可以不是線程安全的(thread-safe)。反過來,不通過列集處理的調用雖然是直接進行的,效率也比較高,但某一時刻可能會有多個客戶同時調用,因此,對象必須要進行同步處理,以便保證對象是線程安全的。
26、不同線程模型(客戶線程與對象線程的不同組合)對列集和同步的不同要求
(1)客戶和對象運行在不同的進程中。客戶調用進程外組件總是要通過代理和存根,所以列集是必須的。因此,COM自動實現了調用的同步處理,對象不必進行同步處理。
(2)客戶和對象運行在同一個線程中。與對象處於同一線程中的客戶調用對象總是直接進行的,而且同一線程中的調用不可能沖突,所以客戶調用即不需要列集,而且對象也不必進行同步處理。
(3)客戶和對象運行在同一個進程中,對象運行在套間線程中,客戶運行在另一個套間線程或自由線程中。因為對象運行在它自己的套間線程中,所以客戶調用總需要列集處理,COM自動實現同步處理,對象不必考慮同步。COM會自動為我們實現接口指針的列集處理,我們也可以自己對接口指針進行列集處理。
(4)客戶和對象運行在同一個進程中,對象運行在自由線程中,客戶運行在另一個套間線程或自由線程中。當客戶調用自由線程中的對象時,雖然對象被自由線程所創建,但調用實際上在客戶線程中執行,所以客戶對接口的調用是直接進行的,因此接口列集是不必要的,但對象必須自己處理同步,因為多個客戶有可能同時調用接口成員函數。
27、套間線程
在套間線程的主函數中有一個消息循環,而且主函數必須對COM庫進行初始化。COM在套間線程中創建了一個隱藏的窗口,主函數的消息循環負責接收消息並分發消息(包括客戶對對象的調用的消息)。
對於運行在套間線程中的COM對象來說,因為這樣的對象只能被此線程訪問,其他的線程只能通過代理/存根調用接口函數,所以對象可以不必擔心同步問題,但對象仍然需要保護全局變量,因為對象的成員函數被所有的同類對象所共享,因而函數有可能會重入。進程內組件DLL程序的入口在多線程環境下有可能被同時訪問到,因此,這些入庫函數如DllGetClassObject和 DllCanUnliadNow仍然需要進行同步處理,以保證多線程訪問時不會發生沖突。進一步來講,DLL組件程序的類廠也必須滿足一定的要求,以保證多個線程同時訪問類廠對象時不會引發沖突問題,尤其當用類廠對象創建多個組件對象時,類廠必須是線程安全的,即內部提供了同步處理。使類廠線程安全只需對引用計數操作進行同步保護即可。
如果套間中的函數要把接口指針傳給另一個線程,不管此線程是套間線程還是自由線程。列集和散集是必須要進行的,列集處理分兩種情況:自動列集和手工列集。自動列集的情況比較簡單,凡是通過COM傳遞的接口指針,COM都會自動列集,包括裝入接口代理和存根代碼等等。手工處理列集也是可能的,因為客戶線程與對象線程在同一個進程中,因此,通過其他途徑傳遞接口指針也很方便。
28、自由線程
自由線程在概念上與Win32的輔助線程完全一致。它們只有一個主函數,當主函數執行完成後,線程就自動結束。在自由線程的主函數中,必須調用 CoInitializeEx函數,而且dwCoInit參數必須指定為COINIT_MULTITHREADED,以便COM知道這是一個自由線程。自由線程中的COM對象必須是線程安全的,所有的同步工作由對象自己處理。
與套間線程類似,自由線程即可以由客戶程序創建,也可以由類廠創建,但不管哪種情況,COM對象總是由自由線程的主函數來創建。
29、進程內組件的線程模型
通常進程內組件並不調用CoInitialize或者CoInitializeEx標識其對象所使用的線程模型,但是,COM需要知道進程內對象的線程模型,以便正確處理跨線程情況下接口指針的傳遞以及對象調用的同步處理,所以,我們要在系統注冊表中指定對象的線程模型。
對於進程內組件程序,為了支持多線程的情形,不管是套間線程模型還是自由線程模型,其入口函數DllGetClassObject和DllCanUnloadNow應該是線程安全的,尤其需要對引用計數包括對象引用計數器以及鎖計數器等進行同步保護。
在實際使用過程中,通常對象被客戶線程所創建,因此客戶線程模型與對象的線程模型有可能不一致,這種不一致性包含兩種可能:支持套間線程模型的對象被自由線程所創建,則COM會生成一個套間線程來運行對象,並把列集後的接口指針傳給客戶線程;第二,支持自由線程的對象被套間線程所創建,則COM會生成一個自由線程來創建對象,並把接口指針經列集後(可以優化)傳給套間線程。
套間線程模型的COM對象,如果用到了自定義接口,則即使是進程內組件程