程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 接觸VC之四:COM組件模型基礎

接觸VC之四:COM組件模型基礎

編輯:關於VC++

一年又一年,已經又過了一年了。我VC的生涯已經兩歲了。可以相當地慶賀一下喲。回顧這一年的學習(唉,還沒有工作實踐呢。這年頭,工作不好找哇。),還學了不少的好東西。其中,最重要的就是COM組件模型,我個人覺得這個幾乎是Windows的核心。許多先進的技術(比如微軟著名的DirectX,ADO,沒有人會不知道吧)都以COM組件的形式發布的。現在,我瞄上了另一個好東東,就是泛型編程技術。它能夠編寫出清晰、靈活、高度可重用的代碼,在ATL中就可以依稀看出它的影子(現在網上ATL文章有很多,我以後也會談到它)。好了,關於泛型編程的事今後再談。

按照我以前的計劃,我應該談談我對COM組件模型的認識了。一來可以對自己的學習狀況進行總結。二來,請教高手,可以幫忙指出錯漏之處。三來,說不定會對初學者們有所幫助。請各位高手多多指正啊.在這裡先謝了。

一、動態鏈接庫:

動態鏈態庫是大部分COM組件的承載對象(不要在意ocx,它同樣也是dll,只不過改了一下後綴而已)。當然Exe同樣也是可以的(TTS中的TextToSpeech對象就是一個例證),只不過在事實上要少得多。

在Windows初期,動態鏈態庫的出現是一場革命。它改變了Windows的一生,也為當今Windows操作系統的霸主地位打下一塊堅實的基石。(關於Windows的歷史問題,我一直沒有弄得太清楚。請VCKBASE的有關史學家們盡快寫出一篇文章來吧^_^)。

微軟對動態鏈接庫就是這樣解釋的:

動態鏈接庫 (DLL) 是作為共享函數庫的可執行文件。動態鏈接提供了一種方法,使進程可以調用不屬於其可執行代碼的函數。函數的可執行代碼位於一個 DLL 中,該 DLL 包含一個或多個已被編譯、鏈接並與使用它們的進程分開存儲的函數。DLL 還有助於共享數據和資源。多個應用程序可同時訪問內存中單個 DLL 副本的內容。

嗯,講得很清楚。動態鏈接庫首先是一個可執行文件(微軟解釋說,exe叫做起直接可執行文件),它裡面包含著一組需要共享的函數。當使用時,動態鏈接庫(和Windows系統)會提供一個方法來使我們的應用程序可以調用其中的函數。此外,動態鏈接庫還會包含一些資源(如:圖標、對話框模板等等)。在MFC中,微軟在現有動態鏈接庫的基礎上施用了一些技巧來提供一些另外功能,如MFC類的導出。

動態鏈接庫的鏈接方式大致分為兩類: 靜態鏈接和動態鏈接.

