简单 yet 可调试的 COM 骨架代码






4.91/5 (50投票s)
2002 年 11 月 10 日
15分钟阅读

256364

2493
教程,演示如何从头开始构建 COM 组件(DLL、EXE、自动化)
引言
这篇文章和主题可能会引起代码专家的不适。但是,即使在 7 年后的今天,在当今以 Windows 为中心的世界中大量使用 COM 的情况下,我仍然经常看到人们对 COM 提出最简单的问题。
微软几年前推出了 ATL 库,希望现成的宏能够简化 COM 开发过程。但在实践中,宏往往会混淆代码的实际作用和预期作用。此外,本文的一个明显基础是 ATL 宏会生成不可调试的代码。宏由 C/C++ 预处理器展开,这意味着几乎不可能找出任何基于 ATL 的代码可能出现的问题。
这就是为什么我将在本文中展示如何从头开始编写工作的 COM 组件,而且不使用一个宏。希望您觉得有用。
本文的其余部分提供了三个(有望可重用)示例 COM 实现
- 一个简单的 COM DLL
- 一个支持自动化的 COM DLL
- 一个支持自动化的 COM EXE
再加上几个工作的测试环境(C/C++ 和 VB)。
一个简单的 COM DLL
您可以在上面找到一个工作的 simplecomserver.zip 包,这是以下步骤的结果代码。
启动 VC6/7,创建一个名为 simplecomserver
的新项目。选择 WIN32 动态库项目向导,并选择简单的 DLL 项目选项。
到目前为止,您应该有一个包含 stdafx.h/cpp 预编译头文件的项目,以及带有以下代码的 simplecomserver.cpp
// simplecomserver.cpp : Defines the entry point for the DLL application.
//
#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
现在我们要声明我们的 COM 接口。让我们创建一个新的 .idl 文件(simplecomserver.idl),然后将以下内容粘贴进去
import "wtypes.idl";
[
uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
version(1.0)
]
library LibCOMServer
{
importlib("stdole32.tlb");
importlib("stdole.tlb");
[ uuid(7F24AABF-C822-4c18-9432-21433208F4DC),
oleautomation
]
interface ICOMServer : IUnknown
{
HRESULT Name([out] BSTR* objectname);
}
[ uuid(6AE24C34-1466-482e-9407-90B98798A712),
helpstring("COMServer object")
]
coclass CoCOMServer
{
[default] interface ICOMServer;
}
}
我们有一个对象内的接口(coclass
关键字),它本身属于一组对象(这里只有一个)的集合,这些对象属于一个类型库(library
关键字)。接口、对象和类型库都有唯一的标识符:它是由一个工具生成的,例如 guidgen.exe(可从 MSDEV / Tools 菜单获得)。
将 simplecomserver.idl 添加到您的项目文件中,右键单击它并编译它。这会在 Debug 文件夹中生成类型库。 simplecomserver.tlb 是一个二进制编译的 IDL 文件,仅此而已。类型库是应用程序和语言将读取和解析的内容,以便在运行时提取接口名称、方法名称、参数限定符等。这就是为什么类型库如此重要的原因。
由于我们为这个 COM 服务器提供了 C/C++ 实现,因此我们还将要求类型库编译器生成一个接口头文件以供派生。右键单击 simplecomserver.idl,选择设置,然后在 MIDL 选项卡中,将“输出头文件名”字段设置为 simplecomserver_i.h。再次编译 IDL 文件。
这个自动生成的头文件有点复杂,因为它包含了 COM 库无法隐藏的许多实现细节。但我们并不真正关心它。我们只需要记住,头文件是一个类声明,我们将实现它。到目前为止这很简单,所以让我们创建一个新的头文件 simplecomserverImpl.h,并提供以下声明
#pragma once
// ICOMServer interface declaration ///////////////////////
//
//
class CoCOMServer : public ICOMServer
{
// Construction
public:
CoCOMServer();
~CoCOMServer();
// IUnknown implementation
//
virtual HRESULT __stdcall QueryInterface
(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
// ICOMServer implementation
//
virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);
private:
// Reference count
long m_cRef ;
};
如果您将其与我们几分钟前在 .idl 文件中声明的内容进行比较,您会发现它看起来非常相似,并且完全用 C/C++ 语法表达。我们还可以注意到来自 IUnknown
接口的三个方法。这些方法通过提供路由功能(QueryInterface
)和安全的引用计数(AddRef
、Release
)来帮助管理 COM 服务器的生命周期。但没有任何神秘之处或隐藏的技巧,我们将立即实现这些方法。让我们创建一个实现文件 simplecomserverImpl.cpp,并将以下代码粘贴进去
#include "stdafx.h"
#include <objbase.h> //
#include "simplecomserver_i.h"
#include <atlbase.h> // CComBSTR
#include "simplecomserverImpl.h"
static long g_cComponents = 0 ; // Count of active components
static long g_cServerLocks = 0 ; // Count of locks
//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{
InterlockedIncrement(&g_cComponents) ;
}
//
// Destructor
//
CoCOMServer::~CoCOMServer()
{
InterlockedDecrement(&g_cComponents) ;
}
//
// IUnknown implementation
//
HRESULT __stdcall CoCOMServer::QueryInterface
(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown || iid == IID_ICOMServer)
{
*ppv = static_cast<ICOMServer*>(this) ;
}
else
{
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
return S_OK ;
}
ULONG __stdcall CoCOMServer::AddRef()
{
return InterlockedIncrement(&m_cRef) ;
}
ULONG __stdcall CoCOMServer::Release()
{
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
这个代码骨架足以实现您在将来遇到的所有接口。当然,现在我们将添加我们自己的自定义实现
//
// ICOMServer implementation
//
HRESULT __stdcall CoCOMServer::Name(/*out*/BSTR* objectname)
{
if (objectname==NULL)
return ERROR_INVALID_PARAMETER;
CComBSTR dummy;
dummy.Append("hello world!");
// Detach() returns an allocated BSTR string
*objectname = dummy.Detach();
return S_OK;
}
到目前为止,我们的 COM 服务器已经实现。当然,任何听说过 COM 的人都会知道 COM 服务器在使用前需要注册。确实,客户端应用程序,无论是什么应用程序,都将间接使用 COM 库,而 COM 库又会在注册表中查找已知 COM 服务器的字典(HKEY_CLASSES_ROOT\CLSID
键)。
因此,我们要做的就是确保我们的构建过程也能自动注册我们的 COM 服务器。这将需要几个步骤,您最好系好安全带!当然,好处是我们永远不需要手动创建许多注册表项,我的意思是,通过原始的、愚蠢的注册表相关代码。这将需要一些工作,因为我们需要注册 COM 对象本身、类型库和接口。
让我们开始添加一些导出函数。这些导出函数可以从外部访问,并允许注册工具(听说过 regsvr32.exe 吗?)请求注册。这就是 DllRegisterServer
函数。让我们添加一种新类型的文件:simplecomserver.def 并将以下内容粘贴进去
LIBRARY "simplecomserver"
DESCRIPTION 'Proxy/Stub DLL'
EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
DllRegisterServer @3 PRIVATE
DllUnregisterServer @4 PRIVATE
.def 文件只是告诉链接器允许列表中的函数从外部访问。例如,您可以使用 dumpbin /exports
命令行(MSDEV 工具,需要在路径中包含 MSDEVDIR),甚至可以使用 Dependency Walker(另一个 MSDEV 工具)在将此文件添加到项目文件并构建项目后立即查看它们。
让我们为四个导出函数提供实现
///////////////////////////////////////////////////////////
//
// Exported functions
//
//
// Can DLL unload now?
//
STDAPI DllCanUnloadNow()
{
if ((g_cComponents == 0) && (g_cServerLocks == 0))
{
return S_OK ;
}
else
{
return S_FALSE ;
}
}
//
// Get class factory
//
STDAPI DllGetClassObject(const CLSID& clsid,
const IID& iid,
void** ppv)
{
// Can we create this component?
if (clsid != CLSID_CoCOMServer)
{
return CLASS_E_CLASSNOTAVAILABLE ;
}
// Create class factory.
CFactory* pFactory = new CFactory ; // Reference count set to 1
// in constructor
if (pFactory == NULL)
{
return E_OUTOFMEMORY ;
}
// Get requested interface.
HRESULT hr = pFactory->QueryInterface(iid, ppv) ;
pFactory->Release() ;
return hr ;
}
//
// Server registration
//
STDAPI DllRegisterServer()
{
HRESULT hr= RegisterServer(g_hModule,
CLSID_CoCOMServer,
g_szFriendlyName,
g_szVerIndProgID,
g_szProgID,
LIBID_LibCOMServer) ;
if (SUCCEEDED(hr))
{
RegisterTypeLib( g_hModule, NULL);
}
return hr;
}
//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
g_szVerIndProgID,
g_szProgID,
LIBID_LibCOMServer) ;
if (SUCCEEDED(hr))
{
UnRegisterTypeLib( g_hModule, NULL);
}
return hr;
}
///////////////////////////////////////////////////////////
//
// DLL module information
//
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason,
void* lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
g_hModule = (HMODULE)hModule ;
}
return TRUE ;
}
这里有几点说明:首先,这四个函数都带有 STDAPI
前缀,这只是告诉链接器导出它们而不进行任何 C++ 符号修饰(否则函数名周围会出现@
和数字,目前来说这是不必要的)。我们还提供了一个新的 DllMain
实现,替换了类向导几分钟前提供的默认代码。现在您需要删除类向导在 simplecomserver.cpp 中为我们生成的代码(否则链接器会因重复的 DllMain()
实现而报错)。
DllRegisterServer()
和 DllUnregisteServer()
是注册的入口点。此时,我们不深入细节,只记住类型库知道 COM 服务器的内部工作原理,并将执行大部分实际注册工作。
然后是 DllGetClassObject()
。这是外部创建我们的 COM 服务器实例所使用的入口点。出于奇怪的原因,COM 的发明者希望有一些中间对象可以进行交互。所以,有了类工厂。这个对象本身的行为很像一个 COM 对象,尽管它没有关联的 IDL 文件。DllGetClassObject()
由 COM 管道代表外部应用程序调用,并期望返回一个有效的 IClassFactory
接口指针。这个接口(又是接口,一个简单的类)实现了 CreateInstance()
构造方法,而后者实际上会实例化我们的 COM 服务器。正如您所见,到目前为止,类工厂没什么可担心的。我们将这段代码复制/粘贴到 simplecomserverImpl.cpp 中
///////////////////////////////////////////////////////////
//
// Class factory
//
class CFactory : public IClassFactory
{
public:
// IUnknown
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
// Interface IClassFactory
virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv) ;
virtual HRESULT __stdcall LockServer(BOOL bLock) ;
// Constructor
CFactory() : m_cRef(1) {}
// Destructor
~CFactory() {;}
private:
long m_cRef ;
} ;
//
// Class factory IUnknown implementation
//
HRESULT __stdcall CFactory::QueryInterface(const IID& iid, void** ppv)
{
if ((iid == IID_IUnknown) || (iid == IID_IClassFactory))
{
*ppv = static_cast<IClassFactory*>(this) ;
}
else
{
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
return S_OK ;
}
ULONG __stdcall CFactory::AddRef()
{
return InterlockedIncrement(&m_cRef) ;
}
ULONG __stdcall CFactory::Release()
{
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
//
// IClassFactory implementation
//
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter,
const IID& iid,
void** ppv)
{
// Cannot aggregate.
if (pUnknownOuter != NULL)
{
return CLASS_E_NOAGGREGATION ;
}
// Create component.
CoCOMServer* pA = new CoCOMServer ;
if (pA == NULL)
{
return E_OUTOFMEMORY ;
}
// Get the requested interface.
HRESULT hr = pA->QueryInterface(iid, ppv) ;
// Release the IUnknown pointer.
// (If QueryInterface failed, component will delete itself.)
pA->Release() ;
return hr ;
}
// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock)
{
if (bLock)
{
InterlockedIncrement(&g_cServerLocks) ;
}
else
{
InterlockedDecrement(&g_cServerLocks) ;
}
return S_OK ;
}
猜猜怎么着,我们完成了!
注册可以通过显式的regsvr32 <path>\Debug\simplecomserver.dll
shell 命令执行,或者通过在项目中添加一个自定义构建步骤来执行。
一旦注册成功,您就可以在注册表中查找新添加的键
+ HKEY_CLASSES_ROOT
+ COMServer.object
+ CLSID = {6AE24C34-1466-482e-9407-90B98798A712}
+ CLSID
+ {6AE24C34-1466-482e-9407-90B98798A712} = "COMServer object"
+ InProcServer32 = <path>\simplecomserver.dll
+ Interface
+ {7F24AABF-C822-4c18-9432-21433208F4DC} = "ICOMServer"
+ TypeLib = {6F818C55-E6AD-488B-9EB6-511C0CCC0612}
+ TypeLib
+ {6F818C55-E6AD-488b-9EB6-511C0CCC0612}
+ 1.0
+ 0
+ win32 = <path>\simplecomserver.tlb
我希望所有这些注册表的混乱现在对您来说都更清楚了。基本上,您可以看到注册 COM 组件只是在正确的位置添加接口、对象和类型库的 ID 的问题。再次强调,这里没有隐藏的技巧。
出于任何原因,在执行注册时可能会出现问题,您可能不会收到成功的“DllRegisterServer
in simplecomserver.dll succeeded.”消息。但不用担心,以下是调试注册的步骤
- 在
DllMain()
实现中,添加对DllRegisterServer()
的调用。这样做,每次加载 DLL 时,DllRegisterServer()
都会被调用。请记住,这是一个临时更改,如果您在注册似乎工作正常时没有删除此调用,它将会在每次加载 DLL 时进行自我注册。 - 在
DllRegisterServer()
中设置一个断点。 - 开始调试,并浏览任何启动进程。实际上,我们正在告诉调试器启动一个进程并自动加载我们的 DLL。
- 调试注册代码。
如果您想注销您的组件,只需在 regsvr32
shell 命令中添加 -u
选项。结果,将调用 DllUnregisterServer()
。
任何有兴趣使用 C/C++ 客户端应用程序测试 COM 服务器的人,这是代码(请参阅 zip 包中的 TestSimplecomserver.dsp 项目)。
#include <atlbase.h>
#include "..\simplecomserver\simplecomserver_i.h" // interface declaration
#include "..\simplecomserver\simplecomserver_i.c" // IID, CLSID
::CoInitialize(NULL);
ICOMServer *p = NULL;
// create an instance
HRESULT hr = CoCreateInstance( CLSID_CoCOMServer, NULL, CLSCTX_ALL,
IID_ICOMServer,
(void **)&p);
if (SUCCEEDED(hr))
{
// call the (only) method exposed by the main interface
//
BSTR message;
p->Name(&message);
// if everything went well, message holds the returned name
//
// ...
CString szMessage;
AfxBSTR2CString(message, szMessage);
AfxMessageBox(szMessage);
// don't forget to release the stuff
::SysFreeString(message);
}
p->Release();
::CoUninitialize();
我们的 COM 服务器的局限性?
- 具有内置自动化语言(VB、Perl、Python 等)的应用程序尚无法与其配合使用。实际上,我们必须将接口从
IUnknown
派生到IDispatch
,并为四个方法提供默认实现。我们将在下一节中看到这一点。 - 这是一个 DLL。要使 COM 服务器运行在一个单独的.EXE进程之外,我们需要一些额外的代码。我们将在下一节中看到这一点。
一个自动化 COM DLL
我们不会从头开始构建支持自动化的 COM DLL,因为它只是对简单 COM 服务器 DLL 的一个小改动。但是,您可以尝试自己做。
结果代码包含在 automcomserver.zip 包中。
我们要做的就是将 IUnknown
接口支持替换为 IDispatch
支持(它本身继承自 IUnknown
,这就是为什么三个方法 QueryInterface()
、AddRef()
和 Release()
不会消失的原因)。但为什么呢?基本上,IDispatch
接口提供了一种便捷的编程方法来解析类型库,并列出匿名 IDispatch
派生接口公开的方法名称。这旨在提供所谓的后期绑定。换句话说,借助后期绑定,客户端应用程序不再需要静态链接到它使用的接口。在简单的 COM 服务器 DLL 中,当我们包含 simplecomserver_i.h 接口声明时,我们实际上是在为编译器提供一个static
函数vtable
。这并不总是好的。后期绑定允许通过给出名称来检索接口函数的位置。从长远来看,这为软件开发人员提供了一个灵活的绑定系统。这项服务由 IDispatch
提供,它是一种指向类型库的指针。
但这还不够,参数呢?自动化语言应该能够在设计时甚至只在运行时即时猜测方法参数。在设计时,问题很容易解决。实际上,当您注册 COM 服务器时,您也注册了类型库,因此任何支持自动化的语言都可以代表您读取它,提取所有对象、接口、方法和参数,然后通过 intellisense(例如)公开它们。如果您使用的是 VB,通常会通过工具 \ 引用菜单添加类型库。然后,对象浏览器会列出所有提到的内容。因此,我们能够调用在设计时即时发现的方法,但运行时呢?在运行时,自动化引擎需要执行类型绑定,这就是 IDispatch
暴露的其他方法的作用。它们提供指向类型库的入口点以及操作系统公开的底层 API,允许提取参数的所有微小细节:它们是[in]
吗?它们是BSTR
吗?……
虽然所有这些听起来有点吓人,但我们不会纠缠于细节。事实上,大部分 IDispatch
实现将是 COM 库之一提供的默认实现。开始了
所以,让我们重新查看 .idl 接口,并对其进行以下更改
- 将
IUnknown
替换为IDispatch
,以反映ICOMServer
接口现在支持自动化 - 在
Name
方法签名左侧添加[id(1)]
前缀。此id
将方法映射到 vtable 中的索引,并用于发现方法。
IDL 接口现在应该如下所示
import "wtypes.idl";
[
uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
version(1.0)
]
library LibCOMServer
{
importlib("stdole32.tlb");
importlib("stdole.tlb");
[ uuid(7F24AABF-C822-4c18-9432-21433208F4DC),
dual,
oleautomation
]
interface ICOMServer : IDispatch
{
[id(1)] HRESULT Name([out, retval] BSTR* objectname);
}
[ uuid(6AE24C34-1466-482e-9407-90B98798A712),
helpstring("COMServer object")
]
coclass CoCOMServer
{
[default] interface ICOMServer;
}
}
如果您现在尝试构建项目,您会遇到错误,因为尽管我们从 IDispatch
派生,但我们尚未提供其实现。就这样吧
#pragma once
// ICOMServer interface declaration ///////////////////////
//
//
class CoCOMServer : public ICOMServer
{
// Construction
public:
CoCOMServer();
~CoCOMServer();
// IUnknown implementation
//
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
//IDispatch implementation
virtual HRESULT __stdcall GetTypeInfoCount(UINT* pctinfo);
virtual HRESULT __stdcall GetTypeInfo(UINT itinfo,
LCID lcid, ITypeInfo** pptinfo);
virtual HRESULT __stdcall GetIDsOfNames(REFIID riid,
LPOLESTR* rgszNames, UINT cNames,
LCID lcid, DISPID* rgdispid);
virtual HRESULT __stdcall Invoke(DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags,
DISPPARAMS* pdispparams, VARIANT* pvarResult,
EXCEPINFO* pexcepinfo, UINT* puArgErr);
// ICOMServer implementation
//
virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);
private:
HRESULT LoadTypeInfo(ITypeInfo ** pptinfo,
const CLSID& libid, const CLSID& iid, LCID lcid);
// Reference count
long m_cRef ;
LPTYPEINFO m_ptinfo; // pointer to type-library
};
现在是实现部分。首先,让我们加载类型库(实际上我们加载 .tlb 文件)
//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{
InterlockedIncrement(&g_cComponents) ;
m_ptinfo = NULL;
LoadTypeInfo(&m_ptinfo, LIBID_LibCOMServer, IID_ICOMServer, 0);
}
HRESULT CoCOMServer::LoadTypeInfo(ITypeInfo ** pptinfo,
const CLSID &libid,
const CLSID &iid,
LCID lcid)
{
HRESULT hr;
LPTYPELIB ptlib = NULL;
LPTYPEINFO ptinfo = NULL;
*pptinfo = NULL;
// Load type library.
hr = ::LoadRegTypeLib(libid, 1, 0, lcid, &ptlib);
if (FAILED(hr))
return hr;
// Get type information for interface of the object.
hr = ptlib->GetTypeInfoOfGuid(iid, &ptinfo);
if (FAILED(hr))
{
ptlib->Release();
return hr;
}
ptlib->Release();
*pptinfo = ptinfo;
return NOERROR;
}
我们还允许外部请求 IDispatch
接口,因为我们现在完全支持它
HRESULT __stdcall CoCOMServer::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown || iid == IID_ICOMServer || iid == IID_IDispatch)
{
*ppv = static_cast<ICOMServer*>(this) ;
}
else
{
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
return S_OK ;
}
我们还为 IDispatch
接口本身提供了默认实现
HRESULT __stdcall CoCOMServer::GetTypeInfoCount(UINT* pctinfo)
{
*pctinfo = 1;
return S_OK;
}
HRESULT __stdcall CoCOMServer::GetTypeInfo
(UINT itinfo, LCID lcid, ITypeInfo** pptinfo)
{
*pptinfo = NULL;
if(itinfo != 0)
return ResultFromScode(DISP_E_BADINDEX);
m_ptinfo->AddRef(); // AddRef and return pointer to cached
// typeinfo for this object.
*pptinfo = m_ptinfo;
return NOERROR;
}
HRESULT __stdcall CoCOMServer::GetIDsOfNames
(REFIID riid, LPOLESTR* rgszNames, UINT cNames,
LCID lcid, DISPID* rgdispid)
{
return DispGetIDsOfNames(m_ptinfo, rgszNames, cNames, rgdispid);
}
HRESULT __stdcall CoCOMServer::Invoke(DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
return DispInvoke(
this, m_ptinfo,
dispidMember, wFlags, pdispparams,
pvarResult, pexcepinfo, puArgErr);
}
构建项目,并注册组件。
又完成了!此 COM 组件支持自动化语言、早期绑定和后期绑定。
让我们玩玩。运行 MS Word,然后进入 Visual Basic 编辑器,并复制/粘贴此代码片段
Sub Macro1()
Dim obj As ICOMServer
Set obj = CreateObject("COMServer.object")
Dim szname As String
szname = obj.Name ' explicit Name([out,retval] BSTR*) method call
MsgBox szname
End Sub
在运行它之前,不要忘记进入工具 \ 引用并为 automcomserver.tlb 类型库添加一个引用。
我们不传递 string
作为输入参数的原因(与 .idl 接口建议的相反)是,当一个参数明确标记为 [out,retval]
时,它实际上是一个结果值,因此赋值给 szname
变量。
一个自动化 COM EXE
出于多种原因,最好在一个单独的进程中运行 COM 组件。出于安全和性能原因的进程隔离只是举几个例子。但这就是事情开始变得有点棘手的地方。事实上,对于最终用户来说,.exe COM 服务器的使用方式与 .dll 几乎相同:CoCreateInstance
、QueryInterface
、方法调用、Release。但开发人员必须处理所有实现细节。
我们将重用我们的代码,添加一个消息循环,并将类对象注册到 COM 库知道的另一个表,该表仅用于进程。
结果代码包含在 automexeserver.zip 包中。
让我们创建一个新项目,这次是 Win32 应用程序而不是 Win32 动态库,并将其命名为 automexeserver。确保 automexeserver.cpp 包含以下内容
// automexeserver.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
return 0;
}
现在复制、重命名并将所有这些文件添加到此项目中
- automcomserver.idl ==> automexeserver.idl
- automcomserverImpl.h ==> automexeserverImpl.h
- automcomserverImpl.cpp ==> automexeserverImpl.cpp
- Registry.h .cpp ==> Registry.h .cpp(此处无需更改)
当然,您需要在文件中进行一些内部更改以反映新的 automexeserver 名称:您需要替换引用 automcomserver 头文件的 #include
语句,并且需要进入项目设置并更改 automexeserver.idl 的 MIDL 选项:将“输出头文件名”字段填写为 automexeserver_i.h。
正如您可能已经猜到的,我们不再需要 automcomserver.def,它用于声明导出函数。事实上,进程不需要导出任何函数,因为将使用的方法调用是 LPC/RPC,而不是我们到目前为止看到的简单的进程内方法调用。无需惊慌,我们不会深入研究封送处理的狂热。所以放松吧。
构建项目是可以的,但它还不能如预期那样工作。
首先,让我们给应用程序入口点添加一些功能。在这里,我们将针对 COM 进程的内部表进行初始化,然后通过可中断的消息循环。 automexeserver.cpp 应该完全反映这一点
// automexeserver.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include <objbase.h> //
#include "automexeserver_i.h"
#include "automexeserverImpl.h"
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// register/unregister server on demand
//
char szUpperCommandLine[MAX_PATH];
// copy command line and work with it.
strcpy (szUpperCommandLine, lpCmdLine);
strupr (szUpperCommandLine);
if (strstr (szUpperCommandLine, "UNREGSERVER"))
{
DllUnregisterServer();
return 0;
}
else if (strstr (szUpperCommandLine, "REGSERVER"))
{
DllRegisterServer();
return 0;
}
// initialize the COM library
::CoInitialize(NULL);
// register ourselves as a class object against the internal COM table
// (this has nothing to do with the registry)
DWORD nToken = CoEXEInitialize();
// -- the message poooommmp ----------------
//
// (loop ends if WM_QUIT message is received)
//
MSG msg;
while (GetMessage(&msg, 0, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// unregister from the known table of class objects
CoEXEUninitialize(nToken);
//
::CoUninitialize();
return 0;
}
需要一些说明
- 正如您所见,用于启动进程的命令行被检查是否可能带有 /regserver 或 /unregserver 参数。确实,COM 进程不是使用标准的 regsvr32 命令行注册的。我们拥有
DllRegisterServer()
和DllUnregisterServer()
的事实不应被误解。我们拥有一直使用的相当方便的注册助手。所以再次使用它们是明智的。但是Dll
前缀并不意味着我们正在注册一个 DLL。事实上,注册的几乎唯一的区别是我们最终有一个LocalServer32
注册表项而不是InProcServer32
。 - 然后我们将自己注册到内部 COM 类对象表中(由 R.O.T. 使用)。
- 然后是消息循环,等待
WM_QUIT
消息,同时处理沿途的所有其他消息。需要注意的是,当您手动杀死进程时,操作系统会发送此WM_QUIT
消息。我们将模仿这一点,以便在客户端应用程序不再需要我们时自我销毁。
为了完成实现,我们在 automexeserverImpl.cpp 中提供了这四个函数:DllRegisterServer()
、DllUnregisterServer()
、CoEXEInitialize()
和 CoEXEUninitialize()
CFactory gClassFactory;
DWORD CoEXEInitialize()
{
DWORD nReturn;
HRESULT hr=::CoRegisterClassObject(CLSID_CoCOMServer,
&gClassFactory,
CLSCTX_SERVER,
REGCLS_MULTIPLEUSE,
&nReturn);
return nReturn;
}
void CoEXEUninitialize(DWORD nToken)
{
::CoRevokeClassObject(nToken);
}
//
// Server registration
//
STDAPI DllRegisterServer()
{
g_hModule = ::GetModuleHandle(NULL);
HRESULT hr= RegisterServer(g_hModule,
CLSID_CoCOMServer,
g_szFriendlyName,
g_szVerIndProgID,
g_szProgID,
LIBID_LibCOMServer) ;
if (SUCCEEDED(hr))
{
RegisterTypeLib( g_hModule, NULL);
}
return hr;
}
//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
g_hModule = ::GetModuleHandle(NULL);
HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
g_szVerIndProgID,
g_szProgID,
LIBID_LibCOMServer) ;
if (SUCCEEDED(hr))
{
UnRegisterTypeLib( g_hModule, NULL);
}
return hr;
}
别忘了,当客户端应用程序告诉我们时,我们需要销毁自己。让我们更新 CoCOMServer::Release()
的实现
ULONG __stdcall CoCOMServer::Release()
{
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
::PostMessage(NULL,WM_QUIT,0,0);
return 0 ;
}
return m_cRef ;
}
现在您可以构建此项目了。在使用 COM 服务器之前,不要忘记命令行:automexeserver.exe /regserver
。您可以使用 VB 代码片段来测试它(不要忘记引用类型库,现在是 automexeserver.tlb)。
如果您想使用 C/C++ 客户端应用程序测试组件,可以使用 TestAutomexeserver.dsp 项目(与 TestSimplecomserver.dsp 的唯一区别是包含了正确的接口头文件。经验表明,如果包含错误的文件,运行时几乎肯定会出现 GPF!)。
终于完成了!这很难吗?
本文为您提供了三个实际的代码示例,反映了您在接近 COM 边界时需要的三种代码。
希望您觉得它们有用。
历史
- 2002 年 11 月 10 日:初始版本
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。