COM編程大體梳理

比較舊的東西,之前寫的文章如今發出來,轉載請註明~!c++

1       COM編程思想--面向組件編程思想

1.1     面向組件編程 

       衆所周知,由C到C++,實現了由面向過程編程到面向對象編程的過渡。而COM的出現,又引出了面向組件的思想。其實,面向組件思想是面向對象思想的一種延伸和擴展。編程

       下面,我就簡單介紹一下面向組件的思想。在之前,應用程序老是被編寫成一個單獨的模塊,就是說一個應用程序就是一個單獨的二進制文件。後來在引入了面向組件的編程思想後,本來單個的應用程序文件被分隔成多個模塊來分別編寫,每一個模塊具備必定的獨立性,也應具備必定的與本應用程序的無關性。通常來講,這種模塊的劃分是以功能做爲標準的。這樣作的好處有不少,好比當對軟件進行升級的時候,只要對須要改動的模塊進行升級,而後用從新生成的一個新模塊來替換掉原來的舊模塊(但必須保持接口不變),而其餘的模塊能夠徹底保持不變。服務器

       總結一下:面向組件編程思想,歸結起來就是四個字:模塊分隔。這裏的「分隔」有兩層含義,第一就是要「分」,也就是要將應用程序(尤爲是大型軟件)按功能劃分紅多個模塊;第二就是要「隔」,也就是每個模塊要有至關程度的獨立性,要儘可能與其餘模塊「隔」開。這四個字是面向組件編程思想的精華所在,也是COM的精華所在!網絡

1.2     COM的幾個重要概念

1.2.1     組件

       上面已經解釋過組件,如今我只想強調一下組件須要知足的一些條件。首先是封裝性,組件必須向外部隱藏其內部的實現細節,使從外部所能看到的只是接口。而後是組件必須能動態連接到一塊兒,而沒必要像面向對象中的class同樣必須從新編譯。如今我只想強調一下組件須要知足的一些條件。首先是封裝性,組件必須向外部隱藏其內部的實現細節,使從外部所能看到的只是接口。而後是組件必須能動態連接到一塊兒,而沒必要像面向對象中的class同樣必須從新編譯。框架

1.2.2     接口

       因爲組件向外部隱藏了其內部的細節,所以客戶要使用組件時就必須經過必定的機制,也就是說要經過必定的方法來實現客戶與組件之間的通訊,這就須要接口。所謂接口就是組件對外暴露的、向外部客戶提供服務的「鏈接點」。 。外部的客戶見不到組件內部的細節,它所能看到的只是接口,這有點像OSI網絡協議分層模型,每一層就像一個組件,它內部的實現細節對於其餘層是不可見的;而每一層經過「服務接入點」向其上層提供服務。分佈式

1.2.3     客戶

       這裏所說的客戶不是指使用軟件的用戶,而是指要使用某一個組件的程序或模塊。也就是說,這裏的客戶是相對組件來講的。函數

2       COM原理

 

2.1     COM與虛函數列表

       COM中的接口其實是一個函數地址表,當組件實現了這個接口後,這個函數地址表中就填滿了組件所實現的那些接口函數的地址。而客戶也就是經過這個函數地址表得到組件中那些接口函數的指針,從而得到組件所提供的服務的。從某種意義上說,咱們能夠把接口理解爲c++中的虛擬基類;或者說,在c++中能夠用虛擬基類來實現接口!這是由於COM中規定的接口的存儲結構,和c++中的虛擬基類在內存中的結構是一致的,咱們能夠簡單的用純粹的C++的語法形式來描述COM是個什麼東西:spa

 class IObject
  {
  public:
    virtual Function1(...) = 0;
    virtual Function2(...) = 0;
    ....
  };.net

 


  class MyObject : public IObject
  {
  public:
    virtual Function1(...){...}
    virtual Function2(...){...}
....
  };設計