靜態鏈接又叫隱式鏈接,這種鏈接方式使我們在代碼中不用語句來指示系統中,我們的應用程序要加載哪些動態鏈接庫。其靜態鏈接聲明是放在工程屬性中的(或者使用#pragma comment(lib,"XXX.lib"),這個可以和#include放在一起)。在指定時,只需要輸入其動態鏈接庫相應的導入庫文件(.lib)。然後,你就可以在程序的任何地方像調用普通函數一樣調用該動態鏈接庫中存在的函數了(當然,你需要包含其相應的頭文件。一般情況下,頭文件會和LIB文件一塊給出)。通過這種方法生成的程序在運行初始化的時候(具體到什麼時候不太清楚。但我可以肯定是在WinMain函數之前了^_^),會自動將動態鏈接庫加載在系統環境中,並將其映射到我們應用程序的進程當中去。當我們調用一個我們進程沒有定義的函數時,VC運行庫會通過查找LIB文件的相關信息找到相應動態鏈接庫的函數並調用它。進程結束時,系統會缷載動態鏈接庫。

動態鏈接又叫顯式鏈接,顧名思義這種方式讓我們必需在代碼通過調用API來顯式地加載動態鏈接庫。COM組件模型全部都是采用這種方式來加載進程內組件模塊(就是Dll)的。(我覺得微軟的專業術語有些混亂耶)。這個方式有許多好處,它可以在運行時決定具體要加載哪個鏈接庫,要調用哪個函數…這才叫動態鏈接呢。

要使用動態鏈接庫並不難,首先要調用LoadLibrary,其原型如下:

HMODULE LoadLibrary(
 LPCTSTR lpFileName  // file name of module
);

參數lpFileName是要加載的動態鏈接庫的文件名。如果加載成功的話,就返回其句柄。否則的,返回NULL。

與這個API相配對的是FreeLibrary,其原型如下:

BOOL FreeLibrary(
 HMODULE hModule  // handle to DLL module
);

這個就不用我多說了吧。

當動態鏈接庫被LoadLibrary所加載時,C運行庫通過_DllMainCRTStartup來完成動態鏈接庫的初始化,如全局對象(變量)、靜態成員變量的生成以及賦初值。最重要的是它還會調用DllMain函數。每一個動態鏈接庫都必須有這個函數,就像應用程序必須有main或WinMain一樣。它的原型是:

BOOL WINAPI DllMain(
 HINSTANCE hinstDLL, // handle to the DLL module
 DWORD fdwReason,   // reason for calling function
 LPVOID lpvReserved  // reserved
);

你可以通過DllMain函數來完成你的動態鏈接庫中的環境初始化和析構操作。啊,事情是這樣的:

DllMain被調用有四種情況,這四種情況可以從fdwReason參數來分別出來:

它們分別是

1. DLL_PROCESS_ATTACH,當動態鏈接庫被加載到進程時,調用DllMain。

2. DLL_THREAD_ATTACH,當進程建立一個新線程時,進程會調用所以已加載了的動態鏈接庫的DllMain。

3. DLL_THREAD_DETACH,當一個線程結束時,進程會調用所以已加載了的動態鏈接庫的DllMain。

4. DLL_THREAD_DETACH,當動態鏈接庫被缷載或進程結束時,調用DllMain。

這樣,通過DllMain函數就可以反應出一個動態鏈接庫的生命周期了。

當加載成功後,我們會得到一個HMODULE句柄。這個句柄的使用與HINSTANCE應用程序實例的句柄很相似(追查定義,HMODULE就是HINSTANCE)。我們可以使用下面一些API函數來使用HMODULE句柄:

LoadBitmap、LoadIcon、LoadString、…、GetProcAddress等等

其中,最重要的就是GetProcAddress。它是用來返回鏈接庫中的某個函數的函數指針,然後我們就可以通過這個函數指針來調用這個鏈接庫函數了。(如果你對函數指針不熟的話,最好再看一看C\C++語法書。我覺得函數指針的聲明方法很怪異)其原型如下:

FARPROC GetProcAddress(
 HMODULE hModule,  // handle to DLL module
 LPCSTR lpProcName  // function name
);

啊,hModule我就不說了。lpProcName參數是一個字符串,這個字符串寫著我們要找的函數的函數名。如果找到了的話,就返回這個函數的指針,否則返回NULL。

舉個例子:

比如說有個鏈接庫函數是”int Plus(int nAugend, int nAddend)”,我要調用它。

HMODULE hMathLib = LoadLibrary("Math.dll");
int (*MyProc)(int, int) = NULL;
int x = 1, y = 1;
MyProc = (int(*)(int, int)) GetProcAddress(hMathLib, "Plus");
If (MyProc != NULL)
{
  printf ("%d", (*MyProc)(x, y));
}
FreeLibrary(hMathLib);

如果我以及這個鏈接庫沒有問題的話,我想輸出結果應該是2。

我仍然認為函數指針的聲明很怪異,可讀性並不高,所以我一般會換一種寫法。

#define DefMathProc(name) int (*name)(int, int)
#define FUNCTION(name) (*name)
DefMathProc(MyProc) = NULL;
MyProc = (DefMathProc()) GetProcAddress(hMathLib, "Plus");
nResult = FUNCTION(MyProc)(x, y);

雖然會出一個警告,但我覺得這樣會舒服一些。

嗯,動態鏈接庫的情況就基本如此了。具體動態鏈接庫的編寫會和COM組件的編寫一塊在後續章節裡談及。

二、面向對象的組件模型-COM

Windows系統霸主地位诂計三四年內是不會被動搖的。因此,有n多Windows開發平台出現在我們面前。n多種開發語言是百花齊放啊。於是,我們像聖經裡說的那樣,操著不同的語言,彼此無法溝通。為改變這一現實,可愛的比爾就站出來了,”偶要改變世界!”。微軟公司制定一個基於二進制通用接口規范-Component Object Model(組件對象模型)。但是,一開始COM的解決目標並非是為了通用接口,而是應用於復合文檔(OLE)的實現。而今由於語言無關性、進程透明性、可重用性、保密性(除非高手高手高高手,有誰能從匯編碼中看出實現技術來)、而且編寫並不困難,所以發展成為了一項應用廣泛的技術。

1) 組件對象與接口

