下載例程-17.6kb
內容:
簡介
COM 對象和虛表
GUID
QueryInterface(), AddRef(), and Release()
IClassFactory對象
打包到DLL中
C++/C包含文件
定義文件( DEF)
安裝DLL,注冊對象
C實例程序
C++實例程序
修改代碼
接下來是什麼?
簡介:
有大量例子教大家如何使用和創建COM/OLE/ActiveX組件,但這些例子大都使用了微軟基礎類(MFC)、.NET、C#、WTL,至少會使用了ATL,因為這些框架會提供給你一些已經“封裝” 好了的模板代碼。不幸的是,這些框架對程序員隱藏了所有底層細節,所以你永遠不會真正明白使用COM組件的本質。更好的掌握如何使用一個特定的框架是建立在你熟練掌握COM的基礎上。
如果你正嘗試不使用MFC、WTL、.NET、ATL、C#、或者甚至一些C++代碼,只使用標准的C來處理COM對象,則這方面的例子和資料非常匮乏。本文就是介紹在不使用其他框架,只使用標准C創建COM組件文章系列的第一部分。
對於標准的Win32控件,例如Static、Edit、ListBox、ComboBox等,你可以獲得一個控件的句柄(也就是一個HWND)並通過發消息(通過 SendMessage)給它來操縱它。同時當這個控件要通知你一些事情或給你一些數據時,它也通過消息的形式返回給你(也就是通過把它們放入你自己的消息隊列中,你再通過GetMessage獲取他們)。
對於一個OLE/COM對象而言則完全不是這樣。你不能來回發消息,取而代之的是,COM對象給你一些可以調用來操縱這個對象的一些函數指針。例如,一個IE浏覽器對象會給你一 個函數指針,通過這個指針你可以調用來引發浏覽器在你的一個窗口中去加載並顯示Web頁面。一個Office的對象會給你一個函數指針,你可以通過它加載一個文檔。如果COM對象要通知你一些事情或發給你一些數據,那麼你需要在你的程序中寫特殊的函數,提供這些函數的指針(給COM對象)以便對象在需要的 時候調用它們。換句話說你需要在你的程序中創建你自己的COM對象。其中在C中真正麻煩是怎麼定義你自己的COM對象。為了這樣做,你需要知道一個COM對象的每個細節-這些原始的東西在預制的框架中對你而言則是隱藏的,在接下來的一系列文章中我將展示它。
總之,你通過調用COM對象中的函數來操縱它,同時它在你的程序中也是通過函數調用來通知你一些事情或傳給你一些數據或通過其他方式與你的程序交互。這個方法類似於DLL中的函 數調用,就像在你的C程序中DLL也要調用函數一樣-有幾分像“回調”。但是與DLL不同的是,你不能用LoadLibrary()和GetProcAddress()去獲得這個COM對象的函數指針。我們馬上就會揭示它,你需要使用一個與之不同的操作系統函數來獲得一個對象指針,然後 用這個對象去獲得指向它的函數的指針。
COM對象和虛表
在學習怎樣使用COM對象之前,我們首先需要了解一下COM對象是什麼。認識它的最好的方式是創建我們自己的COM對象。但在我們這樣做之前,讓我們給出一個C結構數據類型。作為一個C程序員,你應該對它相當熟悉。這是一個例子的定義,一個簡單的結構(叫“IExample”),它包含兩個成員-一個DWORD(通過“count” 成員名來存取)和一個80個字符長度的數組(通過“buffer” 成員名來存取)。
[cpp]
DWORD count;
char buffer[80];
};
讓我們用typedef來使它可以提前使用:
[cpp]
typedef struct {
DWORD count;
char buffer[80];
}IExample;
接下來是一個對於這個結構分配一個實例的例子(忽略了錯誤檢查),同時初始化它的成員:
[cpp]
IExample * example;
example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
example->count = 1;
example->buffer[0] =0;
你知道一個結構可以存儲一個指向函數的指針嘛?希望你知道,這是個例子。我們有一個參數是char*的函數,返回值是個long。這是我們的函數:
[cpp]
long SetString(char *str)
{
return(0);
}
現在我們需要把這個指向這個函數的指針存儲在IExample中。在這裡我們定義IExample,添加一個成員(“SetString”)來存儲指向上面的函數的指針(並且我也用了一個typedef來使它提前可用):
[cpp]
typedef longSetStringPtr(char *);
typedef struct {
SetStringPtr * SetString;
DWORD count;
char buffer[80];
} IExample;
接下來是我們在分配的IExample中給SetString指針賦值,然後用這個指針調用來調用SetString:
[cpp]
example->SetString =SetString;
long value =example->SetString("Some text");
好,可能我們需要存儲兩個函數指針。這是第二個函數:
[cpp]
longGetString(char *buffer, long length)
{
return(0);
}
讓我們重新定義IExample,添加另一個函數成員(“GetString”)來存儲指向第二個函數的指針:
[cpp]
typedeflong GetStringPtr(char *, long);
typedef struct {
SetStringPtr * SetString;
GetStringPtr * GetString;
DWORD count;
char buffer[80];
} IExample;
接下來我們初始化這個成員:
[cpp]
example->GetString= GetString;
但你可能會說我不想把函數指針直接存儲在IExample中。相反的,我們更願意使用一個函數指針數組。例如,讓我們定義第二個結構來達到存儲我們的兩個函數指針的目的。我們將叫它IExampleVtbl結構,它的定義是這樣的:
[cpp]
typedef struct {
SetStringPtr * SetString;
GetStringPtr * GetString;
} IExampleVtbl;
現在,我們把指向上面的數組的指針存儲在IExample中。我們要添加一個叫“lpVtbl”的新成員來達到這個目的(當然,我們得刪除SetString和GetString成員,因為他們已經挪到IExampleVtbl結構中了)
[cpp]
typedef struct {
IExampleVtbl * lpVtbl;
DWORD count;
char buffer[80];
} IExample;
所以下面是一個分配內存並初始化IExample的例子(當然,包括IExampleVtbl):
[cpp]
// 由於IExample_Vtbl的內容永遠不會改變,
// 所以我把它定義為靜態的並且用以下方法初始化它。
// 它可以被大量的IExample實例復制。
static const IExampleVtblIExample_Vtbl = {SetString, GetString};
IExample * example;
// 創建 (分配內存) 一個IExample結構.
example = (IExample*)GlobalAlloc(GMEM_FIXED, sizeof(IExample));
// 初始化IExample(也就是把指向IExample_Vtbl賦值給它).
example->lpVtbl =&IExample_Vtbl;
example->count = 1;
example->buffer[0] =0;
接著可以這樣調用我們的函數:
[cpp]
char buffer[80];
example->lpVtbl->SetString("Sometext");
example->lpVtbl->GetString(buffer,sizeof(buffer));
此外需要說明的是,在我們的函數中可能需要通過訪問結構中的“count”和“buffer”成員來調用他們。所以我們要做的是總要把指向這個結構的指針作為第一個參數傳入。讓我們重寫我們的函數來達到這一點:
[cpp]
typedeflong SetStringPtr(IExample *, char *);
typedeflong GetStringPtr(IExample *, char *, long);
longSetString(IExample *this, char * str)
{
DWORD i;
// 把傳入的str拷貝到IExample的buffer中
i = lstrlen(str);
if (i > 79) i = 79;
CopyMemory(this->buffer, str, i);
this->buffer[i] = 0;
return(0);
}
long GetString(IExample*this, char *buffer, long length)
{
DWORD i;
// 拷貝IExample的buffer到傳入的buffer中
i = lstrlen(this->buffer);
--length;
if (i > length) i = length;
CopyMemory(buffer, this->buffer, i);
buffer[i] = 0;
return(0);
}
當調用IExample結構的函數時把它的結構指針傳入:
[cpp] view plaincopy
example->lpVtbl->SetString(example,"Some text");
example->lpVtbl->GetString(example,buffer, sizeof(buffer));
如果你曾經用過C++,你可能認為:等一下,它好像很眼熟啊。是的,我們上邊做的就是用標准C來創建一個C++類。IExample結構實際上是一個C++類(一個不繼承於 其他任何類的類)。一個C++類實際上除了第一個成員總是一個數組指針,這個數組包含所有類成員函數的指針,與結構沒什麼差別。並且每個函數的第一個參數總是類(也就是結構)本身的指針。(它也就是隱藏的this指針)
簡單說來,一個COM對象實際上就是一個C++類。你現在可能會認為:“哇噻!IExample現在就是一個COM對象嘛?這就是它的全部嘛??它就這麼簡單!”打住!IExample正在接近這一點,但對於它還有很多,它不會這麼容易。如果它是這樣,它就不會是微軟技術了,現在做什麼?
首先,讓我先來介紹一下COM術語。你看到上面的指針數組-IExampleVtbl結構了嘛?COM文檔中把它定義為接口或虛表。
一個COM對象在虛表(也就是我們的IExampleVtbl結構)中首先需要有三個被命名為QueryInterface、AddRef和Release的函數。 當然,我們也必須寫這三個函數。微軟已經把這三個函數的調用參數,返回值和調用約定指定好了。我們需要#include一些微軟的包含文件(他們在你的C編譯器包中,或者你下載的微軟的SDK中)。我們這樣重新定義我們的IExampleVtbl結構:
[cpp]
#include<windows.h>
#include<objbase.h>
#include<INITGUID.H>
typedefHRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **);
typedef ULONGSTDMETHODCALLTYPE AddRefPtr(IExample *);
typedef ULONGSTDMETHODCALLTYPE ReleasePtr(IExample *);
typedef struct {
// 前3個成員必須叫是QuryInterface、AddRef和Release
QueryInterfacePtr *QueryInterface;
AddRefPtr *AddRef;
ReleasePtr *Release;
SetStringPtr *SetString;
GetStringPtr *GetString;
} IExampleVtbl;
讓我們查看typedef過的QueryInterface。首先,這個函數返回一個HRESULT,它被簡單定義為LONG。接著,它用了STDMETHODCALLTYPE。這意味參數不通過寄存器傳遞,而是通過棧。並且也約定了誰來平棧。事實上,對於COM對象,我們應該確保所有我們的函數都被定義為STDMETHODCALLTYPE,並返回一個LONG(HRESULT)。QueryInterface的第一個參數是用於函數調用的對象指針。我們難道不是在把IExample轉化為一個COM對象嘛?是 的,這也是我們要傳遞的參數的原因。(記住確保傳遞給我們函數的第一個參數是一個用於調用這些函數的結構指針?COM完全強制依賴以上的定義)
稍後,我們展示一個REFIID是什麼,並且也提到QueryInterface的第三個參數,注意AddRef和Release也傳遞同樣的我們用於調用他們的結構指針。
好,在我們沒有忘記前,讓我們添加HRESULTSTDMETHODCALLTYPE到SetString和GetString:
[cpp]
typedef HRESULTSTDMETHODCALLTYPE SetStringPtr(IExample *, char *);
typedef HRESULTSTDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);
HRESULT STDMETHODCALLTYPESetString(IExample *this, char * str)
{ ...
return(0);
}
HRESULT STDMETHODCALLTYPEGetString(IExample *this, char *buffer, long value)
{
...
return(0);
}
總之,一個COM對象基本上是一個C++類。這個C++類是一個總是以它的虛表指針(一個函數指針數組)為起點的結構。並且在虛表中最開始的三個函數總是被命名為QueryInterface、AddRef和Release。額外的函數也可以出現在虛表中,它們的名字依賴對象它自身的定義。(你決定要加入你的 COM對象中的函數)。例如,IE的Browser對象勿庸置疑有與播放音樂對象不同的函數。但是所有的COM對象都以它們的虛表指針開始,最開始的三個虛表指針指向對象的QueryInterface、AddRef、和Release函數。一個對象的函數的第一個參數是一個指向對象(結構)自身的指針。 這是一個約定,一定要遵守。
GUID
讓我們繼續我們的構造IExample為一個真正的COM對象之旅。現在要寫我們的QueryInterface、AddRef和Release函數。但在我們動手之前,我們必須談談一個叫全 局唯一表示符(GUID)的東東。哦,它是什麼?它是一個用特殊的一連串字節填充的16字節數組。當我說它是特殊的時候,我的意思是唯一。一個GUID(也就是16字節數組)不能與另一個GUID有同樣的字節序列,無論何時何地。每個GUID在任何時候被創建都有唯一的16位序列數。
那麼你怎樣創建這個唯一的16位序列呢?你可以用一個微軟的GUIDGEN.EXE工具。它打包在你的編譯器中,或者你也可以在SDK找到它。運行它你會看到這個窗口:
當你一運行GUIDGEN時,它自動生成一個新的GUID給你,顯示在Result框中。注意在你的Result框中看到的會與上面的有所不同。畢竟,每個 單一的GUID生成與其他的是不同的。所以你最好看到一些與我看到的不同的東東。繼續單擊“NewGUID”按鈕會看到一些不同的數字出現在Result框中。單擊一整天,看看是否會生成同一個序列數超過一次,不會。同時,也沒人會生成一些與你生成的序列相同的數。
你可以單擊“Copy”按鈕來把這個信息傳輸到剪切板上,然後把它粘貼到其它地方(像你的源代碼中)。這是我這樣做,粘貼完的東東:
[cpp]
//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(<<name>>,0xb5b3d8e, 0x574c, 0x4fa3,
0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce,0x24, 0xc2);
上面是一個宏,一個#define在微軟的包含文件中,它會告訴你的編譯器把上面的內容編譯成一個16位數組。
但是有一個事情我們必須做。我們必須用一些我們要用的這個GUID的C變量名來替換<<name>>。我們叫它CLSID_IExample.
[cpp]
//{0B5B3D8E-574C-4fa3-9010-25B8E4CE24C2}
DEFINE_GUID(CLSID_IExample,0xb5b3d8e, 0x574c, 0x4fa3,
0x90, 0x10, 0x25, 0xb8, 0xe4, 0xce, 0x24,0xc2);
現在我們有了一個可以用於IExample的GUID。
我們還需要一個GUID給IExample的虛表(“接口”),也就是,我們的IExampleVtble結構。所以繼續單擊GUIDGEN.EXE的“NewGUID”按鈕,並拷貝、粘貼到其他地方。這次,我們將用一個命名為IID_IExample的C變量名來替換<<name>>。下面是我粘貼、編輯過的結果:
[cpp]
//{74666CAC-C2B1-4fa8-A049-97F3214802F0}
DEFINE_GUID(IID_IExample,0x74666cac, 0xc2b1, 0x4fa8,
0xa0, 0x49, 0x97, 0xf3,0x21, 0x48, 0x2, 0xf0);
總之,每個COM對象有它自己GUID,每個GUID是由不同的16位字節數組組成。一個GUID可以通過GUIDGEN.EXE工具生成。一個COM對象的虛表(也就是接口)也得有一個GUID。
QueryInterface(),AddRef(), and Release()
當然我們要允許其他程序來獲得我們創建、初始化的IExample結構(也就是一個COM對象),那麼這個程序就可以調用我們的函數了。(我們先不給出另一個程序怎樣來獲得我們的IExample。我們將在後面討論它)。
除我們自己的COM對象以外,可能有很多其他COM組件安裝在一個特定的計算機上。(再次,我們將推後討論怎樣安裝我們的COM組件。)不同的計算機可能安裝了不同的COM組件。一個程序怎樣確定我們的IExampleCOM對象是否已經安裝了,怎樣來把它與其他所有COM對象區別開來?
記住每個COM對象有一個完全唯一的GUID,我們的IExample對象也是。我們的IExample虛表也有一個GUID。我們需要做的是告訴這個程序的開發者 IExample對象和它的虛表的GUID。通常,你給他一個包含上面你用GUIDGEN.EXE獲得的兩個GUID的宏的文件(.H)。OK,這樣其它程序就知道IExample和它的虛表的GUID。它可以用他們做什麼呢?
在這我們的QueryInterface函數派上用場了。記住每個COM對象必須有一個QueryInterface函數(也得有AddRef和Release)。其它程序會傳遞我們 IExample的虛表的GUID給我們的QueryInterface函數,我們檢查它並確認它是IExample虛表的GUID。如果它是,那麼我們會返回一些值來讓這個程序知道它確實擁有了一個IExample對象。如果傳入一個錯誤的GUID,我們會返回一些錯誤值讓它知道它沒有獲得這個 IExample對象。所以,一台計算機上的所有COM對象,除了我們自己的QueryInterface,如果傳給他們的QueryInterface一個IExample虛表的GUID都會返回一個錯誤值。
傳給QueryInterface的第二個參數是我們要檢查的GUID。如果傳入的GUID與我們的IExample的虛表GUID匹配,我們會返回給傳給我們的同一個對象指針給第三個參 數(一個句柄)。如果不匹配,我們把這個句柄置零。另外,如果這個GUID匹配QueryInterface返回一個NOERROR(被#define為0)的LONG值。如果不匹配返回非零錯誤值(E_NOINTERFACE)。那麼,讓我們來看一下IExample的QueryInterface:
[cpp]
HRESULTSTDMETHODCALLTYPE QueryInterface(IExample *this,
REFIID vTableGuid,void **ppv)
{
// 檢查GUID是否與IExample的虛表的GUID相匹配。
// 記得我們給出了一個C變量的IID_IExample對應與我們的虛表GUID。
// 我們可以用一個OLE的IsEqualIID的函數來比較
if(!IsEqualIID(riid, &IID_IExample))
{
//我們不認可傳給我們的GUID,通過清除調用這的句柄來讓它知道,
// 返回E_NOINTERFACE
*ppv = 0;
return(E_NOINTERFACE);
}
// 它是匹配的!
// 首先我們用同一個它傳給我們的對象指針來填充它的句柄。
// 這是我們創建/初始化的我們的IExample,它將從我們這裡獲的對象指針
*ppv = this;
// 現在調用我們的AddRef函數,把this指針傳給IExample
this->lpVtbl->AddRef(this);
// 讓他知道他確實擁有了一個IExample
return(NOERROR);
}
現在讓我們來討論一下AddRef和Release函數。你會注意到如果我們真的擁有了一個IExample,我們會在QueryInterface中調用AddRef.. .
記住我們替其它程序分配IExample的空間。它只是簡單獲得它的使用權。當其它程序不用它時我們有責任釋放它。我們怎樣知道這些呢?
我們會使用一個名為“引用計數”的東東。如果你回頭看看我們的IExample的定義,你會發現我放了一個DWORD成員(count)在它裡面。我們將使用這個成員。當我們創建了一個IExample時,我們把它初始化為0。然 後,當AddRef每被調用一次我們會把這個成員增加1,當Release每被調用一次我們會把這個成員減1。 所以,當把我們的IExample傳遞給QueryInterface時,我們會調用AddRef來增加它的count成員。當其它程序使用完後,這個程序會把我們的IExample傳遞 給我們的Release函數,在那我們會對這個成員進行減操作。當它減為0時我們會釋放IExample。
COM的另一個重要規則。如果你獲得了一個其他人創建的COM對象,在你使用完後你必須調用Release函數。我們當然也認為其他程序在使用完我們的IExample對象後後調用我們的Release函數。
下面是我們的AddRef和Release函數:
[cpp]
ULONGSTDMETHODCALLTYPE AddRef(IExample *this)
{
// 增加引用計數(count成員)
++this->count;
// 我們應該返回這個更新後的count。
return(this->count);
}
ULONGSTDMETHODCALLTYPE Release(IExample *this)
{
// 減少引用計數
--this->count;
// 如果它現在為0,我們要釋放IExample
if (this->count == 0)
{
GlobalFree(this);
return(0);
}
// 我們應該返回這個更新後的count。
return(this->count);
}
我們接下來還要做一些其它事情。微軟定義了一個叫IUnknown的COM對象。它是什麼呢?一個IUnknown對象就像IExample,它的虛表只包含QueryInterface、AddRef和Release 函數(也就是說,它不包含像我們的IExample虛表中的SetString和GetString一類的附加函數)。換句話說,一個IUnkown是一個空的最小的COM對象。微軟給IUnknown對象創建了一個特殊的GUID。但你知道嗎?我們的IExample對象也可以裝扮成一個IUnkown 對象。畢竟在它裡面有QueryInterface、AddRef和Release函數。如果他們關心的僅僅是前三個函數,沒人需要知道它其實是一個IExample對象。如果其它程序傳遞給我們的是IExample的GUID或者一個IUnkown的GUID,我們只要更改一行代碼返回成功就可以 了。順便說一下,微軟的頭文件中給IUnknown的GUID起了一個IID_IUnkown的C變量名:
[cpp]
//檢查這個GUID是否與IExample的GUID或IUnknown的GUID匹配
if(!IsEqualIID(vTableGuid, &IID_IExample) &&
!IsEqualIID(vTableGuid,&IID_IUnknown))
總之,對於我們自己的COM對象,我們替其它程序(他們獲得是對象的使用權,用它來調用我們的函數)分配對象的空間。我們負責釋放對象的空間。我們通過AddRef和Release 管理引用計數來達到對象安全。我們的QueryInterface允許其它程序檢查對象是否是他們需要的,也允許我們來增加引用計數。(事實上,QueryInterface還提供一個不同的功能,這個我們以後再討論,但此時,知道這些就夠了)那麼,IExample現在是一個真正的COM對象了嗎?這是肯定的!太棒了! 不太難! 我們已經做到了!
等等!我們還需要把它封裝成其它程序可以使用的形式(也就是說一個動態鏈接庫),寫一段專用的安裝程序代碼,讓其它程序知道如何獲得我們創建的IExmaple(這樣我們還要寫些代碼)。
IClassFactory對象
現在我們需要看一下一個程序如何獲得我們的IExample對象,最終我們還必須寫一些實現的代碼。對於這一點微軟已經設計了一個標准的方法,我們加入第二個COM對象(還用它的函數)到我們的DLL中。這個COM對象叫IClassFactory,它包括了一套特殊的已經被定義在微軟的包含文件中的函數,它也擁有自己的已定義的GUID,同時給出了一個IID_IClassFactory的C變量名。
我們的IClassFactory的虛表有五個特殊函數,它們是QueryInterface、AddRef、Release、CreateInstance和LockServer。注意IClassFactory有自己的QueryInterface、AddRef和Release函數,就像我們的IExample對象那樣。當然,我們的IClassFactory也是一個COM對象,所有的COM對象的虛表都必須以這三個函數開始。(但是為了避免與IExmaple的函數名字沖突,我們在我們IClassFactory函數名前加上“class”前綴,例如classQueryInterface、classAddRef和classRelease。在IClassFactory虛表定義時,它的前三個成員為QueryInterface、AddRef和Release,)
真正重要的函數是CreateInstance。只要程序要我們創建一個IExample對象、初始化對象和返回對象時,它就會調用我們IClassFactory的CreateInstance。事實上,如果程序需要幾個IExample對象,它可以調用CreateInstance幾次。好,一個程序就可以這樣獲得我們的IExample對象。你可以要問“但是其它程序怎樣獲得我們的IClassFactory對象呢?”我們一會再開始。現在,讓我們簡單寫一下我們的IClassFactroy的五個函數,構造它的虛表。構造虛表比較簡單。不像我們的IExample對象的IExampleVtbl,我們不必定義我們的IClassFactory的虛表結構。微軟已經在包含文件中為我們定義了一個IClassFactoryVtbl結構。我們需要做的是聲明我們的虛表和用我們的五個IClassFactory的函數指針來填充它。讓我們用IClassFactory_Vtbl變量名來創建一個靜態的虛表並填充它:
[cpp
static constIClassFactoryVtbl IClassFactory_Vtbl = {classQueryInterface,
classAddRef,
classRelease,
classCreateInstance,
classLockServer};
同樣的,創建一個實際的IClassFcactory對象是容易的,因為微軟也已經定義了這個結構。我們僅僅需要他們中的一個,所以讓我們用一個MyIClassFactoryObj變量名聲明一個靜態的IClassFactory,初始化它的lpVtble成員指向我們上面的虛表:
[cpp]
static IClassFactoryMyIClassFactoryObj = {&IClassFactory_Vtbl};
現在,我們只需要寫上面的五個函數。我們的classAddRef和classRelease函數沒什麼用。因為實際上我們從不要分配我們的IClassFactory(也就是說,我們簡單的把它聲明為靜態的),我們不需要釋放任何東西。所以,classAddRef只是簡單的返回一個1(指出總有一個IClassFactory存在)。ClassRelease也同樣這麼做,因為不需要釋放它,對於我們的IclassFactroy也就不需要做引用計數。
[cpp]
ULONGSTDMETHODCALLTYPE classAddRef(IClassFactory *this)
{
return(1);
}
ULONGSTDMETHODCALLTYPE classRelease(IClassFactory *this)
{
return(1);
}
現在,讓我們看看我們的QueryInterface。它要檢查是否傳給它的GUID是IUnkown的GUID(由於我們的IClassFactory有QueryInterface、AddRef和Release函數,它也可以裝扮成一個IUknown對象)還是IClassFactory的GUID。另外,我們要做像我們在IExample的QueryInterface中做的同樣的事情。
[cpp]
HRESULTSTDMETHODCALLTYPE classQueryInterface(IClassFactory *this,
REFIID factoryGuid,void **ppv)
{
// 檢查GUID是否與IClassFactory或Iuknown的GUID匹配。
if(!IsEqualIID(factoryGuid, &IID_IUnknown) &&
!IsEqualIID(factoryGuid,&IID_IClassFactory))
{
// 不匹配,清除句柄,返回E_NOINTERFACE。
*ppv = 0;
return(E_NOINTERFACE);
}
// 匹配
// 首先,我們用它傳給我們的同一個對象指針填充它的句柄。
// 這是它從我們這裡獲得的我們的IClassFactory(MyClassFactoryObj)
*ppv = this;
// 傳遞IClassFacory,調用我們的IClassFactory的AddRef。
this->lpVtbl->AddRef(this);
// 讓他知道它確實擁有了一個IClassFactroy
return(NOERROR);
}
我們的IClassFactory的LockServer現在就是一個擺設:
[cpp]
HRESULT STDMETHODCALLTYPEclassLockServer(IClassFactory *this, BOOL flock)
{
return(NOERROR);
}
還有一個函數要寫-CreateInstance。下面是它的定義:
[cpp]
HRESULT STDMETHODCALLTYPEclassCreateInstance(IClassFactory *,
IUnknown *, REFIID,void **);
通常,第一個參數會是一個指向我們的被用來調用CreateInstance的IClassFactory對象(MyIClassFactoryObj)指針。僅當我們實現了聚合,我們才使用第二個參數。我們現在先不理這個。如果它非零,就是有人要我們支持聚合,在這我們不支持,我們會通過返回一個錯誤來提示。第三個參數是這個IExample虛表的GUID(如果有人確實要我們來分配、初始化和返回一個IExample對象)。第四個參數是一個用於存放我們返回的我們創建的IExample對象的句柄。
現在讓我們開始寫我們的CreateInstance函數(被名字為classCreateInstance):
[cpp]
HRESULTSTDMETHODCALLTYPE classCreateInstance(IClassFactory *this,
IUnknown *punkOuter, REFIID vTableGuid,void **ppv)
{
HRESULT hr;
struct IExample *thisobj;
// 通過清除調用者的句柄顯示錯誤
*ppv = 0;
// 在IExample中我們不支持聚合
if (punkOuter)
hr = CLASS_E_NOAGGREGATION;
else
{
//創建我們的IExample對象並初始化。
if (!(thisobj = GlobalAlloc(GMEM_FIXED,
sizeof(structIExample))))
hr = E_OUTOFMEMORY;
else
{
// 存儲IExample的虛表。我們把它聲明為一個靜態變量IExample_Vtbl
thisobj->lpVtbl =&IExample_Vtbl;
// 增加引用計數以便如果在調用QueryInterface()有錯誤時
// 我們可以在下面調用Release()並且它會銷毀
thisobj->count = 1;
// 用我們上面分配的IExample指針填充調用者的句柄。
// 我們讓IExample的QueryInterface來做它,
// 因為它也會檢查調用者傳入的GUID,如果一切正確它也會增加
// 引用計數(到2)。
hr = IExample_Vtbl.QueryInterface(thisobj,vTableGuid, ppv);
// 減小引用計數
// 注意:如果在QueryInterface()中發生了一個錯誤,
// 那麼Release會減小計數到0 並替我們釋放IExample。
// 當調用者尋找某種我們不支持的對象(也就是說它是一
// 個我們不認可的GUID)時可能發生錯誤。
IExample_Vtbl.Release(thisobj);
}
}
return(hr);
}
這樣就實現了我們的IClassFactory對象了。
打包到DLL中
為了使其它程序更容易獲得我們的IClassFactory(調用它的CreateInstance函數來獲得IExample對象),我們要把上面的源代碼打包成一個動態鏈接庫(DLL)。本文不討論怎樣去創建一個DLL本身,所以如果你不熟悉它,你首先需要閱讀一下關於DLL的指南。
上面,我們已經寫了IExmaple和IClassFactory對象的所有代碼。我們所需要做的就是把它們粘貼到我們的DLL源代碼中。
但是還有一些事情要做。微軟規定我們必須添加一個叫DllGetClassObject的函數到我們的DLL中。微軟已經定義了它的傳遞參數、它該做什麼和應該返回什麼。其它程序會調用我們的DllGetClassObject來獲得我們的IClassFactory對象指針。(事實上,就像我們以後看到得,程序會調用一個命名為CoGetClassObject的OLE函數,在它內部會調用我們的DllGetClassObject。)所以,這就是一個程序如何獲得我們得IClassFactory對象的方法-通過調用我們的DllGetClassObject。我們的GetClassObject函數必須完成它的工作。這是它的定義:
[cpp]
HRESULTPASCAL DllGetClassObject(REFCLSID objGuid,
REFIID factoryGuid, void**factoryHandle);
第一個傳遞參數是IExample對象的GUID(不是它的虛表GUID)。我們需要檢查它來做確定調用者是否是明確調用我們DLL的DllGetClassObject。注意每個COM的DLL在它裡面都有一個DllGetClassObject函數。所以我們需要用GUID來區分我們的DllGetClassObject與其他COMDLL的DllGetClassObject。
第二個參數是IClassFactory的GUID。
第三個參數是個句柄,程序期望我們通過它返回我們的IClassFactory指針(如果這個程序確實傳入入一個IExampleGUID,不是一個其
HRESULTPASCAL DllGetClassObject(REFCLSID objGuid,
REFIIDfactoryGuid, void **factoryHandle)
{
HRESULT hr;
// 檢查調用者傳入的IExample的GUID。看它是否是我們的DLL實現的COM對象。
if (IsEqualCLSID(objGuid,&CLSID_IExample))
{
// 用我們的IClassFactory對象指針填充調用者的句柄。
// 我們讓我們的IClassFactory的QueryInterface來做,
// 因為它也檢查IClassFactory的GUID和做其他事宜
hr =classQueryInterface(&MyIClassFactoryObj,
factoryGuid,factoryHandle);
}
else
{
// 我們不能解析這個GUID。顯然它不在我們的DLL中。
// 通過清除它的句柄和返回CLASS_E_CLASSNOTAVAILABLE來讓調用者知道
*factoryHandle = 0;
hr = CLASS_E_CLASSNOTAVAILABLE;
}
return(hr);
}
我們幾乎做完了我們需要創建DLL的工作。還有一個事要做。其實程序不會真正加載我們的DLL。而是當程序調用CoGetDllClassObject時,操作系統代替它來完成(也就是說,CoGetClassObject定位我們的DLL文件,對它調用LoadLibrary,用GetProcAddress來得到我們上面的DllGetClassObject,代替這個程序調用它)。不幸的是,微軟沒有設計出一些方法給程序,當程序使用完我們的DLL來告訴操作系統、讓操作系統卸載(FreeLibrary)我們的DLL。所以我們必須幫助操作系統讓它知道我們的DLL什麼時候可以安全的卸載。所以我們必須提供一個叫DllCanUnloadNow的函數,當可以安全刪除的時候返回S_OK,否則返回S_FALSE。
那麼我們怎樣知道它什麼時候安全呢?
我們必須做一些引用計數。確切的說,每當我們分配一個對象給其它程序,我們必須對引用計數增一。其它程序每調對象的Release函數一次,我們要釋放對象,同時對引用計數減少同樣的次數。只有這個計數為零時我們通知操作系統我們的DLL可以安全的卸載了,因為這時我們知道程序不在使用我們的對象了。所以,我們聲明一個叫OutstandingObjects的靜態DWORD變量來保存這個計數。(當然,我們的DLL第一次被加載時,它需要被初始化為0。)
那麼,在哪裡增加這個變量最方便呢?在我們IClassFactory的CreateInstance函數中,在我們GlobalAlloc分配這個對象並確認所有的工作都正確後。這樣,我們要在這個函數中,在調用Release返回正確後增加一行:
[cpp]
static DWORDOutstandingObjects = 0;
HRESULT STDMETHODCALLTYPEclassCreateInstance(IClassFactory *this,
IUnknown *punkOuter, REFIID vTableGuid,void **ppv)
{
...
IExampleVtbl.Release(thisobj);
// 如果一切正確增加外部對象的計數
if (!hr)InterlockedIncrement(&OutstandingObjects);;
}
}
return(hr);
}
那麼,在哪裡減小這個變量最方便呢?在我們IExample的Release函數的正確GlobalFree對象後。所以我們在GlobalFree後增加一行:
[cpp]
InterlockedDecrement(&OutstandingObjects);
但還沒完。(這些混亂的細節微軟永遠不會結束?)微軟還給出一個允許程序在內存中鎖定我們DLL的方法。為此,它可以調用我們IClassFactory的LockServer函數,如果它要我們增加一次我們DLL的鎖的次數則傳入1,如果它要我們減少一次我們DLL的鎖的次數則傳入0。這樣,我們需要第二個命名為LockCount的靜態DWORD引用計數。(當然,當我們的DLL被加載時它也需要初始化為0)我們的LockServer函數現在變成這樣:
[cpp]
static DWORD LockCount =0;
HRESULT STDMETHODCALLTYPE
classLockServer(IClassFactory *this,BOOL flock)
{
if (flock)InterlockedIncrement(&LockCount);
else InterlockedDecrement(&LockCount);
return(NOERROR);
}
現在我們准備寫我們的DllCanUnloadNow函數:
[cpp] view plaincopy
HRESULT PASCALDllCanUnloadNow(void)
{
// 如果有人要重新獲得我們對象的指針,並且其他人還沒有調用Release()。
// 那麼我們返回S_FALSE來提示不能卸載這個DLL。
// 如果有人已經鎖定了它,同樣返回S_FALSE。
return((OutstandingObjects | LockCount) ?S_FALSE : S_OK);
}
如果你下載這個例程,我們DLL的源文件(IExample.c)在IExample目錄下。這個源文件也支持通過微軟的VisualC++工程文件來創建一個DLL(IExample.dll)
C++/C包含文件
像早些時候提到的,為了讓C/C++寫的程序使用我們的IExmaple DLL,我們需要把我們的IExample和它的虛表GUID給其他程序的作者。我們把這些GUID宏放在包含(.H)文件中,它會分發給其他人,它也包含在我們的DLL源代碼中。我們也需要把IExmapleVtbl和IExample結構定義放在這個包含文件中,這樣其它程序就可以通過我們給他的IExample來調用我們的函數了。
到目前為止,我們定義IExampleVtble和IExample結構如下:
[cpp]
typedefHRESULT STDMETHODCALLTYPE QueryInterfacePtr(IExample *, REFIID, void **);
typedefULONG STDMETHODCALLTYPE AddRefPtr(IExample *);
typedef ULONGSTDMETHODCALLTYPE ReleasePtr(IExample *);
typedef HRESULTSTDMETHODCALLTYPE SetStringPtr(IExample *, char *);
typedef HRESULTSTDMETHODCALLTYPE GetStringPtr(IExample *, char *, long);
typedef struct {
QueryInterfacePtr *QueryInterface;
AddRefPtr *AddRef;
ReleasePtr *Release;
SetStringPtr *SetString;
GetStringPtr *GetString;
} IExampleVtbl;
typedef struct {
IExampleVtbl *lpVtbl;
DWORD count;
char buffer[80];
} IExample;
上面代碼有一個問題。我們不想讓其它程序知道我們的“count”和“buffer”成員。我們要對其它程序隱藏它們。我們決不允許程序直接訪問我們對象的數據成員。它應該只知道“lpVtbl”成員,通過它來調用我們的函數。所以,就程序相關性而言,我們要這樣定義IExample:
[cpp]
typedef struct {
IExampleVtbl *lpVtbl;
} IExample;
最後,上面的C定義有一個問題。對於一個要使用我們的COM對象的C++程序員來說,它真不是容易的事。畢竟,即使我們用C寫了IExample,我們的IExample結構是一個真正的C++類。但對於一個使用它的C++程序而言,把它定義成C++類比C結構要容易的多。
為了替換上面的定義,微軟提供了一個我們可以使用它在某種程度上使C和C++都可以工作且隱藏額外數據成員的宏。為了使用這個宏,我們必須首先把我們的對象名(就是IExample)定義為INTERFACE。在這之前,我們必須undef這個符號來避免編譯器的警告。然後我們使用DECLARE_INTERFACE_宏。在這個宏裡面,我們列出我們的IExample函數。它看起來是這個樣子的:
[cpp]
#undef INTERFACE
#defineINTERFACE IExample
DECLARE_INTERFACE_(INTERFACE, IUnknown)
{
STDMETHOD (QueryInterface) (THIS_ REFIID,void **) PURE;
STDMETHOD_ (ULONG, AddRef) (THIS) PURE;
STDMETHOD_ (ULONG, Release) (THIS) PURE;
STDMETHOD (SetString) (THIS_ char *)PURE;
STDMETHOD (GetString) (THIS_ char *,DWORD) PURE;
};
它可能看起來有點怪怪的。
當在定義一個函數時,只要函數返回HRESULT就要使用STDMETHOD。我們的QueryInterface、SetString和GetString函數返回的是HRESULT。AddRef和Release不是。後面的兩個返回ULONG。這就是我們用STDMETHOD_(以下滑線結尾)替換它們兩個的原因。然後我們把函數名字放入括號中。如果函數不返回HRESULT,我們需要放入它的返回值的類型,然後在這個函數名前加一個逗號。在函數名後,在括號中列出函數的參數。THIS是指向我們對象(也就是IExample)的一個指針。如果傳給函數的僅僅是這個指針,那麼你只需簡單的把THIS放在括號中。AddRef和Release函數就是這種情況。但其它函數有額外的參數。所以我們必須用THIS_(以下劃線結尾)。然後我們列出剩余參數。注意在THIS_和剩余參數之間沒有逗號。但是每個剩余參數之間有逗號。最後,我們放一個PURE字和分號。
當然,這是一個不可思議的宏,通過這種方法定義COM對象,普通的C編譯器和C++編譯器都可以正常工作。
“但我們的IExample結構定義在哪裡?”。你可能這麼問。這個宏確實非常不可思議。它使C編譯器自動生成只包含“lpVtbl”成員的IExample結構定義。所以僅通過這個方法定義我們的虛表,我們自動獲得適合其它程序員的IExample定義。
粘貼我們的兩個GUID宏到包含文件中,一切准備就緒。我這樣創建了IExample.h文件。
但你知道,我們的IExample事實上有兩個數據成員。所以我們必須做的是在我們的DLL源文件中定義一個IExample的“異型”。我們叫它“MyRealIExample”,它是我們的IExample的真正的定義。
[cpp]
typedef struct {
IExampleVtbl *lpVtbl;
DWORD count;
char buffer[80];
} MyRealIExample;
我們在IClassFactory的CreateInstance中改變一行以便分配MyRealIExample結構:
[cpp]
if (!(thisobj =GlobalAlloc(GMEM_FIXED, sizeof(struct MyRealIExample))))
其它程序不需要知道我們給它的對象內部的額外數據成員(對其他程序隱藏有實際的目的)。畢竟,這兩個結構擁有指向同一函數指針數組的同一“lpVtbl”。但現在我們的DLL函數可以通過鑄造一個指向MyRealIExample指針的IExample指針來訪問這些“隱藏的”成員。
定義文件(DEF)
我們也需要一個DEF文件來暴露DllCanUnloadNow和DllGetClassObject這兩個函數。微軟的編譯器要求把它們定義為PRIVATE。這是我們的DEF文件,鏈接器依賴它:
[cpp]
LIBRARY IExample
EXPORTS
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
安裝DLL和注冊對象
現在我們完成了構造IExample.dll所需的每件事。我們繼續來編譯IExample.dll。
但我們的工作還沒完。在其它程序可以使用IExample對象(也就是這個DLL)前,我們需要做兩件事情:
安裝DLL在其它程序運行計算機的可以找到的地方。
將DLL作為COM組件注冊
我們需要創建一個拷貝IExample.DLL到一個適當位置的安裝程序。例如,或許我們在ProgramFiles目錄下創建一個“IExample”的目錄,把DLL拷貝到那。(當然我們的安裝程序要做版本檢查,這樣如果我們的DLL的一個後續版本在那已經安裝過了,我們就不用舊版本來覆蓋它。)
我們接著需要注冊這個DLL。這包括創建幾個注冊表鍵值。
我們首先需要在HKEY_LOCAL_MACHINE\Software\Classes\CLSID下創建一個鍵值。我們必須用我們的IExample對象的GUID作為這個新鍵值的名字,,而且它必須是一個特殊格式的文本字符串。
如果你下載了例程,在RegIExample目錄下包含了一個IExample.dll安裝程序。StringFromCLSID函數給出怎樣把我們的IExample的GUID格式化成一個適合於用它來創建注冊表鍵值名的文本字符串。
注意:這個安裝例程在注冊DLL前不拷貝DLL到適當的位置。這比較合理,它允許你選擇任何地方來編譯你的IExample.dll和注冊它。這對於開發、測試來說更方便。一個優秀的安裝程序產品應該把DLL拷貝到合適的位置,並且做版本檢查。這些需要增加部分留給你來做你自己的安裝程序。
在我們的“GUID鍵”下,我們必須創建一個名叫的InprocServer32子鍵。這個子鍵的缺省值要設置成我們的DLL安裝的全路徑。
如果我們不需要限制調用我們DLL函數的程序必須是單線程,我們還必須設置一個命名為ThreadingModel的值為“both”字符串值。由於我們在IExample函數沒有用到全局數據,我們是線程安全的。
運行完我們的安裝程序後,IExample.dll現在已經作為一個COM組件注冊在我們的計算機上了,現在其它程序可以使用它了。
注意:UnregIExample目錄裡包含一個反安裝IExample.dll的例子。它只刪除RegIExample創建的注冊表鍵值項。一個優秀的反安裝產品應該刪除IExample.dll和安裝程序創建的目錄。
C實例程序
現在我們准備寫一個使用我們IExmapleCOM對象的C程序。如果你下載了例程,IexampleApp目錄包含一個C例子程序。
首先,C程序要#include我們的IExample.h包含文件,這樣它可以查詢到IExample對象和它的虛表的GUID。
一個程序在使用COM對象前,它必須通過調用CoInitialize函數來初始化COM。它只需要初始化一次,所以在程序一開始的地方做比較好。
接下來,程序通過調用CoGetClassObject來獲得IExample.dll的IClassFactory對象指針。注意我們傳遞IExample對象的GUID作為第一個參數。我們也傳遞一個我們的classFactory變量指針,如果一切正確,IClassFactory指針會通過classFactroy返回給我們。
一旦我們擁有了一個IClassFactory對象,我們可以調用它的CreateInstance函數來獲得一個IExample對象。注意我們用IClassFactory調用它的CreateInstance函數的方法。我們通過IClassFactory的虛表(也就是它的lpVtbl成員)來得到這個函數。同時注意我們傳IClassFactory指針作為第一個參數。記住這是一個標准的COM。
注意我們傳遞IExample的虛表的GUID作為第三個參數。對於第四個參數,我們傳入一個我們的exampleObj的變量指針,如果一切正確,IExample對象指針會通過exampleObj返回給我們。
一旦我們擁有了一個IExample對象,我們可以釋放IClassFactory對象。記住當程序使用完對象時必須調用對象的Release函數。IClassFactory是一個對象,就像IExample是一個對象一樣。每個對象有自己的Release函數,當我們使用完對象時必須調用它。我們不再需要IClassFactory了。我們不再需要獲得IExample對象了,也不需要調用IClassFactory的其它函數了,所以現在我們可以釋放它了。注意它根本不影響我們的IExample對象。
所以接下來我們調用IClassFactory的Release函數。一旦我們這樣做了,我們的classFactory變量不再包含一個有效的指針。它現在是一個垃圾了。
但我們還擁有IExmaple指針。我們還沒有釋放它。所以接下來我們決定調用IExample的函數。我們調用SetString。然後我們接著調用GetString。注意我們使用IExmaple指針調用它的SetString函數的方法。我們通過IExmaple的虛表獲得這個函數。也要注意我們傳遞IExample指針作為第一個參數。因為它是一個標准的COM。
當我們最後使用完IExample後,我們要釋放它。一旦我們這樣做了,我們的exampleObj變量不再包含一個有效的指針。
最後,我們必須調用CoUninitialize讓COM來清除內部的垃圾。它只需要做一次,所以它最好放在我們程序的結尾(但只有CoInitialize調用成功才需要這麼做)。
也可以用CoCreateInstance函數替換CoGetClassObject(來獲得DLL的IClassFactory),然後調用IClassFactory的CreateInstance。CoCreateInstance本身會調用CoGetClassObject,然後調用IClassFactory的CrateInstance。CoCreateInstance直接返回IExample,繞過獲得 IClassFactory。這是使用的一個例子。
[cpp]
if((hr = CoCreateInstance(&CLSID_IExample, 0,
CLSCTX_INPROC_SERVER,&IID_IExample, &exampleObj)))
MessageBox(0, "Can't create IExampleobject",
"CoCreateInstanceerror",
MB_OK|MB_ICONEXCLAMATION);
C++實例程序
IExampleCplusApp 目錄下包含一個C++例程。它是像C例子一樣正確。但你要注意有些重要的不同之處。首先,因為在IExample.h中宏把IExmaple定義為一個C ++類(而不是一個結構),並且因為C++用特殊的方式處理類,C++程序以不同的格式調用我們的IExample函數。
在C中我們直接通過訪問虛表(通過lpVtble成員)來獲得IExample函數,我們總是傳入IExample作為第一個參數。
C++編譯器知道把一個類的虛表作為它的第一個成員,自動訪問它的lpVtbl成員來獲得它裡面的函數。所以我們不必指定lpVtbl部分。C++編譯器也會自動傳遞這個對象作為第一個參數。
那麼盡管在C中,我們的代碼:
[cpp]
classFactory->lpVtbl->CreateInstance(classFactory,0,
&IID_IExample,&exampleObj);
而在C++中,我們代碼改為:
[cpp]
classFactory->CreateInstance(0,IID_IExample, &exampleObj);
注意:我們也省略IID_IExample的GUID的&。C++的GUID宏不需要指定它。
修改代碼
創建你自己的對象,給IExample目錄作個拷貝。刪除Debug和Release子目錄,還有下面的文件也得刪除:
[cpp]
IExample.dsp
IExample.dsw
IExample.ncb
IExample.opt
IExample.plg
在剩下的文件(IExample.c、IExample.h、IExample.def)中搜索IExample字符串並用你自己的對象名替換它(例如,IMyObject.c等)。在這個目錄下以你的新對象名創建一個新的VisulaC++工程,工程的類型要選“Win32 Dynamic-Link Library”。創建一個空的工程,然後把上面的三個文件加到工程中。你一定要用GUIDGEN.EXE給你的對象和它的虛表生成你自己的GUID。不要用我生成的GUID。替換.H文件中的GUID宏(同時記住也要替換GUID宏的<<name>>部分)。刪除.C和.H文件中的SetString和GetString函數,添加你自己的函數。修改.H文件你添加的函數定義的INTERFACE宏。修改MyRealIExample的數據成員為你需要的。修改安裝程序源文件中的前三個字符串。在這個例子程序中,搜索並替換IExample為你的對象名。
接下來是什麼?
雖然一個C或者C++程序,或者一個用大部分編譯語言編寫的程序,可以使用我們的COM對象,我們必須添加一些東東來支持大多數解釋性語言來使用我們的對象,例如VisualBasic、VBScript、Jscript、Python等。這會是這個系列第二部分的主題。