IObject就是咱們常說的接口,MyObject就是所謂的COM組件。記住接口都是純虛類。COM中全部函數都是虛函數,都必須經過虛函數表VTable來調用,這一點是無比重要的,必需時刻牢記在心。爲了讓你們確切瞭解一下虛函數表是什麼樣子,從《COM+技術內幕》中COPY了下面這個示例圖:

 

 

 

 

 

2.2     COM基本接口類    

2.2.1     IUnknown

     COM規範規定任何接口都必須從IUnknown繼承,IUnknown包含三個函數,分別是 QueryInterface、AddRef、Release。這三個函數是無比重要的,並且它們的排列順序也是不可改變的。

       引用計數。AddRef用於增長引用計數。Release用於減小引用計數。首先咱們考慮com對象只實現一個接口的狀況,不妨把接口成爲IsomeInterface, 由於IsomeInterface繼承與IUnknown,因此ISomeInterface接口成員函數中包含IUnknown的三個函數。假設有一個客戶程序的許多個邏輯模塊使用到了該com對象, 從而在客戶程序不少地方保持了該接口指針的引用, 好比說有三個地方分別用了pSomeInterface1 pSomeInterface2, pSomeInterface3只想該接口指針。 在客戶程序這三個邏輯塊中, 它能夠調用接口成員函數一夥的接口所提供的服務,若是他一直要該接口提供的服務,把他就須要控制該對象使它一直保持的內存中。若是用完了該對象,那他就應該通知接口再也不須要服務。 因爲每一個邏輯模塊並不知道其餘的邏輯模塊是否在繼續使用COM對象, 他們只能知道本身是否還須要該對象。而對於COM對象來講, 只要有任意一個邏輯模塊還須要使用它, 那麼他就必須駐留在內存中不能釋放本身。COM採用了引用計數來解決這個問題。 當客戶獲得一個指向該對象接口的指針時,計數加1,用完計數減1;當對接口指針複製或賦值,引用計數加1.若是一個COM對象實現了多個接口,則能夠採用同螻蟻計數計數。只要計數不爲0 它就繼續生存下去,反之,則表示客戶再也不使用該對象,它就能夠被清除了。

       接口查詢。當客戶建立COM對象以後, 建立函數總會爲咱們返回一個接口指針, 由於搜有的接口都繼承了IUnknown,因此咱們就能夠經過QueryInterface一個接口指針。

       接口原則性:

(1)對於同一個COM對象的不一樣接口,查詢到的IUnknown接口必須徹底相同,也就是說每一個IUnknown接口指針是惟一的,所以對於兩個接口指針咱們能夠經過判斷其查詢到的IUnknown指針是否相同來判斷他們是否指向同一個對象。 反之若是查詢的不是IUnknown接口,而是其餘接口,則經過不一樣而途徑獲得的接口指針容許不同。這就容許有的對象能夠在必要的時候才動態生成接口指針, 不用的時候能夠把接口指針釋放掉。

(2)接口對成型。對每個接口查詢其自身總應該成功。

(3)自檢討。若是從一個接口指針查詢到另外一個接口指針, 則從第二個接口指針再回到第一個接口指針也一定成功。

(4)接口的傳遞性

(5)接口查詢時間無關性

       咱們來看看 QueryIInterface的實現方法, 咱們考慮這種支持多接口對象的的實現方法。在c++中實現多接口COm對象有兩種簡單方法, 一種是使用多重繼承,八所支持的接口類做爲基類,而後在對象類中實現接口函數。另外一種試試先內嵌接口類成員。 咱們這裏使用多重集成的辦法實現多個接口支持。

       咱們用字典對象作列子,字典對象實現兩個接口:IDictionary和ISpellCheck首先咱們來看一看

 

 

 

 

 

 

 

 

 

 

 

 

接口的轉換過程當中存在虛列表的裁剪問題。

