COM是基于二進(jìn)制的組件模塊,從設(shè)計(jì)之初就以支持所有語(yǔ)言作為它的一個(gè)目標(biāo),這篇文章主要探討COM的跨語(yǔ)言部分。
idl文件
一般COM接口的實(shí)現(xiàn)肯定是以某一具體語(yǔ)言來(lái)實(shí)現(xiàn)的胶哲,比如說(shuō)使用VC++語(yǔ)言,這就造成了一個(gè)問(wèn)題潭辈,不同的語(yǔ)言對(duì)于接口的定義鸯屿,各個(gè)變量的定義各不相同,如何讓使用vc++或者說(shuō)Java等其他語(yǔ)言定義的接口能被別的語(yǔ)言識(shí)別萎胰?為了達(dá)到這個(gè)要求碾盟,定義了一種文件格式idl——(Interface Definition Language)接口定義語(yǔ)言,IDL提供一套通用的數(shù)據(jù)類型技竟,并以這些數(shù)據(jù)類型來(lái)定義更為復(fù)雜的數(shù) 據(jù)類型冰肴。一般來(lái)說(shuō),一個(gè)文件有下面幾個(gè)部分說(shuō)明
- 接口的定義
- 組件庫(kù)的定義
- 實(shí)現(xiàn)類的定義
而各個(gè)部分又包括他們的屬性定義榔组,以及函數(shù)成員的定義
屬性:
屬性是在接口定義的上方熙尉,使用“[]”符號(hào)包裹,一般在屬性中使用下面幾個(gè)關(guān)鍵字:
object:標(biāo)明該部分是一個(gè)對(duì)象(可以理解為c++中的對(duì)象搓扯,包括接口和具體的實(shí)現(xiàn)類)
uuid:標(biāo)明該部分的GUID
version:該部分的版本
接口定義
接口定義采用關(guān)鍵字interface检痰,接口函數(shù)定義在一對(duì)大括號(hào)中,它的定義與類的定義相似锨推,其中函數(shù)定義需要修飾函數(shù)各個(gè)參數(shù)的作用铅歼,比如使用in 表示它作為輸入?yún)?shù),out表示作為輸出參數(shù)换可,retval表示該參數(shù)作為返回值椎椰,一般在VC++定義的接口中,函數(shù)返回值為HRESULT沾鳄,但是需要返回一個(gè)值供外界調(diào)用慨飘,此時(shí)就使用輸出參數(shù)并加上retval表示它將在其他語(yǔ)言中作為函數(shù)的返回值。
組件庫(kù)定義
庫(kù)使用library關(guān)鍵字定義译荞,在定義庫(kù)的時(shí)候瓤的,它的屬性一般定義GUID和版本信息,而在庫(kù)中通常定義庫(kù)中的實(shí)現(xiàn)類的相關(guān)信息吞歼,庫(kù)中的信息也是寫(xiě)在一對(duì)大括號(hào)中
實(shí)現(xiàn)類的定義
接口實(shí)現(xiàn)類使用關(guān)鍵字coclass圈膏,接口類的屬性一般定義一個(gè)object,一個(gè)GUID篙骡,然后一般定義實(shí)現(xiàn)類不需要向在C++中那樣定義它的各個(gè)接口本辐,各個(gè)數(shù)據(jù)成員桥帆,只需要告知它實(shí)現(xiàn)哪些接口即可医增,也就是說(shuō)它繼承自哪些接口慎皱。
下面是一個(gè)具體的例子:
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;
};
};
上面的例子中定義了一個(gè)IMyString接口繼承自IUnknown接口,函數(shù)參數(shù)列表中in表示參數(shù)為輸入?yún)?shù),out表示它為輸出參數(shù)叶骨,retval表示該參數(shù)是函數(shù)的返回值茫多。import導(dǎo)入了一個(gè)庫(kù)文件類似于include。而importlib導(dǎo)入一個(gè)tlb文件忽刽,我們可以將其看成VC++中的#pragma comment導(dǎo)入一個(gè)lib庫(kù)
從上面不難看出一個(gè)IDL文件至少有3個(gè)ID天揖,一個(gè)是接口ID,一個(gè)是庫(kù)ID跪帝,還有一個(gè)就是實(shí)現(xiàn)類的ID
在VC環(huán)境中通過(guò)midl命令可以對(duì)該文件進(jìn)行編譯今膊,編譯會(huì)生成下面幾個(gè)我們?cè)诰帉?xiě)實(shí)現(xiàn)時(shí)會(huì)用到的重要文件:
- 一個(gè).h文件:包含各個(gè)部分的聲明,以及接口的定義
- 一個(gè)_i.c文件:包含各個(gè)部分的定義伞剑,主要是各個(gè)GUI的定義
需要實(shí)現(xiàn)的導(dǎo)出函數(shù)
一般我們需要在dll文件中導(dǎo)出下面幾個(gè)全局的導(dǎo)出函數(shù):
STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);
STDAPI DllGetClassObject(const CLSID & rclsid, const IID & riid, void ** ppv);
STDAPI DllCanUnloadNow(void);
其中DllRegisterServer用來(lái)向注冊(cè)表中注冊(cè)模塊的相關(guān)信息斑唬,主要注測(cè)在HKEY_CLASSES_ROOT中,主要定義下面幾項(xiàng)內(nèi)容:
- 字符串名稱項(xiàng)黎泣,該項(xiàng)中包含一個(gè)默認(rèn)值恕刘,一般給組件的字符串名稱;CLSID子健抒倚,一般給實(shí)現(xiàn)類的GUID褐着;CurVer子健一般是子健的版本
- 以版本字符串為鍵的注冊(cè)表項(xiàng),該項(xiàng)中主要保存:默認(rèn)值托呕,當(dāng)前版本的項(xiàng)目名稱含蓉;CLSID當(dāng)前版本庫(kù)的實(shí)現(xiàn)類的GUID
- 在HKEY_CLASSES_ROOT/CLSID子健中注冊(cè)以實(shí)現(xiàn)類GUID字符串為鍵的注冊(cè)表項(xiàng),里面主要包含:默認(rèn)值项郊,組件字符串名稱;InprocServer32馅扣,組件所在模塊的全路徑;ProgID組件名稱呆抑;TypeLib組件類型庫(kù)的ID岂嗓,也就是在定義IDL文件時(shí),定義的實(shí)現(xiàn)庫(kù)的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}") }, //類型庫(kù)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
};
使用上一篇博文的代碼厌殉,來(lái)循環(huán)注冊(cè)這些項(xiàng)即可
DllGetClassObject:該函數(shù)用來(lái)生成對(duì)應(yīng)的工廠類,而工廠類負(fù)責(zé)產(chǎn)生對(duì)應(yīng)接口的實(shí)現(xiàn)類侈咕。
DllCanUnloadNow:函數(shù)用來(lái)詢問(wèn)是否可以卸載對(duì)應(yīng)的dll公罕,一般在COM中有兩個(gè)全局的引用計(jì)數(shù),用來(lái)記錄當(dāng)前內(nèi)存中有多少個(gè)模塊中的類耀销,以及當(dāng)前有多少個(gè)線程在使用它楼眷,如果當(dāng)前沒(méi)有線程使用或者存在的對(duì)象數(shù)為0,則可以卸載
實(shí)現(xiàn)類的定義
實(shí)現(xiàn)部分的整體結(jié)構(gòu)圖如下:
由于所有類都派生自IUnknown,所在在這里就不顯示這個(gè)基類了罐柳。
每個(gè)實(shí)現(xiàn)類都對(duì)應(yīng)了一個(gè)它具體的類工廠掌腰,而項(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宏展開(kāi)如下:
#define STDMETHOD(method) virtual HRESULT __stdcall method
所以上面的代碼展開(kāi)后就變成了:
virtual HRESULT __stdcall CreateInstance((IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
另外3個(gè)派生自IUnknown接口就沒(méi)什么好說(shuō)的,主要說(shuō)說(shuō)另外兩個(gè):
CreateInstance:主要用來(lái)生成對(duì)應(yīng)的實(shí)現(xiàn)類张吉,然后再調(diào)用實(shí)現(xiàn)類——CMyString的QueryInterface函數(shù)生成對(duì)應(yīng)的接口
LockServer:當(dāng)前是否被鎖壮萘骸:如果傳入的值為TRUE,則表示被鎖住肮蛹,對(duì)應(yīng)的鎖計(jì)數(shù)器+1勺择, 否則 -1
至于CMyString類的代碼與之前的大同小異,也就沒(méi)什么說(shuō)的伦忠。
其他語(yǔ)言想要調(diào)用省核,以該項(xiàng)目為例,一般會(huì)經(jīng)歷下面幾個(gè)步驟:
- 調(diào)用對(duì)應(yīng)語(yǔ)言提供的產(chǎn)生接口的函數(shù)昆码,該函數(shù)參數(shù)一般是傳入一個(gè)組件的字符串名稱气忠。如果要引用該項(xiàng)目中的組件則會(huì)傳入FirstComLib.MyString
- 在注冊(cè)表的HKEY_CLASSES_ROOT\組件字符串名\CLSID(比如HKEY_CLASSES_ROOT\FirstComLib.MyString\CLSID)中找到對(duì)應(yīng)的CLSID值
- 在HKEY_CLASSES_ROOT\CLSID\對(duì)應(yīng)ID\InprocServer32(CLSID\{EBD699BA-A73C-4851-B721-B384411C99F4}\InprocServer32)位置處找到對(duì)應(yīng)模塊的路徑
- 加載該模塊
- 根據(jù)IDL文件告知其他語(yǔ)言里面存在的接口,由語(yǔ)言調(diào)用對(duì)應(yīng)的創(chuàng)建接口的函數(shù)創(chuàng)建接口
- 調(diào)用模塊的導(dǎo)出函數(shù)DllGetClassObject將查詢到的CLSID作為第一個(gè)參數(shù)未桥,并將接口ID作為第二個(gè)參數(shù)傳入笔刹,得到一個(gè)接口
6.后面根據(jù)idl文件中的定義,直接調(diào)用接口中提供的函數(shù)
真實(shí)ATLCOM項(xiàng)目的解析
最后來(lái)看看一個(gè)正式的ATLCOM項(xiàng)目里面的內(nèi)容冬耿,來(lái)復(fù)習(xí)前面的內(nèi)容舌菜,首先通過(guò)VC創(chuàng)建一個(gè)ATLCOM的dll項(xiàng)目
在項(xiàng)目上右鍵-->New Atl Object,輸入接口名稱亦镶,IDE會(huì)根據(jù)名稱生成一個(gè)對(duì)應(yīng)的接口日月,還是以MyString接口為例,完成這一步后缤骨,整個(gè)項(xiàng)目的類結(jié)構(gòu)如下:
這些全局函數(shù)的作用與之前的相同爱咬,它里面多了一個(gè)_Module的全局對(duì)象,該對(duì)象類似于MFC中的CWinApp類绊起,它用來(lái)表示整個(gè)項(xiàng)目的實(shí)例精拟,里面封裝了對(duì)于引用計(jì)數(shù)的管理,以及對(duì)項(xiàng)目中各個(gè)接口注冊(cè)信息的管理虱歪,所以看DllRegisterServer等函數(shù)就會(huì)發(fā)現(xiàn)它們里面其實(shí)很簡(jiǎn)單蜂绎,大部分的工作都由_Module對(duì)象完成。
整個(gè)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)容相差無(wú)幾笋鄙,多了一個(gè)helpstring屬性师枣,該屬性用于產(chǎn)生幫助信息,當(dāng)使用者在調(diào)用接口函數(shù)時(shí)IDE會(huì)將此提示信息顯示給調(diào)用者萧落。
由于有系統(tǒng)框架給我們做的大量的工作践美,我們?cè)僖膊挥藐P(guān)心像引用計(jì)數(shù)的問(wèn)題洗贰,只需要將精力集中在編寫(xiě)接口的實(shí)現(xiàn)上,減少了不必要的工作量陨倡。
至此從結(jié)構(gòu)上說(shuō)明了為了實(shí)現(xiàn)跨語(yǔ)言COM組件內(nèi)部做了哪些工作敛滋,當(dāng)然只有這些工作是肯定不夠的,后面會(huì)繼續(xù)說(shuō)明它所做的另一塊工作——提供的一堆通用的變量類型玫膀。