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