天天看點

ATL炒冷飯學習之一:COM對象與C++對象的差別1. 二進制對象标準2. IUnknown - 生命期,發現新内容和版本3. IDL

ATL炒冷飯學習之一:COM對象與C++對象的差別

ATL(Active Template Library,活動模闆庫)是微軟開發的一套 COM(Component Object  Model,元件對象模型)支援庫。

1. 二進制對象标準

C++ 的對象布局

衆所周知,在 C++裡面,一個類可以擁有成員資料和成員函數,如下面的類:就是一個具有兩個屬性和一個函數的一個類:

class MyDemoClass

{

    int  number;

    string  name;

public:

    MyDemoClass(int,name);

    string getName() const;

    // ...

};

void print( const MyDemoClass& obj )

{

    cout<<obj.getName()<<endl;

}

      熟悉VC的程式員都知道,如果這個類和函數是用 VC編譯出來的,那麼就不要指望在 Borland C++ Builder 裡面使用它。别說

lib 檔案格式不一樣,可能的原因是兩個編譯器下面的 string、使用的堆和 BCB都可能是有差别的,任何一個細微的差别,都可能導緻問題,而且是 crash。是以,C++的對象标準不是二進制的,而是源代碼級的。就連不同的 C++編譯器都不遵循一個标準,更不要說跨語言的VB,pascal的互動了。

僅有 vtable 的類

     在 C++的類中,有一種函數叫做虛拟函數。虛拟函數的調用是“動态綁定的”,也就是說,是在運作的時候才決定的。譬如說這段代碼:

class MyInterface

{

    virtual int getID() const = 0;

};

MyInterface* p = getSomeObject();

cout<<p->getID()<<endl;

這段代碼中的 p->getID()的調用,可能和下面的僞代碼差不多:

function_pointer* vtbl = (function_pointer*)p;

p->vtbl[0];

       也就是說,在c++中調用一個函數的時候,使用一個整數(虛函數表的下标)來唯一确定這個函數。并且,因為沒有資料成員,是以這個類可能隻有一個指針。這個指針的偏移量不用計算,就是0;或者說,如果 vtbl的布局是相同的,并且調用某個函數的調用規範也是相同的,那麼可以在不同的編譯器之間共享這個對象指針。要統一vtbl的布局和調用方式,比統一不同編譯器類的布局,标準庫的實作以及各種編譯參數要簡單多了。

COM

     COM,全稱為 Component Object Model,是 MS提出的一個二進制的對象标準。它以來的就是vtable所說的相同的vtbl 布局。任何一種可以通過間接指針調用函數的語言都可以實作或者使用COM 對象,這些語言包括 C/C++,VB,Pascal,Java,...,甚至 DHTML。在COM 中,類使用者所看到的就是一個又一個的“接口”,也就是隻有一個 vtbl的類,它的每一個函數的調用方式是 __stdcall。

2. IUnknown - 生命期,發現新内容和版本

對象生命期

    OO 系統中,對象的所有權和生命期是一個永恒的話題:這是因為,在複雜的OO系統中,對象之間的關系非常複雜,使用中既不希望在引用一個對象的時候發現它已經不存在了,也不希望一個無用的對象繼續在記憶體中占用系統的資源。C++提出了auto_ptr,shared_ptr,...,這一切都為了友善管理對象的生命期。在沒有Garbage Collection的系統中,常用的管理方式之一就是引用計數。當需要使用一個對象的時候,把它的引用計數加一,使用完了,把它的引用計數減一。對象可以在内部維護一個計數器,也可以忽略這些資訊(對于靜态/棧上的對象來說)也就是說,通過一個對象引用計數使得對于不同對象生命期的管理具有同樣的接口。

IUnknown接口具有兩個方法:

ULONG AddRef();

ULONG Release();

     前者可以把接口的引用計數加一,後者可以把接口的引用計數減一(請注意,是接口的引用計數!這和對象的引用計數有差別)同時使用引用計數還可以避免一個問題,就是建立形式不同的問題。譬如說,如果你在一個dll 裡面有一個函數:string* getName();。你在 exe裡面調用這個函數,獲得一個指針,當使用完了這個指針以後,可能會delete 它,因為在 dll 裡面,這個指針是通過 new建立的。可是這樣很可能會崩潰,因為你的 dll 和你的 exe可能是用不同的堆,這樣,exe 的 delete在自己的堆裡面尋找這個指針,往往會失敗。

發現新内容

C++裡面,如果擁有一個基類的指針,可以通過強制類型轉換獲得派生類的指針,進而獲得更豐富的功能:

class Derived:public Base

{

public:

    virtual void

anotherCall();

};

Base* b = getBase();