組件對象、接口是COM的根基。

下面,請允許我用C++對象做一個類比。

組件對象與C++對象的意義是基本相同的。它是一個功能、屬性與邏輯的整體。它是一個實體對象,通過對它的接口操作,可以使用它所提供的功能。

接口相當於C++對象中的public成員。它被暴露給外部使用者,使用者只被允許調用這些被暴露在外面的接口來使用對象的功能。與public成員有所不同的是,接口不是一個變量也不是一個函數,而應該是一組函數。在邏輯上,這個組函數應該是功能相關的。一個組件對象可以擁有許多個接口。

我只知道C++的COM實現方法,至於Dephi我就一無所知。

C++實現方法是:由C++類對象來完成組件對象的實現,由C++純虛類來代表接口。C++類對象通過多重繼承多個接口,來的擁有多個接口。

下面,我舉一個例子,來說明C++中的組件對象與接口的關系(下面的例子並不是一個COM實現,只是用來表示組件對象與接口的關系)

我如果要做一個人的組件對象的話,我首先要定義一些接口來表示人的外部表現行為。

class physiology
{
public:
    virtual void eat(Food in) = 0;
    virtual void drink(Liquid in) = 0;
    virtual Somethings toilet() = 0;
};
class psychics
{
public:
    virtual Sound laugh() = 0;
    virtual Sound cry() = 0;
    virtual Sound angry() = 0;
};
class dynamics
{
public:
    virtual Speed run() = 0;
    virtual Speed walk() = 0;
    virtual Interval jump() = 0;
};

我將人的行為分成了生理學、心理學和動力學三類,讓它們分別表示人不同的行為。那麼,這麼三組相關函數就是三個接口。C++組件對象的實現就是從這些接口中多重派生,並實現它們。這樣,我們就得到一個組件對象(聲明啊,本示例只是一個表示概念,真正的COM組件對象還需要加一些東東)。

class human :
    public physiology,
    public psychics,
    public dynamics
{
public:
    void eat(Food in)
{
  cout << "Good! Very delicious!";
}
void drink(Liquid in)
{
  cout << "No! I am not drunk!";
}
Something toilet()
{
  cout << "hum…….";
  return dejecta();
}
Sound laugh()
{
  return Sound("Ha…Ha…Ha");
}
Sound cry()
{
  return Sound("dad!Don’t beat my buns.");
}
Sound angry()
{
  return Sound("where did you go last night? Darling.");
}
Speed run()
{
  cout << "Run, Police come!";
  return 20km/h;
}
Speed walk()
{
  cout << "out. yegg, I am no…not afraid o….of y…you.";
  return 1m/s;
}
Interval jump()
{
  cout << "Yeah….";
  return 4m;
}
};

這樣,一個組件對象就定義完了。當使用組件對象時,系統所給予你的一個指針。它是一個組件對象實現了的虛類指針,我們可以使用它來調用組件對象對於這個純虛類所實現的功能(當然,我們有選擇什麼虛類指針的權利;只要組件對象支持就可以了)。

總之,一個組件對象外部特征是由不同的接口也就是這些虛類所組成,它們向使用者展現組件所提供的功能。

注:如果你的C++虛函數沒學得不太好的話,那麼請找一本C++語法書再看一看. 或請參看VCKBASE第12期的《解析動態聯編》。

2) 標識符(GUID)

上面,我說過COM組件是基於二進制的。那麼要我們使用簽名(比如說類名、接口名)來指定一個組件顯然是不理想的了(至少在識別方面會有些麻煩)。那麼,既然是二進制系統最方便當然就是使用數字標識了。於是,微軟定義了這麼一個結構標准:

typedef struct _GUID
{
    DOWRD Data1;
    WORD Data2;
    WORD Data3;
    WORD Data4[8];
}GUID;

結構用來儲存一些數字信息,來表識一個COM對象,接口以及其它COM元素。這個結構體就叫做標識符。

在C++中一個標識符是這麼表示的:

extern "C" const GUID CLISID_MYSPELLCHECKER =
{0x54bf6567, 0x1007, 0x11d1, {0xb0, 0xaa, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}}

同樣的標識符在其它非C環境中是這麼表示的:

{54bf6567-1007-11d1-b0aa-444553540000}

