1.COM技術概述
COM表示Component Object Model(元件物件模型),它是Microsoft大力推廣的軟體開發技術。採用COM規範開發的應用軟體具有強大的功能,主要有如下幾點:
◆COM是二進位編程規範,可以編寫被多種語言使用的代碼。
◆用於建立ActiveX控制項。
◆通過OLE Automation 控制其它的程式。
◆與其它機器上的對象或程式進行對話,構成分布式應用程式。
Microsoft推出Windows 98和Windows NT 5.0後,整個作業系統的核心都圍繞著COM來建立。我們可以把Windows系統看作是一系列的COM介面,在需要是可以調用這些介面。如DirectX就是一系列的COM介面服務程式,通過它可以進行高效能的Windows圖形程式設計。
用COM技術開發的應用程式從理論上說是客戶/伺服器模式的程式。程式員可以使用一系列的COM服務程式來構造他們自己的應用程式,這些服務程式可以根據需要隨時嵌入到主程式中。在分布式系統中,可以通過網路來訪問這些服務程式。將來,作業系統和整個網路可能會被看作是一套以COM對象形式提供的服務集。一部分程式員負責建立這些服務,而另一部分程式員只負責如何調用它們。其目的是實現軟體的隨插即用。
開發COM應用程式是比較複雜的,通常需採用ActiveX模板庫(ATL)來編程。在這裡我們推薦採用C++ Builder來開發COM程式,Inprise(Borland)公司的物件導向技術一直處於世界領先水平,C++ Builder採用可視化方法,隱藏了ATL的實現細節,自動產生COM介面所需的代碼。
以下的程式舉例採用C++ Builder 4.0 編製,在中文Windows98環境下運行。
2.建立COM服務程式
COM服務程式有三種形式,第一種是駐留在本地機器上以DLL形式提供,該服務程式被調用時,嵌入到調用程式的線程中運行;第二種是駐留在本地機器上以EXE形式提供,該服務程式被調用時將佔用獨立的線程運行;第三種駐留在遠端機器上以EXE形式提供,服務程式通過網路被調用,它在遠端機器上運行,結果通過網路返回調用者。
在此採用第一種形式建立COM服務程式,這也是最常用的形式,DirectX就是採用這種形式提供的。
C++ Builder建立COM服務程式的方法如下:
2.1建立支援COM介面對象的動態串連庫檔案:
◆開啟File/New/ActiveX項目頁,選擇ActiveX Library;
◆選擇Save All 將項目以PCOMServer檔案名稱儲存;此時C++ Builder 自動產生如下的檔案:
PCOMServer.bpr:工程的專案檔;
PCOMServer.h,PCOMServer.cpp:支援COM對象的動態串連庫源檔案,其中有許多函數用於COM介面對象的自動裝配,大家不用去編輯它們;
PCOMServer_ATL.h,PCOMServer_ATL.cpp:ATL形式的檔案供C++ Builder編譯器調用,大家也不要去編輯它們。
◆開啟Project/Options/Linker 屬性頁面不選中Use dynamic RTL選項,開啟Project/Options/Packages屬性頁面不選中Builder with runtime packages選項,這兩步操作可以使開發的COM動態串連庫不依賴C++ Builder的VCL動態串連庫,有利於獨立發行,但在一般情況下還是建議選中這兩項。
2.2建立COM介面對象
開啟File/New/ActiveX屬性頁面,選擇Automation Object表示向服務程式中插入一個自動類型的COM對象,我們選擇這種類型的COM對象是為了可以自動註冊,並且自動支援可以被其他語言調用。此時出現如下的對話方塊,輸入COM類的名字MyCOM即可,對話方塊中的其它選項用於規定COM對象的性質,可查看協助資訊。
2.3通過類型庫編輯器編輯COM對象中相應介面對象的屬性和方法
此時自動進入類型庫編輯器,類型庫用於儲存COM對象的說明,是一個可以被多種語言調用的標頭檔包。在類型庫中,可以定義COM對象的介面,定義介面對象的屬性和方法等。類型庫編輯器如下所示:
可以看出此時自動產生了MyCOM類的一個介面類IMyCOM,在COM應用軟體中我們實際上是與介面對象打交道,下面通過類型庫編輯器為IMyCOM介面定義方法和屬性。
◆單擊編輯器頂部的Method按鈕;
◆在Arributes頁面的Name欄位中輸入方法的名稱,本例中是AddInt用於整數加法;
◆在Parameters頁面中,單擊Add按鈕編輯方法中的參數;
x和y是輸入的兩個整數,ret用於返回運算的結果,必須定義為指標型
◆切換到Flags頁面,可以對介面的屬性作調整;
◆在Text頁面中可以檢查產生的IDL代碼:
[id(0x00000001)]
HRESULT _stdcall AddInt([in] int x, [in] int y, [out, retval] int * ret );
◆單擊Refresh按鈕,此時可以關閉類型庫編輯器。當需要為介面添加新的屬性和方法時,可以通過View/Type Library重新開啟編輯器。選擇Save All用C++ Builder提供的預設檔案名稱檔案類型庫的相關檔案如下:
PCOMServer.TLB: 類型庫檔案;
PCOMServer_TLB.cpp:包含COM介面和對象的說明,其主要目的是方便訪問,在客戶程式中需將本檔案包含到客戶程式的工程中;
PCOMServer_TLB.h: PCOMServer_TLB.cpp的標頭檔,通過#include引入到客戶程式中。
MyCOMImpl.cpp: 該檔案是我們需要編寫程式碼的地方,實作類別型庫定義的介面對象的方法和屬性;
MyCOMImpl.h: MyCOMImpl.cpp的標頭檔。
2.4 實現COM介面中的方法
開啟MyCOMImpl.cpp檔案會發現我們在類型庫編輯器中定義的方法,為該方法編寫代碼如下:
STDMETHODIMP TMyCOMImpl::AddInt(int x, int y, int* ret)
{
*ret=x+y;
return S_OK;
}
2.5 產生DLL檔案並註冊COM對象
◆選擇Project/Builder PCOMServer 產生PCOMServer.DLL檔案。
◆開啟類型庫編輯器,單擊Register按鈕完成對COM對象的註冊。
通過Windows工作列中的Run菜單運行REGEDIT程式,在Windows註冊表的HKEY_CLASSES_ROOT鍵下尋找到PCOMServer.MyCOM子鍵,PCOMServer為DLL檔案的名字,MyCOM為COM對象的名字,在下面可以看到該COM對象的全域唯一描述符CLSID如下:
{59834F03-49F1-11D3-B85B-00E09804A418}
注意:不同的機器產生的描述符不同.
在HKEY_CLASSES_ROOT鍵下尋找到CLSID子鍵,在它下面找{59834F03-49F1-11D3-B85B-00E09804A418}子鍵,下面有如下的條目:
InprocServer32:儲存PCOMServer.DLL的路徑目錄;
ProgID:儲存COM對象的註冊名:PCOMServer.MyCOM;
Typelib:儲存COM對象的CLSID值{59834F03-49F1-11D3-B85B-00E09804A418}。
COM對象就是通過在註冊表中的紀錄實現DLL與客戶程式的自動連接。
3.建立COM客戶程式
客戶程式將訪問PCOMServer.DLL服務程式中的MyCOM對象,這些對象的說明儲存在前面所述的TLB檔案中。我們可以直接將PCOMServer_TLB.cpp加入到客戶程式的專案檔中,並在客戶程式中引用PCOMServer_TLB.h檔案;也可以通過Project/Import Type Library引用PCOMServer_TLB.TLB檔案,重建.cpp和.h檔案,自動完成上述過程。
客戶程式的編程重點是實現對服務程式中COM對象的方法的調用,調用的方法有多種,都是通過所謂的代理介面來完成的,這些代理介面在PCOMServer_TLB.h中有詳細的定義,從這些定義中可以看出這些代理介面調用對象方法的過程。
PCOMServer_TLB.h檔案很重要,包含了調用MyCOM對象的各種介面資訊,該檔案主要內容如下:
// Type Lib: D:/CAI/com/PCOMServer.tlb
// IID/LCID: {5BD378E5-4B57-11D3-B85B-00E09804A418}/0
// Helpfile:
// DepndLst:
// (1) v2.0 stdole, (C:/WINDOWS/SYSTEM/STDOLE2.TLB)
// (2) v4.0 StdVCL, (C:/WINDOWS/SYSTEM/STDVCL40.DLL)
// ************************************************************************
#ifndef __PCOMServer_TLB_h__
#define __PCOMServer_TLB_h__
#pragma option push -b -w-inl
#include <vcl/utilcls.h>
#if !defined(__UTILCLS_H_VERSION) || (__UTILCLS_H_VERSION < 0x0101)
#error "This file requires an newer version of the header file UTILCLS.H"
#endif
#include <olectl.h>
#include <ocidl.h>
#if defined(USING_ATLVCL) || defined(USING_ATL)
#if !defined(__TLB_NO_EVENT_WRAPPERS)
#include <atl/atlmod.h>
#endif
#endif
namespace Stdvcl {class IStrings; class IStringsDisp;}
using namespace Stdvcl;
namespace Pcomserver_tlb
{
DEFINE_GUID(LIBID_PCOMServer, 0x5BD378E5, 0x4B57, 0x11D3, 0xB8, 0x5B, 0x00, 0xE0, 0x98, 0x04, 0xA4, 0x18);
DEFINE_GUID(IID_IMyCOM, 0x5BD378E6, 0x4B57, 0x11D3, 0xB8, 0x5B, 0x00, 0xE0, 0x98, 0x04, 0xA4, 0x18);
DEFINE_GUID(CLSID_MyCOM, 0x5BD378E8, 0x4B57, 0x11D3, 0xB8, 0x5B, 0x00, 0xE0, 0x98, 0x04, 0xA4, 0x18);
interface DECLSPEC_UUID("{5BD378E6-4B57-11D3-B85B-00E09804A418}") IMyCOM;
typedef IMyCOM MyCOM;
#define LIBID_OF_MyCOM (&LIBID_PCOMServer)
interface IMyCOM : public IDispatch
{
public:
virtual HRESULT STDMETHODCALLTYPE AddInt(int x/*[in]*/, int y/*[in]*/, int* ret/*[out,retval]*/) = 0; // [1]
#if !defined(__TLB_NO_INTERFACE_WRAPPERS)
int __fastcall AddInt(int x/*[in]*/, int y/*[in]*/)
{
int ret;
OLECHECK(this->AddInt(x, y, &ret));
return ret;
}
#endif // __TLB_NO_INTERFACE_WRAPPERS
};
#if !defined(__TLB_NO_INTERFACE_WRAPPERS)
template <class T /* IMyCOM */ >
class TCOMIMyCOMT : public TComInterface<IMyCOM>, public TComInterfaceBase<IUnknown>
{
public:
TCOMIMyCOMT() {}
TCOMIMyCOMT(IMyCOM *intf, bool addRef = false) : TComInterface<IMyCOM>(intf, addRef) {}
TCOMIMyCOMT(const TCOMIMyCOMT& src) : TComInterface<IMyCOM>(src) {}
TCOMIMyCOMT& operator=(const TCOMIMyCOMT& src) { Bind(src, true); return *this;}
HRESULT __fastcall AddInt(int x/*[in]*/, int y/*[in]*/, int* ret/*[out,retval]*/);
int __fastcall AddInt(int x/*[in]*/, int y/*[in]*/);
};
typedef TCOMIMyCOMT<IMyCOM> TCOMIMyCOM;
template<class T>
class IMyCOMDispT : public TAutoDriver<IMyCOM>
{
public:
IMyCOMDispT(){}
IMyCOMDispT(IMyCOM *pintf)
{
TAutoDriver<IMyCOM>::Bind(pintf);
}
IMyCOMDispT& operator=(IMyCOM *pintf)
{
TAutoDriver<IMyCOM>::Bind(pintf);
return *this;
}
HRESULT BindDefault(/*Binds to new instance of CoClass MyCOM*/)
{
return OLECHECK(Bind(CLSID_MyCOM));
}
HRESULT BindRunning(/*Binds to a running instance of CoClass MyCOM*/)
{
return BindToActive(CLSID_MyCOM);
}
HRESULT __fastcall AddInt(int x/*[in]*/, int y/*[in]*/, int* ret/*[out,retval]*/);
int __fastcall AddInt(int x/*[in]*/, int y/*[in]*/);
};
typedef IMyCOMDispT<IMyCOM> IMyCOMDisp;
template <class T> HRESULT __fastcall
TCOMIMyCOMT<T>::AddInt(int x/*[in]*/, int y/*[in]*/, int* ret/*[out,retval]*/)
{
return (*this)->AddInt(x, y, ret);
}
template <class T> int __fastcall
TCOMIMyCOMT<T>::AddInt(int x/*[in]*/, int y/*[in]*/)
{
int ret;
OLECHECK(this->AddInt(x, y, &ret));
return ret;
}
template <class T> HRESULT __fastcall
IMyCOMDispT<T>::AddInt(int x/*[in]*/, int y/*[in]*/, int* ret/*[out,retval]*/)
{
static _TDispID _dispid(*this, OLETEXT("AddInt"), DISPID(1));
TAutoArgs<2> _args;
_args[1] = x /*[VT_INT:0]*/;
_args[2] = y /*[VT_INT:0]*/;
return OutRetValSetterPtr(ret /*[VT_INT:1]*/, _args, OleFunction(_dispid, _args));
}
template <class T> int __fastcall
IMyCOMDispT<T>::AddInt(int x/*[in]*/, int y/*[in]*/)
{
int ret;
this->AddInt(x, y, &ret);
return ret;
}
typedef TCoClassCreatorT<TCOMIMyCOM, IMyCOM, &CLSID_MyCOM, &IID_IMyCOM> CoMyCOM;
#endif // __TLB_NO_INTERFACE_WRAPPERS
}; // namespace Pcomserver_tlb
#if !defined(NO_IMPLICIT_NAMESPACE_USE)
using namespace Pcomserver_tlb;
#endif
#pragma option pop
#endif // __PCOMServer_TLB_h__
下面是檔案中說明的主要對象及其定義:
interface IMyCOM : public IDispatch
class TCOMIMyCOMT : public TComInterface<IMyCOM>
class IMyCOMDispT : public TAutoDriver<IMyCOM>
class CoMyCOM: public CoClassCreator
◆IMyCOM:通過IDispatch介面來調用對象的方法,該介面可以使對象被不支援虛擬函數表(VTable)的語言(如Visual Basic)說調用。這是一種很慢很苯的介面調用方式。
◆TCOMIMyCOMT:通過所謂的智能介面來調用對象的方法,既可以實現IDispatch調用,也可以採用VTable進行調用,從而實現最快的調用速度。
◆IMyCOMDispT:通過disp介面來調用對象的方法,可以提高Idispatch介面的訪問速度,但還是比不上VTable介面。
◆CoMyCOM:通過使用CoClassCreator可以自動產生TCOMIMyCOM代理的執行個體。
下面介紹一下實現智能介面和Disp介面調用的客戶程式。這個客戶程式很簡單,有兩個按鈕分別完成兩種介面調用的方法,一個編輯框顯示結果。
◆智能介面的VTable調用方法如下:
int x=6,y=6;
TCOMIMyCOM O;
O=CoMyCOM::Create(); //通過CoClassCreator完成初始化
O->AddInt(x,y,&y); //Vtable形式調用
Edit1->Text=IntToStr(y);
◆DISP介面的調用方法如下:
int x=6,y=6;
IMyCOMDisp app;
app.BindDefault(); //通過Bind完成初始化
app.AddInt(x,y,&y); //Disp形式調用
Edit1->Text=IntToStr(y);
4.小結
上面的程式舉例是很簡單的,但卻詳細說明了COM應用軟體的開發過程。COM技術不是一個程式設計語言,而是一種編程規範和方法。採用COM技術可以開發功能強大的軟體,有利於分布式應用技術的實現,有利於多人合作開發,也可以協助我們理解Windows系統本身。COM的介面技術是比較複雜的,想進一步瞭解COM技術可參閱清華大學出版社的《COM技術內幕》一書。
C++ Builder是開發COM應用軟體的好工具,它隱含了COM實現的細節,我們只需與它打交道就可以開發完善和強大的COM應用程式。希望有更多的人轉到COM應用軟體的開發上來,COM技術是軟體技術未來的發展方向,是實現軟體工程中軟體隨插即用的有效途徑。