2.2.2   IClassFactory

       IClassFactory的做用是建立COM組件。COM類用一個全局惟一的ID(GUID)來標識,稱爲CLSID,COM利用類廠(ClassFactory)來獲得實例化的COM對象。系統用公共空間保存全部可被重用的COM類的CLSID和其具體位置的對應(在Windows下是保存在註冊表中),這樣全部的用戶只要知道CLSID,都能順利找到COM類。而後COM類能夠利用類廠生成COM對象。COM類和類廠的實現代碼能夠在DLL中,也能夠在EXE中,能夠在本地,也能夠在網絡的另外一端。COM對象要實現多個接口(Interface),每一個接口都包含一組函數,也用一個GUID來標識,稱爲IID。QueryInterface()就能夠根據IID來獲得接口指針。IClassFactory最重要的一個函數就是CreateInstance,顧名思議就是建立組件實例,通常狀況下咱們不會直接調用它,API函數都爲咱們封裝好它了。

       COM規定,每一個COM對象類應該有一個相應的類廠對象,若是一個組件實現了多個COM對象類,則會有多個類廠。記下來咱們看看類廠是如何避實用的。由於類廠自己也是一個COM對象,他被用於其餘COM對象的建立過程。那麼類廠對象又是誰建立的呢?答案是DllGetClassObject函數, 這個函數並非COM庫函數,而是一個組件程序的導出函數(相似DLL導出函數),咱們來看下一下這個函數(ATL中自動生成)

 

STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv)

{

              return _AtlModule.DllGetClassObject(rclsid, riid, ppv);

}

 

       在COM庫中有三個函數用於COM接口的建立,他們分別是CoGetClassObject,CoCreateInstance,CoCreateInstanceEx。CoGetClassObject通常用來建立類廠接口。咱們通常使用CoCreateInstance來建立COM接口指針。可是CoCreateInstance不能建立遠程機器上對象若是要建立遠程對象要使用CoCreateInstanceEx。

2.2.3     IDispatch

         它的做用何在呢?除了C++還有不少別的語言,好比VB、 VJ、VBScript、JavaScript等等。能夠這麼說,若是沒有這麼多亂七八糟的語言,那就不會有IDispatch。COM組件是C++類,是靠虛函數表來調用函數的,對於VC來講毫無問題,這原本就是針對C++而設計的,之前VB不行,如今VB也能夠用指針了,也能夠經過VTable來調用函數了,VJ也能夠,但仍是有些語言不行,那就是腳本語言,典型的如 VBScript、JavaScript。不行的緣由在於它們並不支持指針。如今網頁上用的都是這些腳本語言,而分佈式應用也是COM組件的一個主要市場,它不得不被這些腳本語言所調用,既然虛函數表的方式行不通,只能另尋他法,IDispatch應運而生。 調度接口把每個函數每個屬性都編上號,客戶程序要調用這些函數屬性的時侯就把這些編號傳給IDispatch接口就好了,IDispatch再根據這些編號調用相應的函數,僅此而已。固然實際的過程遠比這複雜,僅給一個編號怎麼調用一個函數,還要知道要調用的函數要帶什麼參數,參數類型什麼以及返回什麼東西,而要以一種統一的方式來處理這些問題是件很頭疼的事。IDispatch接口的主要函數是Invoke,客戶程序都調用它,而後Invoke再調用相應的函數,若是看看MS的類庫裏實現 Invoke的代碼就會驚歎它實現的複雜了,由於必須考慮各類參數類型的狀況,所幸咱們不須要本身來作這件事。在ATL中        

 

2.3     CLSID

       CLSID其實就是一個號碼,或者說是一個16字節的數。觀察註冊表,在HKEY_CLASSES_ROOT\CLSID\{......}主鍵下,LocalServer32(DLL組件使用InprocServer32) 中保存着程序路徑名稱。CLSID 的結構定義以下:

typedef struct _GUID {

       DWORD Data1;  // 隨機數

       WORD Data2;   // 和時間相關

       WORD Data3;   // 和時間相關

       BYTE Data4[8];      // 和網卡MAC相關

} GUID;

 

typedef GUID CLSID;  // 組件ID

typedef GUID IID;    // 接口ID

 