這個標識符代表著一個COM對象,這是因為一個COM對象的標識符名都以CLISID_為前綴。接口名則是以IID_為前綴。不要問我,標識符定義與對象具體有什麼關系式。我不知道。它們根本就沒有什麼關系的。一個COM對象在編寫時,我們會使用隨機的方法來確定它的標識符(這個工作可以由VC來幫我們搞定)。一旦COM對象得到一個標識符並發布出去的話,那麼就不能更改了。另外,不要擔心GUID會有所沖突。如果你的高中數學已經及格了的話,那麼請算一算128位二進制中,重復的概率會有多少。假如你真的發現了GUID有沖突的話(你要保證這不是人為),建議你趕去買彩票吧。你離500萬不遠了。

3) IUnknown接口

COM模式所有接口必須遵守一定規范,這就是IUnknown接口的出處。每個一接口都必須從這個接口繼承。在C++中,微軟已經為我們把IUnknown定義好了:

typedef GUID IID;
class IUnknown
{
  public:
    virtual HRESULT _stdcall QueryInterface(const IID& iid, void **ppv) = 0;
    virtual ULONG _stdcall AddRef() = 0;
    virtual ULONG _stdcall Release() = 0;
};

注:void *可以指向任何對象。我開始的時候對void*一點都不理解。這裡使用的原因是傳出與傳入指針類型不確定。

QueryInterface函數功能是當我們得到一個接口指針,並且我們想得到另一個接口指針的時候,提供幫助。我們將我們想要得到的接口的標識符傳給iid,將把指針的做一個次&來傳給ppv。如果QueryInterface成功的話,會返回S_OK。我們指針中就會指向我們想要的接口。

AddRef,Release用於實現引用計數機制。

在二進制系統中,組件對象不像C++環境中對象那樣具有明確的生存期。可能會出現這種情況,兩個(或者兩個以上)的地方(可能是不同的程序之間,也可能是不同的線程之間)同時使用著一個組件對象,如果其中一個地方delete掉了組件對象的話。其它地方不可能會知道,當它們嘗試調用這個象的話,輕則導致重傷,重則導致死亡。這不是我們希望看到的。於是,COM模型設制一個引用計數機制。

當一個地方開始使用對象的時候,它必須調用AddRef()一次。當我們使用QueryInterface時候,QueryInterface必須為我們調用一次AddRef()。AddRef()會使組件對象的引用計數增1。當這個地方不再使用對象時,它必須調用Release()一次。Release()會使組件對象的引用計數減1。當組件對象的引用計數變成0,就表明沒有人再去使用組件對象了。這時,組件對象應該結束自己的生命。這樣,就保證了組件對象生存期間其它程序的安全。

當然,你可以使用自己的引用機制,只要你的行為上支持AddRef和Release。比如說,不設置對象的引用計數,而是為每個接口設置一個引用計數。當所有的接口引用計數都為0時,delete對象。

好了,前面的示例中,我並沒有遵守IUknown規范,下面我要遵守它。我把上次同樣東西用……省略掉了。

// {6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5}
static const GUID IID_IPhysiology =
{ 0x6aaf876e, 0xfced, 0x4ee0, { 0xb5, 0xd3, 0x63, 0xcd, 0x6e, 0x22, 0x42, 0xf5 } };
class IPhysiology:
    public IUnknown
{
public:
    ……
};
// {183FC7A1-4C27-4c38-B72D-D1326E2E8A7C}
static const GUID IID_IPsychics =
{ 0x183fc7a1, 0x4c27, 0x4c38, { 0xb7, 0x2d, 0xd1, 0x32, 0x6e, 0x2e, 0x8a, 0x7c } };
class IPsychics:
    public IUnknown
{
public:
    ……
};
// {5F144D5C-A20C-42e7-8F91-4D5CAE430B29}
static const GUID IID_IDynamics =
{ 0x5f144d5c, 0xa20c, 0x42e7, { 0x8f, 0x91, 0x4d, 0x5c, 0xae, 0x43, 0xb, 0x29 } };
class IDynamics:
    public IUnknown
{
public:
    ……
};
// {ABFA7022-7E2F-4d0e-8A4F-F58BBCEBB2DA}
static const GUID CLISID_Human =
{ 0xabfa7022, 0x7e2f, 0x4d0e, { 0x8a, 0x4f, 0xf5, 0x8b, 0xbc, 0xeb, 0xb2, 0xda } };
class human :
    public IPhysiology,
    public IPsychics,
    public IDynamics
{
public:
……
human()
{
  m_ulRef = 0;
}
HRESULT QueryInterface(const IID& iid, void **ppv)
{
  if (iid == IID_IUnknown || iid == IID_IPhysiology)
  {
    *ppv = static_cast<IPhysiology*>(this);
    (IPhysiology*)(*this))->AddRef();
  }
  else if (iid == IID_IPsychics)
  {
    *ppv = static_cast<IPsychics*>(this);
    (IPsychics*)(*this))->AddRef();
  }
  else if (iid == IID_IDynamics)
  {
    *ppv = static_cast<IDynamics*>(this);
    (IDynamics*)(*this))->AddRef();
  }
  else
  {
    *ppv = NULL;
    return E_NOTINTERFACE;
  }
  return S_OK;
}
ULONG AddRef()
{
  return ++m_ulRef;
}
ULONG Release()
{
  m_ulRef--;
  if (m_ulRef <= 0)
  {
    m_ulRef = 0;
    delete this;
  }
  return m_ulRef;
}
ULONG m_ulRef;
};

