COM是基于二進(jìn)制的組件模塊,從設(shè)計之初就以支持所有語言作為它的一個目標(biāo),這篇文章主要探討COM的跨語言部分。
idl文件
一般COM接口的實(shí)現(xiàn)肯定是以某一具體語言來實(shí)現(xiàn)的,比如說使用VC++語言,這就造成了一個問題,不同的語言對于接口的定義,各個變量的定義各不相同,如何讓使用vc++或者說Java等其他語言定義的接口能被別的語言識別?為了達(dá)到這個要求,定義了一種文件格式idl——(Interface Definition Language)接口定義語言,IDL提供一套通用的數(shù)據(jù)類型,并以這些數(shù)據(jù)類型來定義更為復(fù)雜的數(shù) 據(jù)類型。一般來說,一個文件有下面幾個部分說明
- 接口的定義
- 組件庫的定義
- 實(shí)現(xiàn)類的定義
而各個部分又包括他們的屬性定義,以及函數(shù)成員的定義
屬性:
屬性是在接口定義的上方,使用“[]”符號包裹,一般在屬性中使用下面幾個關(guān)鍵字:
object:標(biāo)明該部分是一個對象(可以理解為c++中的對象,包括接口和具體的實(shí)現(xiàn)類)
uuid:標(biāo)明該部分的GUID
version:該部分的版本
接口定義
接口定義采用關(guān)鍵字interface,接口函數(shù)定義在一對大括號中,它的定義與類的定義相似,其中函數(shù)定義需要修飾函數(shù)各個參數(shù)的作用,比如使用in 表示它作為輸入?yún)?shù),out表示作為輸出參數(shù),retval表示該參數(shù)作為返回值,一般在VC++定義的接口中,函數(shù)返回值為HRESULT,但是需要返回一個值供外界調(diào)用,此時就使用輸出參數(shù)并加上retval表示它將在其他語言中作為函數(shù)的返回值。
組件庫定義
庫使用library關(guān)鍵字定義,在定義庫的時候,它的屬性一般定義GUID和版本信息,而在庫中通常定義庫中的實(shí)現(xiàn)類的相關(guān)信息,庫中的信息也是寫在一對大括號中
實(shí)現(xiàn)類的定義
接口實(shí)現(xiàn)類使用關(guān)鍵字coclass,接口類的屬性一般定義一個object,一個GUID,然后一般定義實(shí)現(xiàn)類不需要向在C++中那樣定義它的各個接口,各個數(shù)據(jù)成員,只需要告知它實(shí)現(xiàn)哪些接口即可,也就是說它繼承自哪些接口。
下面是一個具體的例子:
import "unknwn.idl";
[
object,
uuid(CF809C44-8306-4200-86A1-0BFD5056999E)
]
interface IMyString : IUnknown
{
HRESULT Init([in] BSTR bstrInit);
HRESULT GetLength([out, retval] ULONG *pretLength);
HRESULT Find([in] BSTR bstrFind, [out, retval] BSTR* bstrSub);
};
[
uuid(ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2),
version(1.0)
]
library ComDemoLib
{
importlib("stdole32.tlb");
[
uuid(EBD699BA-A73C-4851-B721-B384411C99F4)
]
coclass CMyString
{
interface IMyString;
};
};
上面的例子中定義了一個IMyString接口繼承自IUnknown接口,函數(shù)參數(shù)列表中in表示參數(shù)為輸入?yún)?shù),out表示它為輸出參數(shù),retval表示該參數(shù)是函數(shù)的返回值。import導(dǎo)入了一個庫文件類似于include。而importlib導(dǎo)入一個tlb文件,我們可以將其看成VC++中的#pragma comment導(dǎo)入一個lib庫
從上面不難看出一個IDL文件至少有3個ID,一個是接口ID,一個是庫ID,還有一個就是實(shí)現(xiàn)類的ID
在VC環(huán)境中通過midl命令可以對該文件進(jìn)行編譯,編譯會生成下面幾個我們在編寫實(shí)現(xiàn)時會用到的重要文件:
- 一個.h文件:包含各個部分的聲明,以及接口的定義
- 一個_i.c文件:包含各個部分的定義,主要是各個GUI的定義
需要實(shí)現(xiàn)的導(dǎo)出函數(shù)
一般我們需要在dll文件中導(dǎo)出下面幾個全局的導(dǎo)出函數(shù):
STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);
STDAPI DllGetClassObject(const CLSID & rclsid, const IID & riid, void ** ppv);
STDAPI DllCanUnloadNow(void);
其中DllRegisterServer用來向注冊表中注冊模塊的相關(guān)信息,主要注測在HKEY_CLASSES_ROOT中,主要定義下面幾項(xiàng)內(nèi)容:
- 字符串名稱項(xiàng),該項(xiàng)中包含一個默認(rèn)值,一般給組件的字符串名稱;CLSID子健,一般給實(shí)現(xiàn)類的GUID;CurVer子健一般是子健的版本
- 以版本字符串為鍵的注冊表項(xiàng),該項(xiàng)中主要保存:默認(rèn)值,當(dāng)前版本的項(xiàng)目名稱;CLSID當(dāng)前版本庫的實(shí)現(xiàn)類的GUID
- 在HKEY_CLASSES_ROOT/CLSID子健中注冊以實(shí)現(xiàn)類GUID字符串為鍵的注冊表項(xiàng),里面主要包含:默認(rèn)值,組件字符串名稱;InprocServer32,組件所在模塊的全路徑;ProgID組件名稱;TypeLib組件類型庫的ID,也就是在定義IDL文件時,定義的實(shí)現(xiàn)庫的GUID。
下面是具體的定義:
const TCHAR *g_RegTable[][3] = {
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}"), 0, _T("FirstComLib.MyString")}, //組件ID
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\InprocServer32"), 0, (const TCHAR*)-1 }, //組建路徑
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\ProgID"), 0, _T("FirstComLib.MyString")}, //組件名稱
{ _T("CLSID\\{EBD699BA-A73C-4851-B721-B384411C99F4}\\TypeLib"), 0, _T("{ADF50A71-A8DD-4A64-8CCA-FFAEE2EC7ED2}") }, //類型庫ID
{ _T("FirstComLib.MyString"), 0, _T("FirstComLib.MyString") }, //組件的字符串名稱
{ _T("FirstComLib.MyString\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")}, //組件的CLSID
{ _T("FirstComLib.MyString\\CurVer"), 0, _T("FirstComLib.MyString.1.0") }, //組件版本
{ _T("FirstComLib.MyString.1.0"), 0, _T("FirstComLib.MyString") }, //當(dāng)前版本的項(xiàng)目名稱
{ _T("FirstComLib.MyString.1.0\\CLSID"), 0, _T("{EBD699BA-A73C-4851-B721-B384411C99F4}")} //當(dāng)前版本的CLSID
};
使用上一篇博文的代碼,來循環(huán)注冊這些項(xiàng)即可
DllGetClassObject:該函數(shù)用來生成對應(yīng)的工廠類,而工廠類負(fù)責(zé)產(chǎn)生對應(yīng)接口的實(shí)現(xiàn)類。
DllCanUnloadNow:函數(shù)用來詢問是否可以卸載對應(yīng)的dll,一般在COM中有兩個全局的引用計數(shù),用來記錄當(dāng)前內(nèi)存中有多少個模塊中的類,以及當(dāng)前有多少個線程在使用它,如果當(dāng)前沒有線程使用或者存在的對象數(shù)為0,則可以卸載
實(shí)現(xiàn)類的定義
實(shí)現(xiàn)部分的整體結(jié)構(gòu)圖如下:
由于所有類都派生自IUnknown,所在在這里就不顯示這個基類了。
每個實(shí)現(xiàn)類都對應(yīng)了一個它具體的類工廠,而項(xiàng)目中CMyString類的類廠的定義如下:
class CMyClassFactory : public IClassFactory
{
public:
CMyClassFactory();
~CMyClassFactory();
STDMETHOD(CreateInstance)(IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
STDMETHOD(LockServer)(BOOL isLock);
STDMETHOD(QueryInterface)(REFIID riid, void **ppvObject);
STDMETHOD_(ULONG, AddRef)(void);
STDMETHOD_(ULONG, Release)(void);
protected:
ULONG m_refs;
};
STDMETHOD宏展開如下:
#define STDMETHOD(method) virtual HRESULT __stdcall method
所以上面的代碼展開后就變成了:
virtual HRESULT __stdcall CreateInstance((IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
另外3個派生自IUnknown接口就沒什么好說的,主要說說另外兩個:
CreateInstance:主要用來生成對應(yīng)的實(shí)現(xiàn)類,然后再調(diào)用實(shí)現(xiàn)類——CMyString的QueryInterface函數(shù)生成對應(yīng)的接口
LockServer:當(dāng)前是否被鎖住:如果傳入的值為TRUE,則表示被鎖住,對應(yīng)的鎖計數(shù)器+1, 否則 -1
至于CMyString類的代碼與之前的大同小異,也就沒什么說的。
其他語言想要調(diào)用,以該項(xiàng)目為例,一般會經(jīng)歷下面幾個步驟:
- 調(diào)用對應(yīng)語言提供的產(chǎn)生接口的函數(shù),該函數(shù)參數(shù)一般是傳入一個組件的字符串名稱。如果要引用該項(xiàng)目中的組件則會傳入FirstComLib.MyString
- 在注冊表的HKEY_CLASSES_ROOT\組件字符串名\CLSID(比如HKEY_CLASSES_ROOT\FirstComLib.MyString\CLSID)中找到對應(yīng)的CLSID值
- 在HKEY_CLASSES_ROOT\CLSID\對應(yīng)ID\InprocServer32(CLSID\{EBD699BA-A73C-4851-B721-B384411C99F4}\InprocServer32)位置處找到對應(yīng)模塊的路徑
- 加載該模塊
- 根據(jù)IDL文件告知其他語言里面存在的接口,由語言調(diào)用對應(yīng)的創(chuàng)建接口的函數(shù)創(chuàng)建接口
- 調(diào)用模塊的導(dǎo)出函數(shù)DllGetClassObject將查詢到的CLSID作為第一個參數(shù),并將接口ID作為第二個參數(shù)傳入,得到一個接口
6.后面根據(jù)idl文件中的定義,直接調(diào)用接口中提供的函數(shù)
真實(shí)ATLCOM項(xiàng)目的解析
最后來看看一個正式的ATLCOM項(xiàng)目里面的內(nèi)容,來復(fù)習(xí)前面的內(nèi)容,首先通過VC創(chuàng)建一個ATLCOM的dll項(xiàng)目
在項(xiàng)目上右鍵-->New Atl Object,輸入接口名稱,IDE會根據(jù)名稱生成一個對應(yīng)的接口,還是以MyString接口為例,完成這一步后,整個項(xiàng)目的類結(jié)構(gòu)如下:
這些全局函數(shù)的作用與之前的相同,它里面多了一個_Module的全局對象,該對象類似于MFC中的CWinApp類,它用來表示整個項(xiàng)目的實(shí)例,里面封裝了對于引用計數(shù)的管理,以及對項(xiàng)目中各個接口注冊信息的管理,所以看DllRegisterServer等函數(shù)就會發(fā)現(xiàn)它們里面其實(shí)很簡單,大部分的工作都由_Module對象完成。
整個IDL文件的定義如下:
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(E3BD0C14-4D0C-48F2-8702-9F8DBC96E154),
dual,
helpstring("IMyString Interface"),
pointer_default(unique)
]
interface IMyString : IDispatch
{
};
[
uuid(A61AC54A-1B3D-4D8E-A679-00A89E2CBE93),
version(1.0),
helpstring("FirstAtlCom 1.0 Type Library")
]
library FIRSTATLCOMLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(11CBC0BE-B2B7-4B5C-A186-3C30C08A7736),
helpstring("MyString Class")
]
coclass MyString
{
[default] interface IMyString;
};
};
里面的內(nèi)容與上一次的內(nèi)容相差無幾,多了一個helpstring屬性,該屬性用于產(chǎn)生幫助信息,當(dāng)使用者在調(diào)用接口函數(shù)時IDE會將此提示信息顯示給調(diào)用者。
由于有系統(tǒng)框架給我們做的大量的工作,我們再也不用關(guān)心像引用計數(shù)的問題,只需要將精力集中在編寫接口的實(shí)現(xiàn)上,減少了不必要的工作量。
至此從結(jié)構(gòu)上說明了為了實(shí)現(xiàn)跨語言COM組件內(nèi)部做了哪些工作,當(dāng)然只有這些工作是肯定不夠的,后面會繼續(xù)說明它所做的另一塊工作——提供的一堆通用的變量類型。