起源及復(fù)合文件
復(fù)合文件概念:
文件的 COM 結(jié)構(gòu)化存儲(chǔ)的實(shí)現(xiàn)。復(fù)合文件將單獨(dú)對(duì)象存儲(chǔ)在單一的、結(jié)構(gòu)化文件中,此文件由兩個(gè)主要元素組成:存儲(chǔ)對(duì)象和流對(duì)象。二者結(jié)合使用,可象文件內(nèi)的文件系統(tǒng)一樣起作用。
特點(diǎn):
(1)復(fù)合文件的內(nèi)部是使用指針構(gòu)造的一棵樹進(jìn)行管理的。使用的是單向指針,因此當(dāng)做定位操作的時(shí)候,向后定位比向前定位要快
(2)復(fù)合文件中的“流對(duì)象”,是真正保存數(shù)據(jù)的空間。它的存儲(chǔ)單位為512字節(jié)。
(3)不同的進(jìn)程,或同一個(gè)進(jìn)程的不同線程可以同時(shí)訪問(wèn)一個(gè)復(fù)合文件的不同部分而互不干擾
(4)復(fù)合文件則提供了非常方便的“增量訪問(wèn)”能力
(5)當(dāng)頻繁地刪除文件,復(fù)制文件后,磁盤空間會(huì)變的很零碎,需要使用磁盤整理工具進(jìn)行重新整合。和磁盤管理非常相似,復(fù)合文件也會(huì)產(chǎn)生這個(gè)問(wèn)題,在適當(dāng)?shù)臅r(shí)候也需要整理,但比較簡(jiǎn)單,只要調(diào)用一個(gè)函數(shù)就可以完成了
復(fù)合文件函數(shù)
復(fù)合文件的函數(shù)和磁盤目錄文件的操作非常類似。所有這些函數(shù),被分為3種類型:WIN API 全局函數(shù),存儲(chǔ) IStorage 接口函數(shù),流 IStream 接口函數(shù)(“接口”看成是完成一組相關(guān)操作功能的函數(shù)集合)
小結(jié):
復(fù)合文件,結(jié)構(gòu)化存儲(chǔ),是微軟組件思想的起源,在此基礎(chǔ)上繼續(xù)發(fā)展出了持續(xù)性、命名、ActiveX、對(duì)象嵌入、現(xiàn)場(chǎng)激活......一系列的新技術(shù)、新概念。因此理解和掌握復(fù)合文件是非常重要的,即使在你的程序中并沒(méi)有全面使用組件技術(shù),復(fù)合文件技術(shù)也是可以單獨(dú)被應(yīng)用的。
GUID 和接口
CLSID(注1)的方式間接描述這些對(duì)象數(shù)據(jù)的處理程序路徑。CLSID 其實(shí)就是一個(gè)號(hào)碼,或者說(shuō)是一個(gè)16字節(jié)的數(shù)。CLSID 的結(jié)構(gòu)定義如下:
typedef struct _GUID {
DWORD Data1; // 隨機(jī)數(shù)
WORD Data2; // 和時(shí)間相關(guān)
WORD Data3; // 和時(shí)間相關(guān)
BYTE Data4[8]; // 和網(wǎng)卡MAC相關(guān)
} GUID;
typedef GUID CLSID; // 組件ID
typedef GUID IID; // 接口ID
產(chǎn)生 CLSID
1. 如果使用開發(fā)環(huán)境編寫組件程序,則IDE會(huì)自動(dòng)幫你產(chǎn)生 CLSID;
2. 你可以手工寫 CLSID,但千萬(wàn)不要和人家已經(jīng)生成的 CLSID 重復(fù)呀,所以嚴(yán)重地不推薦
3. 程序中,可以用函數(shù) CoCreateGuid() 產(chǎn)生 CLSID;
4. 使用工具產(chǎn)生 GUID(注2);
微軟為了使用方便,也支持另一個(gè)字符串名稱方式,叫 ProgID。由于 CLSID 和 ProgID 其實(shí)是一個(gè)概念的兩個(gè)不同的表示形式,所以在程序中可以隨便使用任何一種。疑問(wèn)?!字符串名稱容易重復(fù)啊!!
介紹一下 CLSID 和 ProgID 之間的轉(zhuǎn)換方法和相關(guān)的函數(shù):
函數(shù)
|
功能說(shuō)明
|
CLSIDFromProgID()
CLSIDFromProgIDEx()
|
由 ProgID得到CLSID。沒(méi)什么好說(shuō)的,你自己都可以寫,查注冊(cè)表貝
|
ProgIDFromCLSID()
|
由 CLSID 得到 ProgID,調(diào)用者使用完成后要釋放 ProgID 的內(nèi)存(注5)
|
CoCreateGuid()
|
隨機(jī)生成一個(gè) GUID
|
IsEqualGUID()、IsEqualCLSID()、IsEqualIID()
|
比較2個(gè)ID是否相等
|
StringFromCLSID()、StringFromGUID2()、StringFromIID()
|
由 CLSID,IID 得到注冊(cè)表中CLSID樣式的字符串,注意釋放內(nèi)存
|
關(guān)于接口
COM是接口(組件)的集合,接口是方法和屬性的集合。 要了解COM,就得先了解IUnknown接口,IUnknown接口的C++形式的定義如下:
interface IUnknown
{
virtual HRESULT _stdcall QueryInterface([in]REFIID iid,[out]void * * ppv)=0;
virtual ULONG _stdcall AddRef(void)=0;
virtual ULONG _stdcall Release(void)=0;
}
她實(shí)現(xiàn)了“接口查詢”和“引用計(jì)數(shù)”,她是一個(gè)純抽象基類。 所有COM 定義的接口都必須從她繼承。
IUnknown是所有接口的基礎(chǔ),他負(fù)責(zé)兩項(xiàng)工作:
IUnknown::QueryInterface負(fù)責(zé)得到該組件的其他接口的指針
IUnknown::AddRef/Release負(fù)責(zé)管理該組件的生存期,但有人使用該組件時(shí),保證該組件不會(huì)被意外刪除;再?zèng)]人使用該組件時(shí),保證該組件被自動(dòng)刪除
下面是容器和組件之間的一個(gè)模擬對(duì)話過(guò)程:
|
容器 協(xié)商部分
|
組件 應(yīng)答部分
|
1
|
根據(jù)CLSID啟動(dòng)組件 。 CoCreateInstance()
|
生成對(duì)象,執(zhí)行構(gòu)造函數(shù),執(zhí)行初始化動(dòng)作。
|
2
|
你有IUnknown接口嗎?
|
有,給你!
|
3
|
恩,太好了,那么你有IPersistStorage接口嗎?(注9) IUnknown::QueryInterface(IID_IPersistStorage...)
|
沒(méi)有!
|
4
|
真差勁,連這個(gè)都沒(méi)有。那你有IPersistStreamInit接口嗎?(注10) IUnknown::QueryInterface(IID_IPersistStreamInit...)
|
哈,這個(gè)有,給!
|
5
|
好,好,這還差不多。你現(xiàn)在給我初始化吧。 IPersistStreamInit::InitNew()
|
OK,初始化完成了。
|
6
|
完成了?好!現(xiàn)在你讀數(shù)據(jù)去吧。 IPersistStreamInit::Load()
|
讀完啦。我根據(jù)數(shù)據(jù),已經(jīng)在窗口中顯示出來(lái)了。
|
7
|
好,現(xiàn)在咱們各自處理用戶的鼠標(biāo)、鍵盤消息吧......
|
......
|
8
|
哎呀!用戶要保存退出程序了。你的數(shù)據(jù)被用戶修改了嗎? IPersistStreamInit::IsDirty()
|
改了,用戶已經(jīng)修改啦。
|
9
|
那好,那么用戶修改后,你的數(shù)據(jù)需要多大的存儲(chǔ)空間呀? IPersistStreamInit::GetSizeMax()
|
恩,我算算呀......好了,總共需要500KB。
|
10
|
暈,你這么個(gè)小玩意居然占用這么大空間?!......好了,你可以存了。 IPersistStreamInit::Save()
|
謝謝,我已經(jīng)存好了。
|
11
|
恩。拜拜了您那。(注11) IPersistStreamInit::Release();IUnknown::Release()
|
執(zhí)行析構(gòu)函數(shù),刪除對(duì)象。
|
12
|
我自己也該退出了...... PostQuitMessage()
|
|
數(shù)據(jù)類型
簡(jiǎn)單調(diào)用組件
1、組件的啟動(dòng)和釋放
圖一 組件調(diào)用機(jī)制
由上圖可以看出,當(dāng)調(diào)用組件的時(shí)候,其實(shí)是依靠代理(運(yùn)行在本地)和存根(運(yùn)行在遠(yuǎn)端)之間的通訊完成的。具體來(lái)說(shuō),當(dāng)客戶程序通過(guò) CoCreateInstance() 函數(shù)啟動(dòng)組件,則代理接管該調(diào)用,它和存根通訊,存根則它所在的本地(相對(duì)于客戶程序來(lái)說(shuō)就是遠(yuǎn)程了)執(zhí)行 new 操作加載對(duì)象。遵守幾個(gè)原則:
(1)啟動(dòng)組件得到一個(gè)接口指針(Interface)后,不要調(diào)用AddRef()。因?yàn)橄到y(tǒng)知道你得到了一個(gè)指針,所以它已經(jīng)幫你調(diào)用了AddRef()函數(shù);
(2)通過(guò)QueryInterface()得到另一個(gè)接口指針后,不要調(diào)用AddRef()。因?yàn)?/span>......和上面的道理一樣;
(3)當(dāng)你把接口指針賦值給(保存到)另一個(gè)變量中的時(shí)候,請(qǐng)調(diào)用AddRef();
(4)當(dāng)不需要再使用接口指針的時(shí)候,務(wù)必執(zhí)行Release()釋放;
(5)當(dāng)使用智能指針的時(shí)候,可以省略指針的維護(hù)工作;
2、內(nèi)存分配和釋放
函數(shù)內(nèi)部根據(jù)實(shí)際需要?jiǎng)討B(tài)申請(qǐng)內(nèi)存,而調(diào)用者負(fù)責(zé)釋放。這雖然違背了上述原則,但 COM 從方便性和效率出發(fā),確實(shí)是這么設(shè)計(jì)的。
|
C語(yǔ)言
|
C++語(yǔ)言
|
Windows 平臺(tái)
|
COM
|
IMalloc 接口
|
BSTR
|
申請(qǐng)
|
malloc()
|
new
|
GlobalAlloc()
|
CoTaskMemAlloc()
|
Alloc()
|
SysAllocString()
|
重新申請(qǐng)
|
realloc()
|
|
GlobalReAlloc()
|
CoTaskRealloc()
|
Realloc()
|
SysReAllocString()
|
釋放
|
free()
|
delete
|
GlobalFree()
|
CoTaskMemFree()
|
Free()
|
SysFreeString()
|
以上這些函數(shù)必須要按類型配合使用(比如:new 申請(qǐng)的內(nèi)存,則必須用 delete 釋放)。在 COM 內(nèi)部,當(dāng)然你可以隨便使用任何類型的內(nèi)存分配釋放函數(shù),但組件如果需要與客戶進(jìn)行內(nèi)存的交互,則必須使用上表中的后三類函數(shù)族。
(1)BSTR 內(nèi)存在上回書中,已經(jīng)有比較豐富的介紹了,不再重復(fù);
(2)CoTaskXXX()函數(shù)族,其本質(zhì)上就是調(diào)用C語(yǔ)言的函數(shù)(malloc...);
(3)IMalloc 接口又是對(duì) CoTaskXXX() 函數(shù)族的一個(gè)包裝。包裝后,同時(shí)增強(qiáng)了一些功能,比如:IMalloc::GetSize()可以取得尺寸,使用 IMallocSpy 可以監(jiān)視內(nèi)存的使用;
3、參數(shù)傳遞方向
參數(shù)是動(dòng)態(tài)分配的內(nèi)存指針,那么遵守如下的規(guī)定:
方向
|
申請(qǐng)人
|
釋放人
|
提示
|
[in]
|
調(diào)用者
|
調(diào)用者
|
組件接收指針后,不能重新分配內(nèi)存
|
[out]
|
組件
|
調(diào)用者
|
組件返回指針后,調(diào)用者“愛(ài)咋咋地”(注3)
|
[in,out]
|
調(diào)用者
|
調(diào)用者
|
組件可以重新分配內(nèi)存
|
用 ATL 寫第一個(gè)組件(關(guān)于流程多寫代碼就熟悉了,這里不重復(fù)!)
1、建立 ATL 工程方法
圖一、建立 ATL DLL 工程
Dynamic Link Library(DLL) 表示建立一個(gè) DLL 的組件程序。
Executable(EXE) 表示建立一個(gè) EXE 的組件程序。
Service(EXE) 表示建立一個(gè)服務(wù)程序,系統(tǒng)啟動(dòng)后就會(huì)加載并執(zhí)行的程序。
Allow merging of proxy/stub code 選擇該項(xiàng)表示把“代理/存根”代碼合并到組件程序中,否則需要單獨(dú)編譯,單獨(dú)注冊(cè)代理存根程序。代理/存根,這個(gè)是什么概念?還記得我們?cè)?/span>上回書中介紹的嗎?當(dāng)調(diào)用者調(diào)用進(jìn)程外或遠(yuǎn)程組件功能的時(shí)候,其實(shí)是代理/存根負(fù)責(zé)數(shù)據(jù)交換的。關(guān)于代理/存根的具體變成和操作,以后再說(shuō)啦......
Support MFC 除非有特殊的原因,我們寫 ATL 程序,最好不要選擇該項(xiàng)。
2、增加 ATL 對(duì)象類方法
圖二、選擇建立簡(jiǎn)單COM對(duì)象
Category Object 普通組件。其中可以選擇的組件對(duì)象類型很多,但本質(zhì)上,就是讓向?qū)臀覀兡J(rèn)加上一些接口。比如我們選 "Simple Object",則向?qū)Ыo我們的組件加上 IUnknown 接口;我們選 "Internet Explorer Object",則向?qū)С思由?/span> IUnknown 接口外,再增加一個(gè)給 IE 所使用的 IObjectWithSite 接口。當(dāng)然了,我們完全可以手工增加任何接口。
Category Controls ActiveX 控件。其中可以選擇的 ActiveX 類型也很多。我們?cè)诤罄m(xù)的專門介紹 ActiveX 編程中再討論。
Category Miscellaneous 輔助雜類組件。
Categroy Data Access 數(shù)據(jù)庫(kù)類組件(我最討厭數(shù)據(jù)庫(kù)編程了,所以我也不會(huì))。
圖四、接口屬性
Threading Model 選擇組件支持的線程模型。默認(rèn)Apartment,它代表什么那?簡(jiǎn)單地說(shuō):當(dāng)在線程中調(diào)用組件函數(shù)的時(shí)候,這些調(diào)用會(huì)排隊(duì)進(jìn)行。因此,這種模式下,我們可以暫時(shí)不用考慮同步的問(wèn)題。
Interface 接口基本類型。Dual 表示支持雙接口(注2),這個(gè)非常 非常重要,非常非常常用,但我們今天不講。Custom 表示自定義借口。切記!切記!我們的這第一個(gè) COM 程序中,一定要選擇它!!!!(如果你選錯(cuò)了,請(qǐng)刪除全部?jī)?nèi)容,重新來(lái)過(guò)。)
Aggregation 我們寫的組件,將來(lái)是否允許被別人聚合(注3)使用。Only 表示必須被聚合才能使用,有點(diǎn)類似 C++ 中的純虛類,你要是總工程師,只負(fù)責(zé)設(shè)計(jì)但不親自寫代碼的話,才選擇它。
Support ISupportErrorInfo 是否支持豐富信息的錯(cuò)誤處理接口。以后就講。
Support Connection Points 是否支持連接點(diǎn)接口(事件、回調(diào))。以后就講。
Free Threaded Marshaler 暫時(shí)我也不知道
3、添加接口函數(shù)方法
編譯、注冊(cè)、調(diào)用
關(guān)于編譯
2-1 最小依賴
“最小依賴”,表示編譯器會(huì)把 ATL 中必須使用的一些函數(shù)靜態(tài)連接到目標(biāo)程序中。這樣目標(biāo)文件尺寸會(huì)稍大,但獨(dú)立性更強(qiáng),安裝方便;反之系統(tǒng)執(zhí)行的時(shí)候需要有 ATL.DLL 文件的支持。如何選擇設(shè)置為“最小依賴”呢?答案是:刪除預(yù)定義宏“_ATL_DLL”,操作方法見(jiàn)圖一、圖二。
圖一、在vc6.0中,設(shè)置方法
圖二、在 vc.net 2003中,設(shè)置方法
2-2 CRT庫(kù)
如果在 ATL 組件程序中調(diào)用了 CRT 的運(yùn)行時(shí)刻庫(kù)函數(shù),比如開平方 sqrt() ,那么編譯的時(shí)候可能會(huì)報(bào)錯(cuò)“error LNK2001: unresolved external symbol _main”。怎么辦?刪除預(yù)定義宏“_ATL_MIN_CRT”!操作方法也見(jiàn)圖一、圖二。(vc.net 2003 中的這個(gè)項(xiàng)目屬性叫“在 ATL 中最小使用 CRT”)
2-3 MBCS/UNICODE
這個(gè)不多說(shuō)了,在預(yù)定義宏中,分別使用 _MBCS 或 _UNICODE。
2-4 IDL 的編譯
COM 在設(shè)計(jì)初期,就定了一個(gè)目標(biāo):要能實(shí)現(xiàn)跨語(yǔ)言的調(diào)用。既然是跨語(yǔ)言的,那么組件的接口描述就必須在任何語(yǔ)言環(huán)境中都要能夠認(rèn)識(shí)。怎么辦?用 .h 文件描述?------ C語(yǔ)言程序員笑了,真方便!BASIC 程序員哭了:-( 因此,微軟使用了一個(gè)新的文件格式---IDL文件(接口定義描述語(yǔ)言)。IDL 是一個(gè)文本文件,它的語(yǔ)言語(yǔ)法比較簡(jiǎn)單,很象C。具體 IDL 文件的講解,見(jiàn)下一回《COM 組件設(shè)計(jì)與應(yīng)用(八)之添加新接口》。IDL 經(jīng)過(guò)編譯,生成二進(jìn)制的等價(jià)類型庫(kù)文件 TLB 提供給其它語(yǔ)言來(lái)使用。圖三示意了 ATL COM 程序編譯的過(guò)程:
圖三、ATL 組件程序編譯過(guò)程
說(shuō)明1:編譯后,類型庫(kù)以 TLB 文件形式單獨(dú)存在,同時(shí)也保存在目標(biāo)文件的資源中。因此,我們將來(lái)在 #import 引入類型庫(kù)的時(shí)候,既可以指定 TLB 文件,也可以指定目標(biāo)文件;
說(shuō)明2:我們作為 C/C++ 的程序員,還算是比較幸福的。因?yàn)?/span> IDL 編譯后,特意為我們提供了 C 語(yǔ)言形式的接口文件。
說(shuō)明3:IDL 編譯后生成代理/存根源程序,有:dlldata.c、xxx_p.c、xxxps.def、xxxps.mak,我們可以用 NMAKE.EXE 再次編譯來(lái)產(chǎn)生真正的代理/存根DLL目標(biāo)文件(注1)。
關(guān)于注冊(cè)
情況1:當(dāng)我們使用 ATL 編寫組件程序,注冊(cè)不用我們來(lái)負(fù)責(zé)。編譯成功后,IDE 會(huì)幫我們自動(dòng)注冊(cè);
情況2:當(dāng)我們使用 MFC 編寫組件程序,由于編譯器不知道你寫的是否是 COM 組件,所以它不會(huì)幫我們自動(dòng)注冊(cè)。這個(gè)時(shí)候,我們可以執(zhí)行菜單“Tools\Register Control”來(lái)注冊(cè)。
情況3:當(dāng)我們寫一個(gè)具有 COM 功能的 EXE 程序時(shí),注冊(cè)的方法就是運(yùn)行一次這個(gè)程序;
情況4:當(dāng)我們需要使用第三方提供的組件程序時(shí),可以命令行運(yùn)行“regsvr32.exe 文件名”來(lái)注冊(cè)。順便說(shuō)一句,反注冊(cè)的方法是“regsvr32.exe /u 文件名”;
情況5:當(dāng)我們需要在程序中(比如安裝程序)需要執(zhí)行注冊(cè),那么:
typedef HRESULT (WINAPI * FREG)();
TCHAR szWorkPath[ MAX_PATH ];
::GetCurrentDirectory( sizeof(szWorkPath), szWorkPath ); // 保存當(dāng)前進(jìn)程的工作目錄
::SetCurrentDirectory( 組件目錄 ); // 切換到組件的目錄
HMODULE hDLL = ::LoadLibrary( 組件文件名 ); // 動(dòng)態(tài)裝載組件
if(hDLL)
{
FREG lpfunc = (FREG)::GetProcAddress( hDLL, _T("DllRegisterServer") ); // 取得注冊(cè)函數(shù)指針
// 如果是反注冊(cè),可以取得"DllUnregisterServer"函數(shù)指針
if ( lpfunc ) lpfunc(); // 執(zhí)行注冊(cè)。這里為了簡(jiǎn)單,沒(méi)有判斷返回值
::FreeLibrary(hDLL);
}
::SetCurrentDirectory(szWorkPath); // 切換回原先的進(jìn)程工作目錄
上面的示例,在多數(shù)情況下可以簡(jiǎn)化掉切換工作目錄的代碼部分。但是,如果這個(gè)組件在裝載的時(shí)候,它需要同時(shí)加載一些必須依賴的DLL時(shí),有可能由于它自身程序的 BUG 導(dǎo)致無(wú)法正確定位。咳......還是讓我們自己寫的程序,來(lái)彌補(bǔ)它的錯(cuò)誤吧......誰(shuí)讓咱們是好人呢 ,誰(shuí)讓咱們的水平比他高呢,誰(shuí)讓咱們?cè)?/span> vckbase 上是個(gè)“榜眼”呢......
關(guān)于組件調(diào)用
總的來(lái)說(shuō),調(diào)用組件程序大概有如下方法:
#include 方法
|
IDL編譯后,為方便C/C++程序員的使用,會(huì)產(chǎn)生xxx.h和xxx_i.c文件。我們真幸福,直接#include后就可以使用了
|
#import 方法
|
比較通用的方法,vc 會(huì)幫我們產(chǎn)生包裝類,讓我們的調(diào)用更方便
|
加載類型庫(kù)包裝類 方法
|
如果組件提供了 IDispatch 接口,用這個(gè)方法調(diào)用組件是最簡(jiǎn)單的啦。不過(guò)還沒(méi)講IDispatch,只能看以后的文章啦
|
加載ActiveX包裝類 方法
|
ActiveX 還沒(méi)介紹呢,以后再說(shuō)啦
|
實(shí)現(xiàn)多接口
按照函數(shù)的功能進(jìn)行分類,把不同功能分類的函數(shù)用多個(gè)接口表現(xiàn)出來(lái)。這樣可以有如下的一些好處:
1、一個(gè)接口中的函數(shù)個(gè)數(shù)有限、功能集中,使用者容易學(xué)習(xí)、記憶和調(diào)用。一個(gè)接口到底提供多少個(gè)函數(shù)合適那?答案是:如果你是黑猩猩,那么一個(gè)接口最多3個(gè)函數(shù),如果你是人,那么一個(gè)接口最好不要超過(guò)7個(gè)函數(shù)。(注1)
2、容易維護(hù)。至少你肉眼搜索的時(shí)候也方便一些呀。
3、容易升級(jí)。當(dāng)我們給組件增加函數(shù)的時(shí)候,不要修改已經(jīng)發(fā)表的接口,而是提供一個(gè)新的接口來(lái)完成功能擴(kuò)展
實(shí)現(xiàn)
1 import "oaidl.idl";
2 import "ocidl.idl";
3 [
4 object,
5 uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
6 helpstring("IMathe Interface"),
7 pointer_default(unique)
8 ]
9 interface IMathe : IUnknown
10 {
11 [helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
12 };
13 [
14 uuid(CD7672F7-C0B4-4090-A2F8-234C0062F42C),
15 version(1.0),
16 helpstring("Simple3 1.0 Type Library")
17 ]
18 library SIMPLE3Lib
19 {
20 importlib("stdole32.tlb");
21 importlib("stdole2.tlb");
22 [
23 uuid(C6F241E2-43F6-4449-A024-B7340553221E),
24 helpstring("Mathe Class")
25 ]
26 coclass Mathe
27 {
28 [default] interface IMathe;
29 };
30 };
1-2
|
引入 IUnknown 和ATL已經(jīng)定義的其它接口描述文件。import 類似與 C 語(yǔ)言中的 #include
|
3-12
|
一個(gè)接口的完整描述
|
4
|
object 表示本塊描述的是一個(gè)接口。IDL文件是借用了PRC遠(yuǎn)程數(shù)據(jù)交換格式的說(shuō)明方法
|
5
|
uuid(......) 接口的 IID,這個(gè)值是 ATL 自動(dòng)生成的,可以手工修改或用 guidgen.exe 產(chǎn)生(注3)
|
6
|
在某些軟件或工具中,能看到這個(gè)提示
|
7
|
定義接口函數(shù)中參數(shù)所使用指針的默認(rèn)屬性(注4)
|
9
|
接口叫 IMathe 派生自 IUnknown,于是 IMathe 接口的頭三個(gè)函數(shù)一定就是QueryInterface,AddRef和Release
|
10-12
|
接口函數(shù)列表
|
13-30
|
類型庫(kù)的完整描述(類型庫(kù)的概念以后再說(shuō)),下面所說(shuō)明的行,是需要先了解的
|
18
|
#import 時(shí)候的默認(rèn)命名空間
|
23
|
組件的 CLSID,CoCreateInstance()的第一個(gè)參數(shù)就是它
|
27-29
|
接口列表
|
28
|
[default]表示誰(shuí)提供了IUnknown接口
|
手工修改IDL文件,黑體字部分是手工輸入的。完成后保存。
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(072EA6CA-7D08-4E7E-B2B7-B2FB0B875595),
helpstring("IMathe Interface"),
pointer_default(unique)
]
interface IMathe : IUnknown
{
[helpstring("method Add")] HRESULT Add([in] long n1, [in] long n2, [out,retval] long *pnVal);
};
[ // 所謂手工輸入,其實(shí)也是有技巧的:把上面的接口描述(IMathe)復(fù)制、粘貼下來(lái),然后再改更方便哈
object,
uuid(072EA6CB-7D08-4E7E-B2B7-B2FB0B875595), // 手工或用工具產(chǎn)生的 IID
helpstring("IStr Interface"),
pointer_default(unique)
]
interface IStr : IUnknown
{
// 目前還沒(méi)有任何接口函數(shù)
};
[
uuid(CD7672F7-C0B4-4090-A2F8-234C0062F42C),
version(1.0),
helpstring("Simple3 1.0 Type Library")
]
library SIMPLE3Lib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(C6F241E2-43F6-4449-A024-B7340553221E),
helpstring("Mathe Class")
]
coclass Mathe
{
[default] interface IMathe;
interface IStr; // 別忘了呦,這里還有一個(gè)那
};
};
3-4、打開頭文件(Mathe.h),手工增加類的派生關(guān)系和接口入口表 ,然后保存。
class ATL_NO_VTABLE CMathe :
public CComObjectRootEx <CComSingleThreadModel>,
public CComCoClass <CMathe, &CLSID_Mathe>,
public IMathe, // 別忘了,這里加一個(gè)逗號(hào)
public IStr // 增加一個(gè)基類
{
public:
CMathe()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_MATHE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMathe) // 接口入口表。這里填寫的接口,才能被QueryInterface()找到
COM_INTERFACE_ENTRY(IMathe)
COM_INTERFACE_ENTRY(IStr)
END_COM_MAP()
3-5、好了,一切就緒。接下來(lái),就可以在 IStr 接口中增加函數(shù)了。
IDispatch 接口
自動(dòng)化組件,其實(shí)就是實(shí)現(xiàn)了 IDispatch 接口的組件。IDispatch 接口有4個(gè)函數(shù),解釋語(yǔ)言的執(zhí)行器就通過(guò)這僅有的4個(gè)函數(shù)來(lái)執(zhí)行組件所提供的功能。IDispatch 接口用 IDL 形式說(shuō)明如下:(注1)
[
object,
uuid(00020400-0000-0000-C000-000000000046), // IDispatch 接口的 IID = IID_IDispatch
pointer_default(unique)
]
interface IDispatch : IUnknown
{
typedef [unique] IDispatch * LPDISPATCH; // 轉(zhuǎn)定義 IDispatch * 為 LPDISPATCH
HRESULT GetTypeInfoCount([out] UINT * pctinfo); // 有關(guān)類型庫(kù)的這兩個(gè)函數(shù),咱們以后再說(shuō)
HRESULT GetTypeInfo([in] UINT iTInfo,[in] LCID lcid,[out] ITypeInfo ** ppTInfo);
HRESULT GetIDsOfNames( // 根據(jù)函數(shù)名字,取得函數(shù)序號(hào)(DISPID)
[in] REFIID riid,
[in, size_is(cNames)] LPOLESTR * rgszNames,
[in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID * rgDispId
);
[local] // 本地版函數(shù)
HRESULT Invoke( // 根據(jù)函數(shù)序號(hào),解釋執(zhí)行函數(shù)功能
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * puArgErr
);
[call_as(Invoke)] // 遠(yuǎn)程版函數(shù)
HRESULT RemoteInvoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] DWORD dwFlags,
[in] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * pArgErr,
[in] UINT cVarRef,
[in, size_is(cVarRef)] UINT * rgVarRefIdx,
[in, out, size_is(cVarRef)] VARIANTARG * rgVarRef
);
}
用 MFC 實(shí)現(xiàn)自動(dòng)化組件
3-1:建立一個(gè)工作區(qū)(Workspace)
3-2:建立一個(gè) MFC DLL 工程(Project),工程名稱為“Simple5”
3-3:一定要選擇 automation,切記!切記!
3-4:建立新類
3-5:在新建類中支持automation
Class information - Name 你隨便寫個(gè)類名子啦
Class information - Base class 一定要從 CComTarget 派生呀,只有它才提供了 IDispatch 的支持
Automation - None 表示不支持自動(dòng)化,你要選擇了它,那就白干啦
Automation - Automation 支持自動(dòng)化,但不能被直接實(shí)例化。后面在講解多個(gè) IDispatch 的時(shí)候就用到它了,現(xiàn)在先不要著急。
Automation - Createable by type ID 一定要選擇這個(gè)項(xiàng)目,這樣我們?cè)诤竺娴恼{(diào)用中,VB就能夠CreateObject(),VC就能夠CreateDispatch()對(duì)組件對(duì)象實(shí)例化了。注意一點(diǎn),這個(gè) ID 其實(shí)就是組件的 ProgID 啦。
3-6:?jiǎn)?dòng) ClassWizard,選擇 Automation 卡片,準(zhǔn)備建立函數(shù)
3-7:添加函數(shù)。我們要寫一個(gè)整數(shù)加法函數(shù)Add()。
IDispatch 及雙接口的調(diào)用 雙接口表示在一個(gè)接口中,同時(shí)支持自定義接口和 IDispatch
、IDispatch 接口和雙接口
使用者要想調(diào)用普通的 COM 組件功能,必須要加載這個(gè)組件的類型庫(kù)(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在腳本程序中,由于腳本是被解釋執(zhí)行的,所以無(wú)法使用加載類型庫(kù)的方式進(jìn)行預(yù)編譯。那么腳本解釋器如何使用 COM 組件那?這就是自動(dòng)化(IDispatch)組件大顯身手的地方了。IDispatch 接口需要實(shí)現(xiàn)4個(gè)函數(shù),調(diào)用者只通過(guò)這4個(gè)函數(shù),就能實(shí)現(xiàn)調(diào)用自動(dòng)化組件中所有的函數(shù)。這4個(gè)函數(shù)功能如下:
HRESULT GetTypeInfoCount( [out] UINT * pctinfo)
|
組件中提供幾個(gè)類型庫(kù)?當(dāng)然一般都是一個(gè)啦。 但如果你在一個(gè)組件中實(shí)現(xiàn)了多個(gè) IDispatch 接口,那就不一定啦(注1)
|
HRESULT GetTypeInfo( [in] UINT iTInfo, [in] LCID lcid, [out] ITypeInfo ** ppTInfo)
|
調(diào)用者通過(guò)該函數(shù)取得他想要的類型庫(kù)。 幸好,在 99% 的情況下,我們都不用關(guān)心這兩個(gè)函數(shù)的實(shí)現(xiàn),因?yàn)?/span> MFC/ATL 都幫我們完成了默認(rèn)的一個(gè)實(shí)現(xiàn),如果是自己完成函數(shù)代碼,甚至可以直接返回 E_NOTIMPL 表示沒(méi)有實(shí)現(xiàn)。(注2)
|
HRESULT GetIDsOfNames( [in] REFIID riid, [in,size_is(cNames)] LPOLESTR * rgszNames, [in] UINT cNames, [in] LCID lcid, [out,size_is(cNames)] DISPID * rgDispId)
|
根據(jù)函數(shù)名稱取得函數(shù)序號(hào),為調(diào)用 Invoke() 做準(zhǔn)備。 所謂函數(shù)序號(hào),大家去觀察雙接口 IDL 文件和 MFC 的 ODL 文件,每一個(gè)函數(shù)和屬性都會(huì)有 [id(序號(hào))....] 這樣的描述。
|
HRESULT Invoke( [in] DISPID dispIdMember, [in] REFIID riid, [in] LCID lcid, [in] WORD wFlags, [in,out] DISPPARAMS * pDispParams, [out] VARIANT * pVarResult, [out] EXCEPINFO * pExcepInfo, [out] UINT * puArgErr)
|
根據(jù)序號(hào),執(zhí)行函數(shù)。 使用 MFC/ATL 寫的組件程序,我們也不必關(guān)心這個(gè)函數(shù)的實(shí)現(xiàn)。如果是自己寫代碼,則該函數(shù)類似如下實(shí)現(xiàn): switch(dispIdMember) { case 1: .....; break; case 2: .....; break; .... } 其實(shí),就是根據(jù)序號(hào)進(jìn)行分支調(diào)用啦。(注3)
|
從 Invoke() 函數(shù)的實(shí)現(xiàn)就可以看出,使用 IDispatch 接口的程序,其執(zhí)行效率是比較低的。ATL 從效率出發(fā),實(shí)現(xiàn)了一種叫“雙接口(dual)”的接口模式。下面我們來(lái)看看,到底什么是雙接口:
圖一、雙接口(dual) 結(jié)構(gòu)示意圖
從上圖中可以看出,所謂雙接口,其實(shí)是在一個(gè) VTAB 的虛函數(shù)表中容納了三個(gè)接口(因?yàn)槿魏谓涌诙际菑?/span> IUnknown 派生的,所以就不強(qiáng)調(diào) IUnknown 了,叫做雙接口)。我們?nèi)绻麖娜我庖粋€(gè)接口中調(diào)用 QueryInterface()得到另外的接口指針的話,其實(shí),得到的指針地址都是同一個(gè)。雙接口有什么好處那?答:好呀,多好呀,特別好呀......
使用方式
|
因?yàn)?/span>
|
所以
|
腳本語(yǔ)言使用組件
|
解釋器只認(rèn)識(shí) IDispatch 接口
|
可以調(diào)用,但執(zhí)行效率最低
|
編譯型語(yǔ)言使用組件
|
它認(rèn)識(shí) IDispatch 接口
|
可以調(diào)用,執(zhí)行效率比較低
|
編譯型語(yǔ)言使用組件
|
它裝載類型庫(kù)后,就認(rèn)識(shí)了 Ixxx 接口
|
可以直接調(diào)用 Ixxx 函數(shù),效率最高啦
|
結(jié)論
|
雙接口,既滿足腳本語(yǔ)言的使用方便性,又滿足編譯型語(yǔ)言的使用高效性。 于是,我們寫的所有的 COM 組件接口,都用雙接口實(shí)現(xiàn)嗎? 錯(cuò)!否!NO! 如果不是明確非要支持腳本的調(diào)用,則最好不要使用雙接口,因?yàn)椋?/span>
|
如果所有函數(shù)都放在一個(gè)雙接口中,那么層次、結(jié)構(gòu)、分類不清
|
如果使用多個(gè)雙接口,則會(huì)產(chǎn)生其它問(wèn)題(注4)
|
雙接口、IDispatch接口只支持自動(dòng)化的參數(shù)類型,使用受到限制,某些情況下很不方便嘍
|
還有很多弊病呦,不過(guò)現(xiàn)在我想不起來(lái)嘍......
|
使用方法
示例程序
|
自動(dòng)化組件的使用方式
|
簡(jiǎn)要說(shuō)明
|
示例0
|
在腳本中調(diào)用
|
在第九回/第十回中,已經(jīng)做了介紹
|
示例1
|
使用 API 方式調(diào)用
|
揭示 IDispatch 的調(diào)用原理,但傻子才去這么使用那,會(huì)累死了
|
示例2
|
使用 CComDispatchDriver 的智能指針包裝類
|
比直接使用 API 方式要簡(jiǎn)單多啦,這個(gè)不錯(cuò)!
|
示例3
|
使用 MFC 裝載類型庫(kù)的包裝方式
|
簡(jiǎn)單!好用!常用!但它本質(zhì)上是使用 IDispatch 接口,所以執(zhí)行效率稍差
|
示例4
|
使用 #import 方式加載類型庫(kù)方式
|
#import 方式使用組件,咱們?cè)?/span>第七回中講過(guò)啦。常用!對(duì)雙接口組件,直接調(diào)用自定義接口函數(shù),不再經(jīng)過(guò) IDispatch,因此執(zhí)行效率最高啦
|
錯(cuò)誤與異常處理
事件和通知
流程:
客戶端啟動(dòng)組件(Simple11.IEvent1.1)并得到接口指針 IEvent1 *;
調(diào)用接口方法 IEvent1::Advise() 把客戶端內(nèi)部的一個(gè)接收器(sink)接口指針(ICallBack *)傳遞到組件服務(wù)器中;
調(diào)用 IEvent1::Add() 去計(jì)算兩個(gè)整數(shù)的和;
但是計(jì)算結(jié)果并不通過(guò)該函數(shù)返回,而是通過(guò) ICallBack::Fire_Result() 返回給客戶端;
當(dāng)客戶端不再需要接受事件的時(shí)候,調(diào)用 IEvent1::Unadvise() 斷開和組件的聯(lián)系。
連接點(diǎn)
持續(xù)性
屬性包