COM元件的三種呼叫機制
這裡的COM元件可以是一個程序內伺服器(In-Process Server),也可以是一個程序外伺服器(Out-Of-Process Server)。一般情況下,我們在使用這些COM元件的時候,只要保證COM是正確註冊了,根本不用關心DLL是怎麼被load進來的,或者Exe的程序是被怎麼建立的,一切都交給系統提供的COM啟動機制,而之中用的最多的就是:
STDAPI CoCreateInstance(REFCLSID rclsid,LPUNKNOWN pUnkOuter,DWORD dwClsContext,REFIID riid,LPVOID * ppv);
它會幫你找到需要的DLL/Exe,載入或者啟動它們,然後建立你需要的那個COM物件。
一些智慧指標,如CComPtr, CComQIPtr, _com_ptr_t(XXXPtr)也提供了諸如CreateInstance的方法,歸根結底也是呼叫到此函式。
其實,啟動COM並不只有這麼一種方法,為了解決不同的問題,我們至少有三種啟動COM元件的方式,下面逐一介紹每種方式的啟動機理。
登錄檔方式(Registry)
這是啟用COM最早出現的方式,過程是完全封裝在CoCreateInstance函式裡面的,其大致過程如下:
- 根據傳入的CLSID在登錄檔中查詢其所在DLL或Exe
- 載入DLL或啟動Exe
- 建立類廠物件
- 根據類廠建立COM物件
這裡,第一步中登錄檔的資訊是在HKEY_CLASSES_ROOT\CLSID下面,根據下圖,我們不難得到某個CLSID所對應的元件。
Manifest檔案方式(Registry Free)
第一種方式要求COM元件註冊在登錄檔中,所以其是全域性共享的。可很多時候,我們不希望這樣,比如我們不想操作登錄檔,因為這涉及到許可權問題;比如我不想全域性共享,而是讓不同的應用程式能夠獨立的使用它所需要的版本。這就需要用到Registry-Free COM,我在關於registry-free COM的幾點侷限中介紹了這方面的知識。它和Registry COM最大的區別在於用manifest檔案代替了登錄檔。其實就是把原來放在登錄檔裡的資訊放到了manifest檔案中,manifest檔案可以和你的應用程式在同一目錄下,也可以作為resource嵌入到應用程式中。
一般情況下,你要為你的COM元件提供一個manifest,說明這個COM元件的檔名,支援的COM物件,介面和typelib等等, 這叫Assembly Manifest,如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<assemblyIdentity type="win32" name="ComDLL" version="1.0.0.0" /><file name = "ComDLL.dll">
<comClass clsid="{A1A70915-98B9-429F-A985-353452C664CE}" threadingModel = "Apartment" />
<typelib tlbid="{7B0B4D95-AF97-4D2A-8BA3-2CAABAA22E8A}" version="1.0" helpdir=""/>
</file><comInterfaceExternalProxyStub
name="IDLLTestObject"
iid="{A3F5D53C-3DC6-430B-89D8-5BED54B67718}"
proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
baseInterface="{00000000-0000-0000-C000-000000000046}"
tlbid = "{7B0B4D95-AF97-4D2A-8BA3-2CAABAA22E8A}" />
</assembly>
而應用程式則要提供一個manifest說明用到了哪些COM元件,這叫Application Manifest。如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type = "win32" name = "RegistryFreeWay" version = "1.0.0.0" />
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="COMDLL"
version="1.0.0.0" />
</dependentAssembly>
</dependency>
</assembly>
Registry Free COM的啟動也是封裝在CoCreateInstance函式中,其過程為:
- 根據傳入的CLSID在manifest檔案中查詢其所在的DLL
- 載入DLL
- 建立類廠物件
- 根據類廠建立COM物件
除了第一步,其過程與Registry COM的方式基本一致,但要注意的是,Registry Free COM是不支援程序外伺服器的。這是一個很大的遺憾,但是沒辦法,這是其內在缺陷,因為Registry Free COM是基於Activation Context的,而Activation Context是一種程序內的機制。
程式方式(Customized)
Registry Free COM是解決了程序內伺服器(dll)的side-by-side的問題,可是程序外伺服器呢(exe)?微軟並沒有提供這樣的機制,但是這並不是說我們只能把程序外伺服器作為全域性共享的元件註冊在登錄檔中了。既然知道CoCreateInstance的工作機制和exe的路徑,我們完全可以自己完成這個過程來建立COM元件。其過程如下:
- 確定程序外伺服器Exe的路徑,一般我們可以將其放在應用程式同目錄下
- 利用類廠物件建立COM物件
這裡CoGetClassObject之所以能成功,是因為在用CreateProcess啟動程序時,該程序外伺服器已經呼叫CoRegisterClassObject在class table中儲存了所有的類廠物件。當然,為了保證其side-by-side性,CoRegisterClassObject應該以REGCLS_SINGLEUSE的方式註冊物件:
REGCLS_SINGLEUSEAfter an application is connected to a class object with CoGetClassObject, the class object is removed from public view so that no other applications can connect to it.
到了這裡,我們不難發現程序內伺服器其實也可以用這種方式來啟動:
- 確定程序內伺服器DLL的路徑,一般我們可以將其放在應用程式同目錄下。
- 利用類廠物件建立COM物件
具體程式碼可以參考Demo:COMActivation\CustomizedWay\CustomizedWay.cpp
總結一下,有些比較常用的COM元件,如MSXML,DirectX等,都是採取全域性註冊的方式,這樣可以有效的節省磁碟和記憶體,但這些元件必須有非常好的後向相容性,而這是非常痛苦的一件事情。所以很多時候,我們會採用Registry Free的方式來side-by-side。而對於程序外伺服器,因為Registry Free COM不支援,我們可以自己模擬整個COM元件啟動的過程來實現side-by-side的目的。這三種方式各有所長,在一些大型的應用的,你可能會同時用到這三種方式。
更新:
最近在實現一個registry free的exe,本來想寫篇文章。後來覺得在這裡補充一下會比較好:
1. 你可以只需要一個Assembly Manifest, 用來宣告相關的檔案與介面,類等,並將其link
到目標app中,application manifest其實並不是必須的。
2. COM並不預設支援registry free的exe,所以你需要顯式的CreateProcess來啟動exe,並且你還需要提供一個manifest檔案來描述支援的介面和tlb檔案的位置: 因為涉及到兩個程序間的通訊,用tlb來做介面的marshalling是必須的, 如:
< file name="Server.tlb">
< typelib tlbid="{D98A091D-3A0F-4C3E-B36E-61F62068D488}" version="1.0" helpdir=""/>
< /file>
< comInterfaceExternalProxyStub name="IServer" iid="{4CA6F74F-B927-4694-806B-59E16C3FFA55}" tlbid="{D98A091D-3A0F-4C3E-B36E-61F62068D488}" proxyStubClsid32="{00020424-0000-0000-C000-000000000046}">
< /comInterfaceExternalProxyStub>
注意,你需要將所有的介面都列上,因為在marshalling某個介面的時候,需要通過這裡的說明找到對應的tlb檔案。 可以利用SDK提供的mt.exe來提取tlb的內容先。
3. 如果是程序內伺服器,你只需在Client端嵌入此manifest,但是如果是程序外伺服器,Client和Server都需要嵌入此manifest檔案,因為雙方都需要知道如何來做marshalling。
在測試過程中一定要保證你的exe和tlb都是沒有註冊的,不然會干擾你本機的結果,等拿到客戶那邊就比較危險了。