介紹
對於我來說,理解COM(Component Object Model,組件對象模型)絕不亞於 一次長途旅行。我相信,每一個想要理解COM之後基本原理的程序員都必須使用普通的C++編 寫至少一個簡單的COM對象,也就是說,不依靠MFC/ATL所提供的任何模板或宏的支持。在本 文中,我將要逐步介紹如何從基本原理出發來創建簡單的COM對象。這些組件可用於VC/VB的 客戶端程序。
作為練習,我們將要嘗試設計一個COM組件,這一組件將要實現假想的 快速相加算法。它必須傳入兩個長數據類型的參數,並返回另一個長參數給用戶,也就是相 加算法的結果。我們現在開始設計接口。
接口
COM對象的接口並不涉及到實際 的實現,但是它的方法則標志著COM對象中用來和外界通信的部分。我們將我們的接口命名為 IAdd,它的聲明使用接口定義語言(Interface Definition Language,IDL)。IDL是用來定 義函數標志的語言,它獨立於各種程序語言之間,這就使得RPC底層能夠在不同的計算機之間 對參數進行打包、裝載與解包。在我們的IAdd接口中,我們擁有SetFirstNumber和 SetSecondNumber方法,它們用來傳遞加法的參數。還有一個方法,DoTheAddition,它用來 完成加法並將結果回傳給客戶端。
第1步:
創建一個新的Win32 DLL工程(比 如說AddObj),我們將會在這個文件夾中創建接下來的所有文件。創建一個空文件,然後鍵 入以下內容。將它保存為IAdd.idl。接口的標識符可以使用工具uuidgen.exe來生成。
import "unknwn.idl";
[
object,
uuid (1221db62-f3d8-11d4-825d-00104b3646c0),
helpstring("interface IAdd is used for implementing a super-fast addition Algorithm")
]
interface IAdd : IUnknown
{
HRESULT SetFirstNumber(long nX1);
HRESULT SetSecondNumber(long nX2);
HRESULT DoTheAddition([out,retval] long *pBuffer);
};
[
uuid(3ff1aab8-f3d8-11d4-825d-00104b3646c0),
helpstring("Interfaces for Code Guru algorithm implementations .")
]
library CodeGuruMathLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
interface IAdd;
}
第2步:
使用命令行編譯器MIDL.exe(注意:midl.exe隨 VC++一同附帶,並且可能會包括一些對於midl的路徑問題,你可能需要修改你的路徑變量設 置)來編譯文件IAdd.idl。
經過編譯,會生成以下文件:
IAdd.h 包含 C++風格的接口聲明。
dlldata.c 包含了代理DLL的代碼。用於在不同的進程或計算 機上調用該對象的情況。
IAdd.tlb 二進制文件,描述了我們的IAdd接口以及它的 所有方法。該文件可以被我們COM組件的所有客戶端使用。
IAdd_p.c 包含了代理 DLL的編組代碼。用於在不同的進程或計算機上調用該對象的情況。
IAdd_i.c 包含 了接口的IID。
第3步:
現在我們來創建這個COM對象。創建一個新文件 (AddObj.h),聲明一個C++類,將其命名為CAddObj,繼承自接口IAdd(文件IAdd.h)。請 記住,IAdd從IUnknown繼承而來,它亦是一個抽象基類。因此,我們要和IUnknown一樣為抽 象基類IAdd聲明所有的方法。
///////////////////////////////////////////////////////////
//
//AddObj.h
//包含了實現IAdd接口的C++類聲明
//
#include "IAdd.h"
extern long g_nComObjsInUse;
class CAddObj :
public IAdd
{
public:
//IUnknown接口
HRESULT __stdcall QueryInterface(
REFIID riid ,
void **ppObj);
ULONG __stdcall AddRef();
ULONG __stdcall Release();
//IAdd接口
HRESULT __stdcall SetFirstNumber( long nX1);
HRESULT __stdcall SetSecondNumber( long nX2);
HRESULT __stdcall DoTheAddition( long *pBuffer);
private:
long m_nX1 , m_nX2; //加法的操作數
long m_nRefCount; //管理引用計數
};
///////////////////////////////////////////////////////////
第4步 :
我們將要為IAdd接口的所有方法提供實現。創建一個新文件(AddObj.cpp)並實現 如下代碼的所有方法。
///////////////////////////////////////////////////////////
//
//AddObj.cpp
//包含IAdd接口的方法實現
//
#include <objbase.h>
#include "AddObj.h"
#include "IAdd_i.c"
HRESULT __stdcall CAddObj::SetFirstNumber( long nX1)
{
m_nX1=nX1;
if (m_bIsLogEnabled) WriteToLog("Junk");
return S_OK;
}
HRESULT __stdcall CAddObj::SetSecondNumber( long nX2)
{
m_nX2=nX2;
return S_OK;
}
HRESULT __stdcall CAddObj::DoTheAddition( long *pBuffer)
{
*pBuffer =m_nX1 + m_nX2;
return S_OK;
}
第5步:
我們還需要實現IUnknown的方法。我們將要在相同 的文件AddObj.cpp中實現3個固定的方法(AddRef、Release和QueryInterface)。那個 private成員m_nRefCount用於維護對象的生存期。m_nRefCount不會直接減少或增加,我們采 用了一種線程安全的做法,也就是使用互鎖增加和減少的API。
HRESULT __stdcall CAddObj::QueryInterface(
REFIID riid ,
void **ppObj)
{
if (riid == IID_IUnknown)
{
*ppObj = static_cast(this) ;
AddRef() ;
return S_OK;
}
if (riid == IID_IAdd)
{
*ppObj = static_cast(this) ;
AddRef() ;
return S_OK;
}
//
//如果控制達到了這裡,那麼就讓客戶端知道
//我們不支持請求的接口
//
*ppObj = NULL ;
return E_NOINTERFACE ;
}//QueryInterfac方法
ULONG __stdcall CAddObj::AddRef()
{
return InterlockedIncrement(&m_nRefCount) ;
}
ULONG __stdcall CAddObj::Release()
{
long nRefCount=0;
nRefCount=InterlockedDecrement(&m_nRefCount) ;
if (nRefCount == 0) delete this;
return nRefCount;
}
第6步:
我們已經完成了Add COM對象的功能部分。就像每 一條COM的准則一樣,每一個COM對象都必須有一個接口IClassFactory的單獨實現。客戶端將 會使用這個接口來獲得我們IAdd接口實現的一個實例。IClassFactory接口就像其它所有的 COM接口一樣,也是繼承自IUnknown的。因此,我們將要提供IUnknown方法的實現以及 IClassFactory方法(LockServer和CreateInstance)的實現。創建一個新文件(名之為 AddObjFactory.h),聲明一個類CAddFactory,繼承自IClassFactory。
///////////////////////////////////////////////////////////
//
//AddObjFactory.h
//包含了IClassFactory實現的C++類聲明
//
class CAddFactory : public IClassFactory
{
public:
//IUnknown接口的方法
HRESULT __stdcall QueryInterface(
REFIID riid ,
void **ppObj);
ULONG __stdcall AddRef();
ULONG __stdcall Release();
//IClassFactory接口的方法
HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv) ;
HRESULT __stdcall LockServer(BOOL bLock) ;
private:
long m_nRefCount;
};
第7步:
現在需要實現CAddFactory的方法。創建一個新文 件(AddObjFactory.cpp),在其中提供IUnknown和IClassFactory所有方法的實體。AddRef 、Release和QueryInterface方法就和類CAddObj的實現相似。CreateInstance方法的實現如 下,其中實例化了CAddObj類並回傳了請求的接口指針。LockServer方法則沒有特定的實現。
HRESULT __stdcall CAddFactory::CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv)
{
//
//這一方法可以使得全體客戶端來制造組件
//類工廠提供了一種機制,可以控制生成組件的方法。
//通過類工廠,組件的作者就可能決定使每一條許可協議
//的創建生效或失效。
//
// 不能聚合
if (pUnknownOuter != NULL)
{
return CLASS_E_NOAGGREGATION ;
}
//
// 創建組件的實例
//
CAddObj* pObject = new CAddObj ;
if (pObject == NULL)
{
return E_OUTOFMEMORY ;
}
//
// 獲得請求的接口
//
return pObject->QueryInterface(iid, ppv) ;
}
HRESULT __stdcall CAddFactory::LockServer(BOOL bLock)
{
return E_NOTIMPL;
}
第8步:
一個進程內的COM對象其實和一個簡單的Win32 DLL 並沒有什麼兩樣,它們都遵守一個特定的協議。每個COM DLL都必須有一個名為 DllGetClassObject的導出函數,客戶端將會調用這個函數來獲得類工廠(IUnknown或 IClassFactory)的一個實例,然後緊接著是CreateInstance方法。創建一個新文件 (Exports.cpp),在其中實現DllGetClassObject。
STDAPI DllGetClassObject(const CLSID& clsid,
const IID& iid,
void** ppv)
{
//
//檢查請求的COM對象是否在此DLL之中
//DLL中可以實現多個COM對象
//
if (clsid == CLSID_AddObject)
{
//
//iid為類工廠指定了請求的接口
//客戶端可以請求IUnknown、IClassFactory、IClassFactory2
//
CAddFactory *pAddFact = new CAddFactory;
if (pAddFact == NULL)
return E_OUTOFMEMORY;
else
{
return pAddFact->QueryInterface(iid , ppv);
}
}
//
//如果控制達到了這裡,那麼這就表示該DLL中沒有實現用戶指定的對象
//
return CLASS_E_CLASSNOTAVAILABLE;
}
第9步:
客戶端還需要知道一個COM DLL什麼時候可以從內 存中卸載。深入一些來講,進程內的COM對象可以通過調用API函數LoadLibrary來進行顯式裝 載,這就可以將DLL裝入客戶端的進程地址空間。同樣,調用FreeLibrary可以將DLL顯式卸載 。
COM客戶端必須知道DLL什麼時候可以被安全卸載,它必須確認當前DLL中沒有任何 存在的COM對象實例。為了讓這個計算更簡單一些,我們將會在COM DLL中CAddObj和 CAddFactory的C++構造函數中增加一個全局變量(g_nComObjInUse)的值。相似地,我們會 在它們各自的析構函數中減少g_nComObjInUse的值。
我們還要導出一個特定的COM函 數DllCanUnloadNow,它的實現如下:
STDAPI DllCanUnloadNow()
{
//
//當DLL中沒有存在的對象時,它就不在使用中了
//(它所有的對象引用計數為0)
//我們將會檢查g_nComObjsInUse的值
//
if (g_nComObjsInUse == 0)
{
return S_OK;
}
else
{
return S_FALSE;
}
}
第10步:
COM對象的位置還需要被寫入注冊表,這可以通過 一個外部的.REG文件實現,或者讓DLL導出一個DllRegisterServer函數。為了清除注冊表的 內容,我們還要導出另一個DllUnregisterServer函數。這兩個函數的實現在Registry.cpp之 中。你可以使用一些簡單的工具(如regsvr32.exe)來裝載一個特定的DLL並執行 DllRegisterServer/DllUnregisterServer。
為了使鏈接器導出這4個函數,我們還需 要創建一個模塊定義文件(Exports.def)。
;
;包含了DLL中導出的函數列表
;
DESCRIPTION "Simple COM object"
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
第11步:
我們需要最後處 理一下我們的Win32 DLL工程AddObj。將IAdd.idl文件插入工程的工作區中。
為此文件設置自 定義的構建選項。
在“Post- build step”對話框中插入一個命令行字符串來在每次構建結束後運行regsvr32.exe。
構建此 DLL。將該IDL文件插入工作區將會使得在每一次文件被修改後外部的編輯更加容易。每一次 我們的工程成功編譯後,COM對象也就注冊完成了。
第12步:
現在來在Visual Basic中使用AddObj這個COM對象。創建一個簡單的EXE工程,並運行以下幾行代碼。請確認要 添加一個IAdd.tlb類型庫的工程引用。
Dim iAdd As CodeGuruMathLib.iAdd
Set iAdd = CreateObject ("CodeGuru.FastAddition")
iAdd.SetFirstNumber 100
iAdd.SetSecondNumber 200
MsgBox "total = " & iAdd.DoTheAddition()
第13步:
我們之前使用了以下的文件:
IAdd.idl 包含了的接口聲明。
AddObj.h 包含了類 CAddObj的C++類聲明。
AddObjFactory.h 包含了類CAddFactory的C++類聲明。
AddObj.cpp 包含了類CAddObj的C++類實現。
AddObjFactory.cpp 包含了類CAddFactory的C++類實現。
Exports.cpp 包含了DllGetClassObject 、DllCanUnloadNow和DllMain的實現。
Registry.cpp 包含了 DllRegisterServer、DllUnregisterServer的實現。
AddObjGuid.h 包含了我 們的COM對象AddObj的GUID值。
第14步:
類型庫也可以隨同AddObj.dll一同被 發布。為了簡化這一過程,IAdd.tlb類型庫也可以作為AddObj.dll的一個資源文件嵌入其中 。這樣一來,就可以只向客戶發布DLL文件AddObj.dll了。
第15步:
一個Visual C++客戶端可以通過以下幾種方法來使用COM接口:
1. #impott "IAdd.tlb"。
2. IAdd.h頭文件。在這種情況下,DLL的賣主必須將IAdd.h 頭文件隨同DLL一同發布。
3. 使用一些向導工具(例如MFC的Class Wizard)產生C++ 代碼。
對於第1種方法,編譯器創建一些包含了接口聲明的中間文件(.tlh、.tli) 。更進一步說,編譯器也在原始接口的基礎上定義了智能接口指針。智能接口指針類使得COM 程序員能更輕松地管理COM對象的生存期。
在以下的例子中#import直接導入了 AddObj.dll,而不是AddObj.tlb,因為我們將TLB文件包含於DLL之中了。否則的話,#import 將導入TLB文件。
在VC++中創建一個新的控制台EXE,輸入以下的內容並編譯。
//
///Client.cpp
//
//客戶端從AddObj.dll中使用COM對象 的演示
//
#include <objbase.h>
#include <stdio.h>
#import "AddObj.dll"
//
//這裡我們對 DLL使用了#import,你也可以對.TLB使用#improt。
//#import直接在輸出文件夾中生 成了兩個文件(.tlh/.tli)。
//
void main()
{
long n1 =100, n2=200;
long nOutPut = 0;
CoInitialize(NULL);
CodeGuruMathLib::IAddPtr pFastAddAlgorithm;
//
//IAddPtr並不是實際的IAdd指針,而是一個C++類模板(_com_ptr_t)
//它嵌入了原始的IAdd指針實例
//在析構的時候,析構函數內部會調用原始接口指針的Release()
//更進一步說,它還重載了operator ->來直接操作內部原始接口指針的所有方 法
//
pFastAddAlgorithm.CreateInstance("CodeGuru.FastAddition");
pFastAddAlgorithm->SetFirstNumber(n1);//“->”重載的運行
pFastAddAlgorithm->SetSecondNumber(n2);
nOutPut = pFastAddAlgorithm->DoTheAddition();
printf("\nOutput after adding %d & %d is % d\n",n1,n2,nOutPut);
}