2.4     COM組件核心IDL

          COM具備語言無關性,它能夠用任何語言編寫,也能夠在任何語言平臺上被調用。但至今爲止咱們一直是以C++的環境中談COM,那它的語言無關性是怎麼體現出來的呢?或者換句話說,咱們怎樣才能以語言無關的方式來定義接口呢?前面咱們是直接用純虛類的方式定義的,但顯然是不行的,除了C++誰還認它呢?正是出於這種考慮,微軟決定採用IDL來定義接口。說白了,IDL實際上就是一種你們都認識的語言,用它來定義接口,不論放到哪一個語言平臺上都認識它。

       什麼是IDL和MIDL?

       IDL是接口定義語言。MIDL是Microsoft的IDL編譯器。在用IDL對接口和組件進行了描述後,能夠用MIDL進行編譯,生成相應的代理和存根DLL的C代碼。爲獲得一個代理/存根DLL,須要編譯和連接MIDL生成的C文件。宏REGISTER_PROXY_DLL將完成代理/存根DLL在註冊表中的註冊操做。

       客戶與一個模仿組件的DLL進行通訊,這個DLL能夠完成參數的列集,此組件被稱爲代理。一個代理就是同另外一個組件行爲相同的組件組件還須要一個存根的DLL,以便對從客戶傳來的數據進行散集。存根也將對傳回給客戶的數據進行列集。

2.5     COM組件運行機制

構造一個建立COM組件的最小框架結構
    IUnknown *pUnk=NULL;
    IObject *pObject=NULL;
    CoInitialize(NULL);
    CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);
    pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
    pUnk->Release();
    pObject->Func();
    pObject->Release();
    CoUninitialize(); 

 這就是一個典型的建立COM組件的框架,看看CoCreateInstance內部作了一些什麼事情。如下是它內部實現的一個僞代碼:
    CoCreateInstance(....)
    {
    .......
    IClassFactory *pClassFactory=NULL;
    CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
    pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
    pClassFactory->Release();
    ........
   } 

 它的意思就是先獲得類廠對象,再經過類廠建立組件從而獲得IUnknown指針。繼續深刻一步,看看CoGetClassObject的內部僞碼:

 CoGetClassObject(.....)
   {
    //經過查註冊表CLSID_Object,得知組件DLL的位置、文件名
    //裝入DLL庫
    //使用函數GetProcAddress(...)獲得DLL庫中函數DllGetClassObject的函數指針。
    //調用DllGetClassObject 
   }
    DllGetClassObject是幹什麼的,它是用來得到類廠對象的。只有先獲得類廠才能去建立組件.
    下面是DllGetClassObject的僞碼:
    DllGetClassObject(...)
    {
    ......
    CFactory* pFactory= new CFactory; //類廠對象
    pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
    //查詢IClassFactory指針
    pFactory->Release();
    ......
    }
    CoGetClassObject的流程已經到此爲止,如今返回CoCreateInstance,看看CreateInstance的僞碼:

        CFactory::CreateInstance(.....)
    {
    ...........
    CObject *pObject = new CObject; //組件對象
    pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
    pObject->Release();
    ...........
    } 

//見MyCom的例子

 

 

2.6      鏈接點

     COM 中的典型方案是讓客戶端對象實例化服務器對象,而後調用這些對象。然而,沒有一種特殊機制的話,這些服務器對象將很難轉向並回調到客戶端對象。COM 鏈接點便提供了這種特殊機制,實現了服務器和客戶端之間的雙向通訊。使用鏈接點,服務器可以在服務器上發生某些事件時調用客戶端。

     有了鏈接點,服務器可經過定義一個接口來指定它可以引起的事件。服務器上引起事件時,要採起操做的客戶端會向服務器進行自行註冊。隨後,客戶端會提供服務器所定義接口的實現。客戶端可經過一些標準機制向服務器進行自行註冊。COM 爲此提供了 IConnectionPointContainer 和 IConnectionPoint 接口。

//見MyCom的例子

相關文章
相關標籤/搜索