您的位置:首页 > 其它

COM技术初探(二)

2008-01-09 14:49 369 查看
一、COM是一个更好的 C++
1. COM 是什么
2. 从 C++ 到 DLL 再到 COM
2.1 C++
2.2 DLL
2.3 COM

二、COM基础
1. COM基本知识
1.1 返回值HRESULT
1.2 初识idl
1.3 IUnkown接口
2. 一个比较简单的COM
2.1 interface.h文件
2.2 math.h文件
2.3 math.cpp文件
2.4 simple.cpp文件
2.5 Math组件的二进制结构图
2.6 小结

三、纯手工创建一个COM组件
1. 从建工程到实现注册
1.1 创建一个类型为win32 dll工程
1.2 定义接口文件
1.3 增加注册功能
1.3.1 增加一个MathCOM.def文件
1.3.2 DllRegisterServer()和DllUnregisterServer()
1.4 MathCOM.cpp文件
1.5 小结
2. 实现ISmipleMath,IAdvancedMath接口和DllGetClassObject()
2.1 实现ISmipleMath和IAdvancedMath接口
2.2 COM组件调入大致过程
2.3 DllGetClassObject()实现
2.4 客户端
2.5 小结
3. 类厂

附录

A 我对dll的一点认识
一. 没有lib的dll
1.1 建一个没有lib的dll
1.2 调试没有lib的dll
二. 带有lib的dll
2.1 创建一个带有lib的dll
2.2 调试带有引用但没有头文件的dll
三. 带有头文件的dll
3.1 创建一个带有引出信息头文件的dll
3.2 调试带有头文件的dll
四. 小结

三、纯手工创建一个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.cppDll入口函数及其他重要函数定义的地方
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 个文件,具体如下:
 
工程文件作用
MathCOMStdafx.h 和 stdafx.cpp预编译文件
 MathCOM.cppDll入口函数及其他重要函数定义的地方
 MathCOM.def模块定义文件
 MathCOM.idl接口定义文件(在1.2后如果编译的话应该还有四个文件)
 math.h和math.cppISmipleMath,IadvancedMath接口的实现类
TestMathCOMMain.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;

//加载我们膁ll
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 起步的。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: