本文主要记录一下COM编程的简单实现。关于COM的简单介绍可以参考文章 COM的简单介绍。
目录
1、代码的主要结构
代码结构主要分为两部分,其中 ComTest 为组件程序部分,ComCtrl 为客户部分(即组件的使用者),具体如下所示:
2、ComTest
ComTest 为 VS2012所创建的 Win32项目->DLL 类型的工程。代码的组成结构如下所示:
- IUnknown 是所有接口的基类;
- IComTest 为组件的接口类,在这个接口类中定义能提供的服务(即函数),所以用户只需要知道这个类就可以了,;
- CComTest 是对象类,继承自接口IComTest,用于实现服务;
- IClassFactory 为类厂类的接口类,不需要开发人员实现;
- CComTestFactory 为类厂类,用于生产CComTest对象;
- 还有一个自注册模块,用于将组件程序注册进注册表中;
具体实现可以参考以下进行开发。
2.1、接口类的定义
IComTest.h
#pragma once
#include <Unknwn.h>
// 接口 IComTest 的GUID标识,可以通过VS自带的工具自动生成
// {24EC0D3E-2C69-4C39-8AAB-59AFF1111982}
static const GUID IID_IComTest =
{ 0x24ec0d3e, 0x2c69, 0x4c39, { 0x8a, 0xab, 0x59, 0xaf, 0xf1, 0x11, 0x19, 0x82 } };
class IComTest : public IUnknown{
public:
virtual BOOL __stdcall SayHello() = 0; //声明一个服务函数,用于测试
};
在这个文件中,主要是声明了接口类的GUID, 即IID_IComTest;同时声明了组件程序的接口函数。接口类没有实现,所以没有对应的 IComTest.cpp 文件。另外这个 IComTest.h 头文件必须包含在用户程序中。
2.2、对象类的定义
CComTest.h
#pragma once
#include <stdio.h>
#include "icomtest.h"
// CComTest 对象的GUID标识,名称为 CLSID
// {CEF56010-936E-4F71-BC51-32F7ED3B3F73}
static const GUID CLSID_CComTest =
{ 0xcef56010, 0x936e, 0x4f71, { 0xbc, 0x51, 0x32, 0xf7, 0xed, 0x3b, 0x3f, 0x73 } };
class CComTest : public IComTest
{
public:
CComTest(void);
~CComTest(void);
public:
//IUnknown member functions
virtual HRESULT __stdcall QueryInterface(const IID& iid, void **ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();
//IComTest member functions
virtual BOOL __stdcall SayHello();
private:
ULONG m_Ref; //引用计数
};
在这个文件中,主要是声明了对象类的GUID(也成为CLSID), 即 CLSID_CComTest;同时声明了对象类的成员函数,这些成员函数是必须要实现的,所以有对应的 CComTest.cpp文件。
CComTest.cpp
#include "stdafx.h"
#include "CComTest.h"
extern ULONG g_ObjNumber; //在ComTest.cpp中定义
CComTest::CComTest(void){
this->m_Ref = 0; //引用计数初始化为0
g_ObjNumber ++; //对象计数 +1
}
CComTest::~CComTest(void){ }
//IUnknown member functions
HRESULT CComTest::QueryInterface(const IID& iid, void **ppv){
if( iid == IID_IUnknown ){
*ppv = (IUnknown *) this ;
((IUnknown *)(*ppv))->AddRef(); //返回的是 IUnknown 接口
}else if ( iid == IID_IComTest ){ //IID_IComTest 已经在 IComTest.h 中定义
*ppv = (IComTest *) this ;
((IComTest *)(*ppv))->AddRef();
}
else{
*ppv = NULL;
return E_NOINTERFACE ;
}
return S_OK;
}
ULONG CComTest::AddRef(){
this->m_Ref ++; //引用计数 +1
return this->m_Ref;
}
ULONG CComTest::Release(){
this->m_Ref --; //引用计数 -1
if(this->m_Ref == 0){ //当引用计数为0时,则删除自身对象
g_ObjNumber --; //同时对象个数 -1
delete this;
return 0;
}
return this->m_Ref;
}
//IComTest member functions
BOOL CComTest::SayHello(){
printf("Hello COM!\n");
return TRUE;
}
2.3、类厂的定义
CComTestFactory.h
#pragma once
#include <Unknwn.h> //包含头文件
class CComTestFactory : public IClassFactory
{
public:
CComTestFactory(void);
~CComTestFactory(void);
public:
//IUnknown member functions
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();
//IClassFactory member functions
virtual HRESULT __stdcall CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void** ppv);
virtual HRESULT __stdcall LockServer(BOOL bLock);
private:
ULONG m_Ref; //引用计数
};
CComTestFactory.cpp
#include "stdafx.h"
#include "CComTest.h"
#include "CComTestFactory.h"
extern ULONG g_LockNumber; //类厂对象 的锁计数器,在ComTest.cpp中定义,下同
extern ULONG g_ObjNumber; //组件中 CComTest 对象的个数,用于判断是否可以卸载本组件,如果值为0则可以卸载
CComTestFactory::CComTestFactory(void){
this->m_Ref = 0; //引用计数初始化为0
}
CComTestFactory::~CComTestFactory(void){}
//IUnknown member functions
HRESULT CComTestFactory::QueryInterface(const IID& iid, void** ppv){
if( iid == IID_IUnknown ){
*ppv = (IUnknown *) this ;
((IUnknown *)(*ppv))->AddRef();
}else if ( iid == IID_IClassFactory ){ //IID_IClassFactory 已经在 Unknwn.h 中定义
*ppv = (IClassFactory *) this ;
((IClassFactory *)(*ppv))->AddRef();
}
else{
*ppv = NULL;
return E_NOINTERFACE ;
}
return S_OK;
}
ULONG CComTestFactory::AddRef(){
this->m_Ref ++; //引用计数 +1
return this->m_Ref;
}
ULONG CComTestFactory::Release(){
this->m_Ref --; //引用计数 -1
//当引用计数为0时,则删除自身对象
if(this->m_Ref == 0){
delete this;
return 0;
}
return this->m_Ref;
}
//IClassFactory member functions
//创建 CComTest 对象,并返回 接口指针
HRESULT CComTestFactory::CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void** ppv){
if (NULL != pUnknownOuter)
return CLASS_E_NOAGGREGATION;
HRESULT hr = E_OUTOFMEMORY;
//Create the object passing function to notify on destruction.
CComTest* pObj = new CComTest();
if (NULL == pObj)
return hr;
//Obtain the first interface pointer (which does an AddRef)
*ppv = NULL;
hr=pObj->QueryInterface(iid, ppv);
if (hr != S_OK) {
//Kill the object if initial creation or FInit failed.
g_ObjNumber --; // Reference count g_ObjNumber be added in CComTest constructor
delete pObj;
}
return hr;
}
HRESULT CComTestFactory::LockServer(BOOL bLock){
if(bLock){
g_LockNumber++;
}else{
g_LockNumber--;
}
return NOERROR;
}
其中最主要的函数是 CreateInstance ,该函数用于创建对应对象类的接口指针。因为每个类厂能创建的对象类是已知的,所以返回的接口指针就是对应对象类的接口指针。CreateInstance函数 是当用户调用 CoCreateInstance函数创建类厂接口指针时,会自行调用,这个与 《COM的简单介绍》中介绍的不是很一致,请注意区分。
2.4、自注册模块
组件的自注册就是要在组件的引出函数中定义 DllRegisterServer 和 DllUnregisterServer 两个函数。其中 DllRegisterServer 函数是往注册表写入注册信息,DllUnregisterServer 函数是从注册表中删除注册信息。
DllRegisterServer
extern "C" HRESULT __stdcall DllRegisterServer()
{
char szModule[1024]; //For char
wchar_t w_szModule[1024]; //For wchar_t
DWORD dwResult = ::GetModuleFileName((HMODULE)g_hModule, w_szModule, 1024);
if (dwResult == 0)
return SELFREG_E_CLASS;
Wchar2Char(szModule, w_szModule); //在这里进行转换,防止对 Registry.cpp进行过多的修改
return RegisterServer(CLSID_CComTest, //对象类 CComTest 的GUID
szModule, //组件程序的完整路径
"ComTest.Object", //ProgID
"ComTest Component", //Description
NULL);
}
DllUnregisterServer
extern "C" HRESULT __stdcall DllUnregisterServer()
{
return UnregisterServer(CLSID_CComTest, "ComTest.Object", NULL);
}
其中,函数 RegisterServer 和 UnregisterServer 全都定义在 Registry.cpp文件中,在该文件中实现了一个比较通用的注册成员。由于该文件中代码比较多,可前往文末最后的下载链接中自行下载。
2.5、ComTest.cpp
在该文件中定义了组件程序必须提供的四个引出函数 DllGetClassObject、DllCanUnloadNow、DllRegisterServer 、DllUnregisterServer ,详细代码如下所示:
// ComTest.cpp : 定义 DLL 应用程序的导出函数。
//
#include "stdafx.h"
#include "olectl.h"
#include "IComTest.h"
#include "CComTest.h"
#include "CComTestFactory.h"
#include "Registry.h"
ULONG g_LockNumber = 0; //类厂对象 的锁计数器
ULONG g_ObjNumber = 0; //组件中 CComTest 对象的个数,用于判断是否可以卸载本组件,如果值为0则可以卸载
HANDLE g_hModule; //DLL句柄
extern "C" HRESULT __stdcall DllGetClassObject(const CLSID& clsid, const IID& iid, void **ppv)
{
if(clsid == CLSID_CComTest ){
CComTestFactory *pFactory = new CComTestFactory;
if (pFactory == NULL) {
return E_OUTOFMEMORY ;
}
HRESULT result = pFactory->QueryInterface(iid, ppv);
return result;
}else{
return CLASS_E_CLASSNOTAVAILABLE;
}
}
extern "C" HRESULT __stdcall DllCanUnloadNow(void)
{
//双重检查,只有当对象计数和锁计数都为0时,才可以卸载组件程序
if ((g_ObjNumber == 0) && (g_LockNumber == 0))
return S_OK;
else
return S_FALSE;
}
// Server registration
extern "C" HRESULT __stdcall DllRegisterServer()
{
char szModule[1024]; //For char
wchar_t w_szModule[1024]; //For wchar_t
DWORD dwResult = ::GetModuleFileName((HMODULE)g_hModule, w_szModule, 1024);
if (dwResult == 0)
return SELFREG_E_CLASS;
Wchar2Char(szModule, w_szModule); //在这里进行转换,防止对 Registry.cpp进行过多的修改
return RegisterServer(CLSID_CComTest, //对象类 CComTest 的GUID
szModule, //组件程序的完整路径
"ComTest.Object", //ProgID
"ComTest Component", //Description
NULL);
}
// Server unregistration
extern "C" HRESULT __stdcall DllUnregisterServer()
{
return UnregisterServer(CLSID_CComTest, "ComTest.Object", NULL);
}
2.6、dllmain.cpp
dllmain.cpp定义了组件程序的入口函数,如下所示:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
extern HANDLE g_hModule; //DLL句柄,在ComTest.cpp中定义
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
g_hModule = hModule; //最重要的就是这条语句
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
2.7、ComTest.def
这个.def文件声明了组件需要引出的几个函数,如下所示:
; ComTest.def : Declares the module parameters for the DLL.
LIBRARY "ComTest"
EXPORTS
; Explicit exports can go here
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllCanUnloadNow PRIVATE
3、ComCtrl
ComCtrl 为 VS2012所创建的 Win32控制台应用程序 类型的工程,主要用于模拟对组件的调用。在ComCtrl工程中,一定要包含ComTest工程中的 IComTest.h头文件,因为在该文件中声明了组件程序的接口。需要注意的是,在使用组件对象之前一定要使用 RegSvr32 命令进行组件的注册:,当注册成功之后会有以下提示,否则会报错:
此时就会在注册表中看到的注册信息
用户详细代码如下所示:
#include "windows.h"
#include <stdio.h>
#include <comutil.h>
int TestCom(){
//初始化COM库,使用默认的内存分配器
if (CoInitialize(NULL) != S_OK) {
printf("Initialize COM library failed!\n");
return -1;
}
HRESULT hResult;
GUID comTestCLSID;
//获取 ProgID为 ComTest.Object 组件的CLISD,其中 ComTest.Object 是在注册注册表时确定的ID
hResult = ::CLSIDFromProgID(L"ComTest.Object", &comTestCLSID);
if (hResult != S_OK) {
printf(">>> Can't find the ComTest CLSID!\n");
return -2;
}else{
LPOLESTR szCLSID;
StringFromCLSID(comTestCLSID, &szCLSID); //将其转化为字符串形式用来输出
wprintf(L">>> Find ComTest CLSID: \"%s\"\n",szCLSID);
CoTaskMemFree(szCLSID); //调用COM库的内存释放
}
IUnknown *pUnknown;
//用此 CLSID 创建一个COM对象,并获取 IUnknown 接口,指向的是类厂对象
hResult = CoCreateInstance( comTestCLSID,
NULL,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(void **)&pUnknown);
if (hResult != S_OK || NULL == pUnknown){
printf("Create object failed!\n");
return -2;
}
IComTest* pComTest;
//通过此 IUnknown 接口查询 IComTest 接口
hResult = pUnknown->QueryInterface(IID_IComTest, (void **)&pComTest);
if (hResult != S_OK) {
pUnknown->Release();
printf("QueryInterface IComTest failed!\n");
return -3;
}
BOOL re = FALSE;
printf(">>> 调用组件接口函数:\n");
re = pComTest->SayHello(); //调用 IComTest 接口中的函数
if(re){
printf(">>> 成功调用 COM 组件!\n");
}else{
printf(">>> ERROR!\n");
}
pComTest->Release(); //释放 IComTest 接口
if (pUnknown->Release()== 0) //释放 IUnknown 接口
printf("The reference count of ComTest object is zero.\n");
CoUninitialize(); //COM库反初始化
return 0;
}
int main(int argc, char* argv[])
{
int re = TestCom();
system("pause");
return 0;
}
以上代码中,当用户调用 CoCreateInstance函数创建类厂接口指针时,会自行调用类厂的CreateInstance函数创建对象,所以不用再进行显示的调用了,这个与 前文《COM的简单介绍》中的最后介绍的协作流程的不是很一致,请注意区分。
4、代码实现
工程采用VS2012编写,全部代码的下载请访问:ComTest.zip下载