Derived* d = (Derived*)b;

    可是,通常這被認為是一個很危險的操作,因為如果你不能确認這個 b的确指向了一個 d,那麼接下去對 d 的使用帶來的很可能是程式崩潰。C++提供了一個機制,可以在類中加入類型資訊,并且通過 dynamic_cast來獲得有效的指針。出于種種原因(早期編譯器對于 dynamic_cast支援不好,或者是類型資訊的加入往往是全局的,開銷很大,...)有些類庫,譬如說MFC,自己實作了類似的機制。這樣的機制的實作,往往是基于在類的内部維護一張表,以知道“自己”是什麼,和“自己”有關系的類是哪些,...;同時,提供給外界一個可以查詢這些資訊的接口。要做到這個,必須解決兩個問題:我怎樣獲得這個公共的接口,以及我用什麼方法來辨別一個類。在标準C++ 中,我們通過運算符 typeid 來獲得一個 const type_info&。在MFC 中,因為所有支援動态類型資訊的類都從 CObject及其子類繼承,是以我們在 CObject中提供這些方法。這樣,第一個問題對它們而言都解決了。在标準 C++

中,表示一個類的動态資訊就是那個type_info,你可以比較它,判斷它,或者擷取一個沒有确定含義的名字,但是你不能用它來做更多事情了。在MFC中,使用一個字元串來辨別一個類,你可以通過一個字元串來動态的獲得它所表示的類的類型資訊。由于使用簡單的字元串非常容易發生沖突,COM使用的是一個 128 位的随機整數 GUID ( Global Unique Identifier),這樣的随機整數被認為是不太可能發生沖突的。IUnknown中的另一個方法就是:

HRESULT QueryInterface( REFIID iid,void** Interface );

       這個 REFIID就是對于一個接口的唯一辨別,我們也成為接口ID。對一個接口指針調用QueryInterface,來詢問另一個接口。如果它支援這個接口,會傳回給你一個指針,否則會傳回一個錯誤。

一些約定

  • 任意一個 COM 接口都必須從 IUknown 接口派生
  • 對任意一個接口 QueryInterface的結果,必須支援自反性,傳遞性,可逆性和持久性。也就是說:對一個interface 詢問它本身必須成功

        如果對類型為 A 的接口 a 詢問接口類型 B 并且成功地傳回了指向 B的指針 b,那麼對 b 再詢問 A,一定能成立。

        如果對類型為 A 的接口 a 詢問接口類型 B 并且成功地傳回了指向 B的指針 b,同時從 b 詢問到了接口 C 的指針 c,那麼必須能夠成功的從a 詢問到接口 C 的指針。請注意,這個 C 的指針并不一定和前面那個 C的指針相同。(也就是說,這個約定隻保證詢問成功,但是不保證傳回的指針相同)

       如果對接口a 詢問接口 B 曾經成功過,那麼以後的每次詢問也将成功。

  • 如果你要傳一個接口給函數,那麼應該是你保證這個接口在函數傳回前的生命期有效。如果一個函數傳回一個接口指針,那麼必須在傳回前AddRef,同理,如果你從一個函數獲得了一個接口指針,那麼不需要再AddRef 了,但是用完後,應該 Release。
  • 特别的,根據上一條,你對一個接口調用了 QueryInterface以後,這個接口傳回前已經被 AddRef 了。
  • 向同一個接口指針多次查詢另一個接口,每次傳回的指針不一定一樣,但是,如果你查詢的接口ID 是 IID_IUnknown,那麼每次傳回的指針都相同。這種說法等價于,指向IUnknown 的指針是一個 COM對象的辨別,也就是說,比較兩個接口是否屬于同一個對象的唯一通用方法是對比從中Query 出來的 IUnknown 接口。此外,需要注意雖然每個 COM 接口都是從IUnknown 派生的但是你不能簡單地通過把一個接口指針賦給一個IUnknown 的指針來獲得一個類辨別,盡管這在 C++ 中是合法的。
  • 引用計數是基于接口的,是以這段代碼可能有問題

       IInterface1* p1 = getObj();

       IInterface2* p2;

       p1->QueryInterface( IID_Interface2, (LPVOID*)&p2);//假設成功

       p1->Release();

      p1->func();//這個時候,p1 可能無效!!雖然這個對象有效

      p2->Release();

3. IDL

       IDL 就是接口定義語言( Interface Definition Language)。上面介紹中說明了,COM中所有的功能都是通過接口來展現的,作為一個二進制标準,需要有一個通用的方法去描述接口。C++雖然強大,但是不适合做這件事,首先,它過于複雜,其次,它的很多資料類型别的語言不一定支援。需要有一個中立的語言來定義整個接口的資料交換情況(我們并不需要用它來定義接口的功能,因為這是依賴于具體實作的。)IDL就是這種語言。可以使用 IDL 來描述一個 COM 對象或者是 COM接口的屬性:這個函數接受什麼參數,每個參數的流向,...。IDL 比 C++占優勢的另一點是,它比較簡單,可以用工具來處理,生成的二進制資訊,可以用來完全描述你的接口的外部特性。

     其實在别的 ORB 系統中,通常是把 IDL作為接口的原始定義語言。譬如說,CORBA 中,先用 IDL描述接口,然後使用某些程式轉換成一個 C++ 定義,并且在這個 C++定義上繼續實作接口的功能,這稱為從 IDL 到 C++ 的映射(mapping)。MS的 MIDL 編譯器也可以實作同樣的功能,但是概念上不一樣。在 COM中,脫離 IDL 直接用 C++實作一個接口,是合法行為,并且是初的常用行為;在 CORB中,可以自己寫一個 C++映射,實際上是一種取巧行為。