這樣我們的組件對象就定義完全了。

下面給出我們這個組件對象的IDL描述和圖形描述

#include "olectl.h"
import "oaidl.idl";
import "ocidl.idl";
[
     object,
     uuid(6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5),
     nonextensible,
     helpstring("IPhysiology 接口"),
     pointer_default(unique)
]
interface IPhysiology : IUnknown
{
     void eat(Food in);
     void drink(Liquid in);
     Somethings toilet();
};
[
     object,
     uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29),
     nonextensible,
     helpstring("IPsychics 接口"),
     pointer_default(unique)
]
interface IPsychics : IUnknown
{
     Sound laugh();
     Sound cry();
     Sound angry();
};
[
     object,
     uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29),
     nonextensible,
     helpstring("IDynamics 接口"),
     pointer_default(unique)
]
interface IDynamics : IUnknown
{
     Speed run() = 0;
     Speed walk() = 0;
     Interval jump() = 0;
};
[
     uuid(6CC7B329-B92F-4A8F-9CDD-1AB6D7E4CF4D),
     version(1.0),
     helpstring("OLEOBJECT 1.0 類型庫")
]
library OLEOBJECTLib
{
     importlib("stdole2.tlb");
     [
       uuid(62FD0E39-DA84-4B19-BAB0-960A27AC2B71),
       helpstring("OlePaint Class")
     ]
     coclass OlePaint
     {
       [default] interface IPhysiology,
       interface IPsychics,
       interface IDynamics
     };
};

請伃細,觀察上面的描述IDL代碼和圖形。並不是太難吧。

4) COM對象的接口原則

為了規范COM的接口機制,微軟向COM開發者發布了COM對象的接口原則。

(1)IUnknown接口的等價性

當我們要等到兩個接口指針,我如何判斷它們從屬於一個對象呢。COM接口原則規定,同一個對象的Queryinterface的IID_IUnknown查詢出來的IUnknown指針值應當相等。也就是說,每個對象的IUnknown指是唯一的。我們可以通過判斷IUnknown指針是否相等來判斷它們是否指向同一個對象。

IUnknown *pUnknown1 = NULL, *pUnknown2 = NULL;
pObjectA->QueryInterface(IID_IUnknown,(void **) &pUnknown1);
pObjectB->QueryInterface(IID_IUnknown,(void **) &pUnknown2);
if (pUnknown1 == pUnknown2)
{
    cout << “I am sure ObjectA is ObjectB.”;
}
else
{
    cout << “I am sure ObjectA is not ObjectB.”;
}

當然,如果查詢的不是IUnknown接口,則無此限制。同一對象對非IUnknown接口的查詢值可以不同。

(2)接口自反性,對一個接口來說,查詢它本身應該是允許的。

設pPsychics是已賦值IPsychics的接口。

那麼pPsychics->QueryInterface(IID_IPsychics,(void **) &XXX);應當成功。

(3)接口對稱性,當我們從一個接口查詢到另一個接口時,那麼我們再從結果接口還可以查詢到原來的接口。

例如:

IPsychics *pSrcPsychics = …something, *pTarget = NULL;
IDynamics *pDynamics = NULL;

如果pSrcPsychics->QueryInterface(IID_IDynamics,(void **) &pDynamics);成功的話。

那麼pDynamics->QueryInterface(IID_IPsychics,(void **) &pTarget);也相當成功。

(4)接口傳遞性。如果我們從第一個接口查詢到了第二個接口,又從第二個接口查詢到了第三接口。則我們應該能夠從第三個接口查詢到第一個接口。其它依此類推。

(5)接口查詢時間無關性。當我們在某時查詢到一個接口,那麼在任意時刻也應該查詢到這個接口。

嗯,COM的基本知識好像這麼多了。好像片篇太長呵。那麼COM實現方法留到下一篇吧。

(待續...)

作者信息:

釋雪

MSN Messenger:[email protected]

QQ:63068279

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved