引言
前幾天山寨了ATL的COM_INTERFACE,了解了一個COM類的如何進行通用的組織。今天再來學習下COM協議,看看如何實現一個COM組件——當然,也是不能用ATL的,不然就學不到什么了。
COM DLL說簡單簡單,說復雜也很復雜。說簡單呢,其實貌似只要導出下面這五個函數就可以了:
DllCanUnloadNow
DllGetClassObject
DllRegisterServer
DllUnregisterServer
DllInstall
(我有點懷疑但不確定DllInstall是不是后來加的,本文中我們先不理它。)
前四個函數中,后兩個是注冊與反注冊,就是寫寫注冊表的事情,簡單。前面兩個,特別是DllGetClassObject,比較關鍵。
先不研究這些,我們先按前兩天的方法寫個COM類吧。
準備一個COM類
首先,我們定義一個接口 ISampleInterface,以及一個類CSampleClass
Interface.h
#include <Unknwn.h>
struct __declspec(uuid("{83C783E3-F989-4E0D-BFC5-631273EDFFDA}")) ISampleInterface : public IUnknown { STDMETHOD(SampleMethod)() PURE; };
class __declspec(uuid("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}")) SampleClass; |
與前面不同的是,這里兩個聲明都寫上了一個UUID,前一個是接口ID(IID),后一個是類ID(CLSID)。這個文件將是提供給COM的使用者的。
SampleClass.h
class SampleClass : public xl::ComClass<SampleClass>, xl::IUnknownImpl<ISampleInterface> { public: SampleClass(); ~SampleClass();
public: STDMETHOD(SampleMethod)();
public: XL_COM_INTERFACE_BEGIN(SampleClass) XL_COM_INTERFACE(ISampleInterface) XL_COM_INTERFACE_END() }; |
SampleClass.cpp
SampleClass::SampleClass() { InterlockedIncrement(&g_nModuleCount); }
SampleClass::~SampleClass() { InterlockedDecrement(&g_nModuleCount); }
STDMETHODIMP SampleClass::SampleMethod() { MessageBox(NULL, _T("SampleMethod called."), _T("Info"), MB_OK | MB_ICONINFORMATION); return S_OK; }
|
SampleMethod 就簡單調用一個MessageBox意思一下。構造函數和析構函數中兩行先不看,后面解釋。
實現DllCanUnloadNow
這個函數的MSDN文檔見:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms690368.aspx
函數原型為:
HRESULT __stdcall DllCanUnloadNow(void);
函數實現的要求是,當此DLL內的所有COM對象都消亡了的時候,返回S_OK;如果還有COM對象存在,就返回S_FALSE。
現在我們有一個COM類SampleClass,它可能被創建一次,然后引用計數加加減減;也可能被創建多次,每個實例的引用計數同樣會被加加減減。當引用計數被減為0的時候,對象將消亡(析構函數被調用)。因此,我們在對象的構造和析構的地方埋點就可以了。
定義一個全局變量:
LONG g_nModuleCount = 0;
然后看到剛才的SampleClass的構造函數和析構函數中的灰色代碼,就可以存對象的創建/銷毀計數了。對于多個COM對象的情況,也可以這么搞。
然后,DllCanUnloadNow的實現就很簡單了:
STDAPI DllCanUnloadNow() { return g_nModuleCount > 0 ? S_FALSE : S_OK; } |
實現DllGetClassObject
終于到關鍵的地方了。以前一直弄不懂這個函數。它的MSDN文檔見:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms680760.aspx
函數原型為:
HRESULT __stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
第一個參數是要創建的對象的CLSID,很明確。第二個參數有點迷惑,MSDN原文是:
A reference to the identifier of the interface that the caller is to use to communicate with the class object. Usually, this is IID_IClassFactory (defined in the OLE headers as the interface identifier for IClassFactory).
“Usually”,它是IID_IClassFactory。我不知道有沒有不“Usually”的情況,也不知道這個接口原先的設計意圖是什么。如果可能,其實完全可以繞開類廠機制,直接用想要使用的那個接口的IID,貌似整套機制也能運轉……有木有達人解釋下它的淵源?
不過呢,目前我就把它當作IID_IClassFactory,其他一律不支持。
我們先要實現一個“類廠”——一個繼承于IClassFactory的COM類:
ClassFactory.h
class ClassFactory : public xl::ComClass<ClassFactory>, public xl::IClassFactoryImpl<> { public: ClassFactory(REFCLSID rclsid); ~ClassFactory();
public: STDMETHOD(CreateInstance)(_In_opt_ IUnknown *pUnkOuter, _In_ REFIID riid, _COM_Outptr_ void **ppvObject);
public: XL_COM_INTERFACE_BEGIN(ClassFactory) XL_COM_INTERFACE(IClassFactory) XL_COM_INTERFACE_END()
private: CLSID m_clsid; }; |
IClassFactory有兩個方法,CreateInstance和LockServer。我們只實現前者,它用于創建一個對象,第一個參數不理它,第二個參數是IID,第三個參數用于輸出。CLSID由構造函數傳入,保存在m_clsid中——類廠是與COM類一一對應的。
ClassFactory.cpp
ClassFactory::ClassFactory(REFCLSID rclsid) { m_clsid = rclsid; }
ClassFactory::~ClassFactory() {
}
STDMETHODIMP ClassFactory::CreateInstance(_In_opt_ IUnknown *pUnkOuter, _In_ REFIID riid, _COM_Outptr_ void **ppvObject) { if (riid == __uuidof(ISampleInterface) && m_clsid == __uuidof(SampleClass)) { ISampleInterface *p = new SampleClass; p->QueryInterface(riid, ppvObject);
return S_OK; }
return CLASS_E_CLASSNOTAVAILABLE; } |
這里只對IID為__uuidof(ISampleInterface)且CLSID為__uuidof(SampleClass))的情況作了響應,創建一個SampleClass對象。注意到創建對象之后有一次QueryInterface,在這里面會做一次AddRef操作,因此引用計數此時為1。其實AddRef可以不出現在代碼中,需要的時候就用QueryInterface代替。
這些都準備好了,最后來實現DllGetClassObject:
STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv) { if (riid == __uuidof(IClassFactory) && rclsid == __uuidof(SampleClass)) { IClassFactory *p = new ClassFactory(rclsid); p->QueryInterface(riid, ppv);
return S_OK; }
return CLASS_E_CLASSNOTAVAILABLE; }
|
實現DllRegisterServer和DllUnregisterServer
這個我就直接貼代碼了:
STDAPI DllRegisterServer(void) { TCHAR szModulePath[MAX_PATH] = {}; GetModuleFileName(g_hModule, szModulePath, ARRAYSIZE(szModulePath));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\InprocServer32"), _T(""), szModulePath);
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}\\ProgID"), _T(""), _T("Streamlet.COMProvider.SampleClass.1"));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1"), _T(""), _T("SampleClass Class"));
xl::Registry::SetString(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1\\CLSID"), _T(""), _T("{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"));
return S_OK; }
STDAPI DllUnregisterServer(void) { xl::Registry::DeleteKeyRecursion(HKEY_CLASSES_ROOT, _T("CLSID\\{0DECBFF5-A8A5-49E8-9962-3D18AAC6088E}"));
xl::Registry::DeleteKeyRecursion(HKEY_CLASSES_ROOT, _T("Streamlet.COMProvider.SampleClass.1"));
return S_OK; } |
注意,我這里并沒有寫得很全,只是注冊幾項必要的。至此,我們的COM組件實現完畢。
調用COM組件
編譯剛才的DLL,并使用regsvr32注冊(注意管理員權限)。然后寫一個小程序來調用之:
#include <tchar.h> #include <Objbase.h> #include "../COMProvider/Interface.h"
int _tmain(int argc, TCHAR *argv[]) { HRESULT hr = CoInitialize(NULL);
ISampleInterface *pSampleInterface = nullptr; hr = CoCreateInstance(__uuidof(SampleClass), nullptr, CLSCTX_INPROC_SERVER, __uuidof(ISampleInterface), (LPVOID *)&pSampleInterface);
if (SUCCEEDED(hr)) { pSampleInterface->SampleMethod(); pSampleInterface->Release(); }
CoUninitialize();
return 0; } |
運行結果:
上述例子代碼見COMProtocol.rar(http://pan.baidu.com/s/1c0GSI7u),庫依賴見http://xllib.codeplex.com/。當然,現在只是簡單地迎合了一下CoCreateInstance,還有許多其他事情要做,且聽下回分解。