三、純手工創建一個COM組件
1、從建工程到實現注冊
在這一過程中我們將完成三個步驟:創建dll的入口函數,定義接口文件,實現注冊功能
1.1創建一個類型為win32 dll工程
創建一個名為MathCOM的win32 dll工程。
在向導的第二步選擇"A smiple dll project"選項。當然如果你選擇一個空的工程,那你自己完成DllMain定義吧。
1.2 定義接口文件
生成一個名為MathCOM.idl的接口文件。並將此文件加入到剛才創建的那個工程裡。
//MathCOM.idl文件
// MathCOM.idl : IDL source for MathCOM.dll
//
// This file will be processed by the MIDL tool to
// produce the type library (MathCOM.tlb) and marshalling code.
import "oaidl.idl";
import "ocidl.idl";
[
uuid(FAEAE6B7-67BE-42a4-A318-3256781E945A),
helpstring("ISimpleMath Interface"),
object,
pointer_default(unique)
]
interface ISimpleMath : IUnknown
{
HRESULT Add([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
HRESULT Subtract([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
HRESULT Multiply([in]int nOp1,[in]int nOp2,[out,retval] int * pret);
HRESULT Divide([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
};
[
uuid(01147C39-9DA0-4f7f-B525-D129745AAD1E),
helpstring("IAdvancedMath Interface"),
object,
pointer_default(unique)
]
interface IAdvancedMath : IUnknown
{
HRESULT Factorial([in]int nOp1,[out,retval]int * pret);
HRESULT Fabonacci([in]int nOp1,[out,retval]int * pret);
};
[
uuid(CA3B37EA-E44A-49b8-9729-6E9222CAE844),
version(1.0),
helpstring("MATHCOM 1.0 Type Library")
]
library MATHCOMLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(3BCFE27E-C88D-453C-8C94-F5F7B97E7841),
helpstring("MATHCOM Class")
]
coclass MATHCOM
{
[default] interface ISimpleMath;
interface IAdvancedMath;
};
};
在編譯此工程之前請檢查Project/Setting/MIDL中的設置。正確設置如下圖:
圖1.4 midl的正確設置
在正確設置後,如編譯無錯誤,那麼將在工程的目錄下產生四個
文件名 作用 MathCOM.h 接口的頭文件,如果想聲明或定義接口時使用此文件 MathCOM_i.c 定義了接口和類對象以及庫,只有在要使用到有關與GUID有關的東西時才引入此文件,此文件在整個工程中只能引入一次,否則會有重復定義的錯誤 MathCOM_p.c 用於存根與代理 dlldata.c 不明
1.3 增加注冊功能
作為COM必須要注冊與注銷的功能。
1.3.1 增加一個MathCOM.def文件
DEF文件是模塊定義文件(Module Definition File)。它允許引出符號被化名為不同的引入符號。
//MathCOM.def文件
; MathCOM.def : Declares the module parameters.
LIBRARY "MathCOM.DLL"
EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
DllRegisterServer @3 PRIVATE
DllUnregisterServer @4 PRIVATE
DllUnregisterServer 這是函數名稱 @4<――這是函數序號 PRIVATE
接下來大致介紹一下DllRegisterServer()和DllUnregisterServer()。(其他兩個函數的作用將在後面介紹)
1.3.2 DllRegisterServer()和DllUnregisterServer()
DllRegisterServer() 函數的作用是將COM服務器注冊到本機上。
DllUnregisterServer() 函數的作用是將COM服務器從本機注銷。
1.4 MathCOM.cpp文件
現在請將 MathCOM.cpp 文件修改成如下:
// MATHCOM.cpp : Defines the entry point for the DLL application. // #include "stdafx.h" #include <objbase.h> #include <initguid.h> #include "MathCOM.h" //standard self-registration table const char * g_RegTable[][3]={ {"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}",0,"MathCOM"}, {"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\InprocServer32", 0, (const char * )-1 /*表示文件名的值*/}, {"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\ProgID",0,"tulip.MathCOM.1"}, {"tulip.MathCOM.1",0,"MathCOM"}, {"tulip.MathCOM.1\\CLSID",0,"{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}"}, }; HINSTANCE g_hinstDll; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { g_hinstDll=(HINSTANCE)hModule; return TRUE; } /********************************************************************* * Function Declare : DllUnregisterServer * Explain : self-unregistration routine * Parameters : * void -- * Return : * STDAPI -- * Author : tulip * Time : 2003-10-29 19:07:42 *********************************************************************/ STDAPI DllUnregisterServer(void) { HRESULT hr=S_OK; char szFileName [MAX_PATH]; ::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH); int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable); for(int i =0;SUCCEEDED(hr)&&i<nEntries;i++) { const char * pszKeyName=g_RegTable[i][0]; long err=::RegDeleteKey(HKEY_CLASSES_ROOT,pszKeyName); if(err!=ERROR_SUCCESS) hr=S_FALSE; } return hr; } /********************************************************************* * Function Declare : DllRegisterServer * Explain : self Registration routine * Parameters : * void -- * Return : * STDAPI -- * Author : tulip * Time : 2003-10-29 19:43:51 *********************************************************************/ STDAPI DllRegisterServer(void) { HRESULT hr=S_OK; char szFileName [MAX_PATH]; ::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH); int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable); for(int i =0;SUCCEEDED(hr)&&i<nEntries;i++) { const char * pszKeyName=g_RegTable[i][0]; const char * pszValueName=g_RegTable[i][1]; const char * pszValue=g_RegTable[i][2]; if(pszValue==(const char *)-1) { pszValue=szFileName; } HKEY hkey; long err=::RegCreateKey(HKEY_CLASSES_ROOT,pszKeyName,&hkey); if(err==ERROR_SUCCESS) { err=::RegSetValueEx( hkey, pszValueName, 0, REG_SZ, ( const BYTE*)pszValue, ( strlen(pszValue)+1 ) ); ::RegCloseKey(hkey); } if(err!=ERROR_SUCCESS) { ::DllUnregisterServer(); hr=E_FAIL; } } return hr; } STDAPI DllGetClassObject(REFCLSID rclsid ,REFIID riid,void **ppv) { return CLASS_E_CLASSNOTAVAILABLE; } STDAPI DllCanUnloadNow(void) { return E_FAIL; }
我只是在此文件中加幾個必要的頭文件和幾個全局變量。並實現了 DllRegisterServer()和DllUnregisterServer()。而對於其他兩引出函數我只返回一個錯誤值罷了。
1.5 小結
現在我們的工程中應該有如下文件:
文件名 作用 Stdafx.h和stdafx.cpp 預編譯文件 MathCOM.cpp Dll入口函數及其他重要函數定義的地方 MathCOM.def 模塊定義文件 MathCOM.idl 接口定義文件(在1.2後如果編譯的話應該還有四個文件)
好了到現在,我的所謂COM已經實現注冊與注銷功能。
如果在命令行或"運行"菜單下項執行如下"regsvr32 絕對路徑+MathCOM.dll"就注冊此COM組件。在執行完此命令後,請查看注冊表項的HKEY_CLASSES_ROOT\CLSID項看看3BCFE27E-C88D-453C-8C94-F5F7B97E7841這一項是否存在(上帝保佑存在)。
如同上方法再執行一下"regsvr32 -u 絕對路徑+MathCOM.dll",再看看注冊表。
其實剛才生成的dll根本不是COM組件,哈哈!!!因為他沒有實現DllGetClassObject()也沒有實現ISmipleMath和IAdvancedMath兩個接口中任何一個。
讓我們繼續前行吧!!!
2、實現ISmipleMath,IAdvancedMath接口和DllGetClassObject()
2.1 實現ISmipleMath和IAdvancedMath接口
讓我們將原來的 CMath 類修改來實現ISmipleMath接口和IAdvancedMath接口。
修改的地方如下:
1) Math.h文件
/*@**#---2003-10-29 21:33:44 (tulip)---#**@ #include "interface.h"*/ #include "MathCOM.h"//新增加的,以替換上面的東東 class CMath : public ISimpleMath, public IAdvancedMath { private: ULONG m_cRef; private: int calcFactorial(int nOp); int calcFabonacci(int nOp); public: CMath(); //IUnknown Method STDMETHOD(QueryInterface)(REFIID riid, void **ppv); STDMETHOD_(ULONG, AddRef)(); STDMETHOD_(ULONG, Release)(); // ISimpleMath Method STDMETHOD (Add)(int nOp1, int nOp2,int * pret); STDMETHOD (Subtract)(int nOp1, int nOp2,int *pret); STDMETHOD (Multiply)(int nOp1, int nOp2,int *pret); STDMETHOD (Divide)(int nOp1, int nOp2,int * pret); // IAdvancedMath Method STDMETHOD (Factorial)(int nOp,int *pret); STDMETHOD (Fabonacci)(int nOp,int *pret); };
2) Math.cpp文件
/*@**#---2003-10-29 21:32:35 (tulip)---#**@ #include "interface.h" */ #include "math.h" STDMETHODIMP CMath::QueryInterface(REFIID riid, void **ppv) {// 這裡這是實現dynamic_cast的功能,但由於dynamic_cast與編譯器相關。 if(riid == IID_ISimpleMath) *ppv = static_cast<ISimpleMath *>(this); else if(riid == IID_IAdvancedMath) *ppv = static_cast<IAdvancedMath *>(this); else if(riid == IID_IUnknown) *ppv = static_cast<ISimpleMath *>(this); else { *ppv = 0; return E_NOINTERFACE; } reinterpret_cast<IUnknown *>(*ppv)->AddRef(); //這裡要這樣是因為引用計數是針對組件的 return S_OK; } STDMETHODIMP_(ULONG) CMath::AddRef() { return ++m_cRef; } STDMETHODIMP_(ULONG) CMath::Release() { ULONG res = --m_cRef; // 使用臨時變量把修改後的引用計數值緩存起來 if(res == 0) // 因為在對象已經銷毀後再引用這個對象的數據將是非法的 delete this; return res; } STDMETHODIMP CMath::Add(int nOp1, int nOp2,int * pret) { *pret=nOp1+nOp2; return S_OK; } STDMETHODIMP CMath::Subtract(int nOp1, int nOp2,int * pret) { *pret= nOp1 - nOp2; return S_OK; } STDMETHODIMP CMath::Multiply(int nOp1, int nOp2,int * pret) { *pret=nOp1 * nOp2; return S_OK; } STDMETHODIMP CMath::Divide(int nOp1, int nOp2,int * pret) { *pret= nOp1 / nOp2; return S_OK; } int CMath::calcFactorial(int nOp) { if(nOp <= 1) return 1; return nOp * calcFactorial(nOp - 1); } STDMETHODIMP CMath::Factorial(int nOp,int * pret) { *pret=calcFactorial(nOp); return S_OK; } int CMath::calcFabonacci(int nOp) { if(nOp <= 1) return 1; return calcFabonacci(nOp - 1) + calcFabonacci(nOp - 2); } STDMETHODIMP CMath::Fabonacci(int nOp,int * pret) { *pret=calcFabonacci(nOp); return S_OK; } CMath::CMath() { m_cRef=0; }
2.2 COM組件調入大致過程
1) COM庫初始化 使用CoInitialize序列函數(客戶端)
2)激活COM(客戶端)
3) 通過注冊表項將對應的dll調入COM庫中(COM庫)
4) 調用COM組件內的DllGetClassObject()函數(COM組件)
5)通過類廠返回接口指針(COM庫)這一步不是必需的
2.3 DllGetClassObject()實現
在MathCOM.cpp裡加入下列語句,
#include "math.h"
#include "MathCOM_i.c"
並將MathCOM.cpp裡的DllGetClassObject()修改成如下:
/********************************************************************* * Function Declare : DllGetClassObject * Explain : * Parameters : * REFCLSID rclsid -- * REFIID riid -- * void **ppv -- * Return : * STDAPI -- * Author : tulip * Time : 2003-10-29 22:03:53 *********************************************************************/ STDAPI DllGetClassObject(REFCLSID rclsid ,REFIID riid,void **ppv) { static CMath *pm_math=new CMath; if(rclsid==CLSID_MATHCOM) return pm_math->QueryInterface(riid,ppv); return CLASS_E_CLASSNOTAVAILABLE; }
2.4 客戶端
接下來我們寫個客戶端程序對此COM進行測試。
新建一個空的名為 TestMathCOM 的 win32 Console 工程,將它添加到 MathCOM workspace 中。
在 TestMathCOM 工程裡添加一個名為 main.cpp 的文件,此文件的內容如下:
//main.cpp文件 #include <windows.h> #include "../MathCOM.h"//這裡請注意路徑 #include "../MathCOM_i.c"//這裡請注意路徑 #include <iostream> using namespace std; void main(void) { //初始化COM庫 HRESULT hr=::CoInitialize(0); ISimpleMath * pSimpleMath=NULL; IAdvancedMath * pAdvancedMath=NULL; int nReturnValue=0; hr=::CoGetClassObject(CLSID_MATHCOM, CLSCTX_INPROC, NULL,IID_ISimpleMath, (void **)&pSimpleMath); if(SUCCEEDED(hr)) { hr=pSimpleMath->Add(10,4,&nReturnValue); if(SUCCEEDED(hr)) cout << "10 + 4 = " <<nReturnValue<< endl; nReturnValue=0; } // 查詢對象實現的接口IAdvancedMath hr=pSimpleMath->QueryInterface(IID_IAdvancedMath, (void **)&pAdvancedMath); if(SUCCEEDED(hr)) { hr=pAdvancedMath->Fabonacci(10,&nReturnValue); if(SUCCEEDED(hr)) cout << "10 Fabonacci is " << nReturnValue << endl; } pAdvancedMath->Release(); pSimpleMath->Release(); ::CoUninitialize(); ::system("pause"); return ; }
關於如何調試dll請參閱附錄A
2.5 小結
到現在我們應該有 2 個工程和 8 個文件,具體如下:
工程 文件 作用 MathCOM Stdafx.h 和 stdafx.cpp 預編譯文件 MathCOM.cpp Dll入口函數及其他重要函數定義的地方 MathCOM.def 模塊定義文件 MathCOM.idl 接口定義文件(在1.2後如果編譯的話應該還有四個文件) math.h和math.cpp ISmipleMath,IadvancedMath接口的實現類 TestMathCOM Main.cpp MathCOM的客戶端,用於測試MathCOM組件在此部分中我們已經完成一個可以實用的接近於完整的 COM組件。我們完成了此COM組件的客戶端。如果你已經創建COM實例的話,你可能會發現在此部分的客戶端並不是用CoCreateInstance()來創建COM實例,那是因為我們還沒有在此COM組件裡實現IClassFactory接口(此接口在下一部分實現)。
通過這個例子,我希望大家明白以下幾點:
1) DllGetClassObject()的作用,請參看COM組件調入大致過程這一節,同時也請將斷點打在DllGetClassObject()函數上,仔細看看他的實現(在沒有實現IClassFactory接口的情況下)和他的傳入參數。
2) 為什麼在這個客戶端程序裡不使用CoCreateInstance()來創建COM實例而使用CoGetClassObject()來創建COM實例。你可以試著用CoCreateInstance()來創建Cmath,看看DllGetClassObject()的第一參數是什麼?
3) 實現IClassFactory接口不是必需的,但應該說是必要的(如何實現請看下一章)
4) 應掌握DllRegisterServer()和DllUnregisterServer()的實現。
5) 客戶端在調用COM組件時需要那幾個文件(只要由idl文件產生的兩個文件)
3、類廠
附錄
A 我對 dll 的一點認識
目標:寫幾個比較簡單的dll並了解**.dll與**.lib的關系。
一:沒有lib的dll
1.1建一個沒有lib的dll
1) 新建一個com_1.cpp文件(注意此dll根本沒有什麼用)
2) 在com_1.cpp寫下下面的代碼
3) 按下F5運行,所有的東西都按確定。
4) 應該出現如下錯誤:
Linking...
Creating library Debug/COM_1.lib and object Debug/COM_1.exp
LIBCD.lib(crt0.obj) : error LNK2001: unresolved external symbol _main
Debug/COM_1.exe : fatal error LNK1120: 1 unresolved externals
5)進入 project|setting,在 "C/C++" 屬性框的 "project Options" 裡把
"/D ''_console''" 修改成"/D ''_WINDOWS''"。
6)進入project|setting,在 "link" 屬性框的 "project Options" 裡增加下
面的編譯開關 "/dll "
增加的編譯開關大致如下:
kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib
ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /incremental:yes
/pdb:"Debug/COM_1.pdb" /debug /machine:I386 /out:"Debug/COM_1.dll" /implib:"Debug/COM_1.lib"
/pdbtype:sept
注意:"/dll"應該與後面的開關之間有一個空格
//com_1.cpp #include <objbase.h> BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } }
現在可以編譯了,這小片段代碼將會生成一個dll,但這個dll是沒有用的。沒有引出函數和變量。
1.2 調試沒有 lib 的 dll
1) 新建一個工程 Client,工程類型為 console,將上面創建的 dll copy 到 client 工程目錄下
2) 增加 Client.cpp(代碼見下)到工程 Client 中去
3) 選中 Client 工程,並在 project|setting|debug|Category 下拉框,如圖:
圖1.4 調試
注意這是一種調試 dll 的方法
5) 現在可以在Client和COM_1.dll裡打斷點調試了。
在這裡我們只能調試DllMain()函數,因為那個dll裡除了就沒別的東西了,下面我開始 增加一點東西。
二:帶有lib的dll
2.1 創建一個帶有lib的dll
我們在原來的基礎上讓上面的代碼產生一個lib了。新的代碼如下:
#include <objbase.h> extern "C" __declspec(dllexport) void tulip (void) { ::MessageBox(NULL,"ok","I''am fine",MB_OK); } BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } return TRUE; }
在這個dll裡,我們引出一個tulip函數。如果此時我們想要在客戶調用此函數應該用什麼方法呢?
上面的代碼除了生成dll外,他比第一個程序多產生一個lib文件,現在應該知道dll與lib的關系吧。Lib文件是dll輸出符號文件。如果一個dll沒有任何東西輸出那麼不會有對應的lib文件,但只要一個dll輸出一個變量或函數就會相應的lib文件。總的說來,dll與lib是相互配套的。
當某個dll他有輸出函數(或變量)而沒有lib文件時,我們應該怎麼調用 dll 的函數呢?請看下面的方法。
2.2 調試帶有引用但沒有頭文件的 dll
注意:本方法根本沒有用 COM_1.lib 文件,你可以把 COM_1.lib 文件刪除而不影響。
此時的客戶端代碼如果下:
#include <windows.h> int main(void) { //定義一個函數指針 typedef void ( * TULIPFUNC )(void); //定義一個函數指針變量 TULIPFUNC tulipFunc; //加載我們的dll HINSTANCE hinst=::LoadLibrary("COM_1.dll"); //找到dll的tulip函數 tulipFunc=(TULIPFUNC)GetProcAddress(hinst,"tulip"); //調用dll裡的函數 tulipFunc(); return 0; }
對於調用系統函數用上面的方法非常方便,因為對於User32.dll,GUI32.dll這種dll,我沒有對應的lib,所以一般用上面的方法。
三:帶有頭文件的dll
3.1 創建一個帶有引出信息頭文件的dll
如果用上面的方法調用我們自己創建的dll那太煩了!因為我們的dll可能沒有像window這樣標准化的文檔。可能過了一段時間後,我們都會忘記dll內部函數的格式。再如當我們把此dll發布客戶時,那個客戶肯定會在背後罵你的!
這時我們需要一個能了解dll引出信息途徑。我創建一個.h文件。繼續我們旅途。
我們的dll代碼只需要修改一點點,代碼如下:
#include <objbase.h> #include "header.h"//看到沒有,這就是我們增加的頭文件 extern "C" __declspec(dllexport) void tulip (void) { ::MessageBox(NULL,"ok","I''am fine",MB_OK); } BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) { HANDLE g_hModule; switch(dwReason) { case DLL_PROCESS_ATTACH: g_hModule = (HINSTANCE)hModule; break; case DLL_PROCESS_DETACH: g_hModule=NULL; break; } return TRUE; }
而 header.h文件只有一行代碼:
extern "C" __declspec(dllexport) void tulip (void);
3.2 調試帶有頭文件的dll
而此時我們的客戶程序應該變成如下樣子:(比第二要簡單多了)
#include <windows.h> #include "..\header.h"//注意路徑 //注意路徑,加載 COM_1.lib 的另一種方法是 Project | setting | link 設置裡 #pragma comment(lib,"COM_1.lib") int main(void) { tulip();//只要這樣我們就可以調用dll裡的函數了 return 0; }
四:小結
今天講了三種 dll 形式,第一種是沒有什麼實用價值的,但能講清楚 dll 與 lib 的關系。我們遇到的情況大多數是第三種,dll 的提供一般會提供 **.lib 和 **.h 文件,而第二種方法適用於系統函數。
希望各位高手指正與交流,
注:今天一時興起,寫了上面的東西,本來我是總結一下有關 COM 的東西,但寫著寫著就成這個樣子,COM 也是從 dll 起步的。
(全文完)