为 Win32 应用程序添加 COM 自动化支持






4.78/5 (16投票s)
如何为现有的 Win32 应用程序提供 COM 自动化功能?
引言
互联网上存在大量文章和教程,详细介绍了如何编写支持自动化的 COM 服务器应用程序。这些文章和教程涵盖了开发这些支持 COM 的应用程序的各种方法,包括使用纯文本编辑器手动编写所有内容,使用 ATL 类和宏,使用 ATL 向导,使用 C++ 属性等等。然而,所有这些文章和教程都存在一个小问题;它们只解释如何从头开始开发应用程序,而不是如何为现有应用程序添加 COM 自动化支持功能。
在本文中,我将向您展示如何获取现有的 Win32 应用程序并为其添加 COM 自动化支持。当一个应用程序拥有多年开发积累的众多功能和复杂功能,并且从头开始重新开发以确保从一开始就添加 COM 自动化内容既困难又不可行时,出现这种需求是很有可能的。
使用代码
下载包中的代码是使用 Visual Studio 2010 开发的。但这并不意味着其开发过程中涉及的任何步骤都特定于该版本的 Visual Studio;事实上,这里描绘的相同步骤可以在任何其他版本的 Visual Studio 中使用。请注意,要打开解决方案,您需要使用 Visual Studio 2010 或更高版本。
另一点需要注意是,我使用了属性来添加 COM 自动化基础结构代码。这不是开发 COM 应用程序的唯一方法,但它被认为是最简单的方法之一,因为它为开发人员节省了编写大量样板代码的工作。使用任何其他方法都会使本文的篇幅大大增加。关键在于展示添加 COM 自动化支持所需的代码以及放置它的位置,而不是讨论构建 COM 应用程序所需的各种方法和组件。
演练
在接下来的几个部分中,我们将开发一个简单的 Win32 应用程序,然后添加最少的结构代码,使其支持 COM 自动化。在此之后,添加任何其他功能并通过我们的 COM 自动化接口公开它们将相对容易。
创建一个 Win32 应用程序
这只是一个初步步骤,以便我们有东西可以构建我们之后的工作。当然,在实际情况下,您手中已经有了要转换的应用程序。对于此步骤,我选择使用“Win32 Project”向导创建一个名为 NaiveApp 的项目,如下所示。
请注意,我没有勾选右侧的 ATL 选项,即使我稍后会使用它们。我想强调的事实是,Win32 应用程序的构建并没有打算使用 ATL 和 COM,这在实际情况下非常真实。
如果您运行该应用程序,您会发现一个带有帮助菜单栏的空窗口。帮助菜单包含一个名为“关于…”的项目,单击时会显示一个关于框。这是一个非常简单且微不足道的应用程序,但它足以满足我们的说明目的。
修改项目设置和代码
现在我们有了一个可以处理的应用程序,下一步是开始添加粘合代码,使我们的应用程序能够提供 COM 自动化对象。如前所述,有不同的方法可以做到这一点,但我将使用 Visual C++ 支持的 C++ 属性以及一些 ATL 来完成任务,同时最大限度地减少需要编写的代码。熟悉 COM 服务器编程其他方法的开发人员可以使用这些方法获得相同的结果。
我们将按以下顺序修改项目。重要的是要注意,顺序并非严格强制,但它们通常遵循执行这些操作的逻辑和直观顺序。
- 我们必须告诉项目它将使用 ATL。为此,我们在项目属性的“常规”属性页中将“ATL 使用情况”设置更改为“静态链接到 ATL”。
确保为所有配置更改此设置,而不是仅为单个配置更改。 - 现在我们添加注册表脚本。这是包含注册表项结构的文件,这些项将被写入注册表,以便操作系统能够识别我们的新 COM 服务器。在资源视图中,我们添加一个类型为 REGISTRY 的自定义资源(因为它不是内置的标准类型之一)。
现在您会看到 Visual Studio 对我们帮助不大。它自动将 REGISTRY 资源命名为IDR_REGISTRY1
,并将二进制文件 registry.bin 与资源关联(要查看二进制文件,请切换到解决方案资源管理器视图)。这不太好,因为注册表脚本文件通常具有 .rgs 扩展名,并且是纯文本文件,而不是二进制文件。我们很快就会解决这个问题,但首先将以下代码粘贴到打开的二进制编辑器中并保存。HKCU { Software { Classes { AppID { '%APPID%' = s 'NaiveAppServer' 'NaiveApp.exe' { val AppID = s '%APPID%' } } } } }
我不会详细介绍注册表脚本的语法(您可以在 MSDN 网站上阅读)。这里重要的是要注意,您应该将应用程序名称替换为上面的 NaiveAppServer 和 NaiveApp.exe。现在回到错误的资源和文件名问题,我不得不关闭 Visual Studio 中的解决方案,因为在打开时无法修复。我通过 Visual Studio ATL 向导创建了一个 COM 服务器测试应用程序(另一个单独的项目),并通过检查向导如何通过不同的项目文件生成和链接注册表脚本和资源来找到解决此问题的办法。所以这里是解决此问题的步骤
- 完全关闭 Visual Studio 中的解决方案。
- 将文件 registry.bin 重命名为 NaiveAppServer.rgs。
- 在文本编辑器(如记事本)中打开 NaiveApp.rc 并更改行
IDR_REGISTRY1 REGISTRY "registry.bin"
toIDR_NAIVEAPPSERVER REGISTRY "NaiveAppServer.rgs"
- 在文本编辑器中打开 resource.h 并将
IDR_REGISTRY1
替换为IDR_NAIVEAPPSERVER
。 - 在文本编辑器中打开 NaiveApp.vcxproj 并将 registry.bin 替换为新名称 NaiveAppServer.rgs。在 NaiveApp.vcxproj.filters 中执行相同的操作。
- 现在您可以重新打开 Visual Studio 中的解决方案。
- 下一步是添加使用 ATL 所需的一些头文件和宏。在 NaiveApp.cpp 的顶部附近,我们添加以下行。
#define _ATL_APARTMENT_THREADED #define _ATL_ATTRIBUTES #include <atlbase.h>
第一个宏指定我们将使用单线程单元模型。第二个宏使我们能够使用 ATL 属性,正如我们稍后将要做的那样。 - 我们的 COM 服务器需要一个新函数(Visual Studio 生成的函数名为
_tWinMain
,这是一个遵循 Microsoft 通用文本映射范例的名称,但我们将简单地使用WinMain
,因为它更常用)。这个新函数应该初始化 COM 并执行其他一些 COM 维护工作。我们可以在应用程序的WinMain
函数中编写这些内容,但幸运的是,我们不必这样做。有一个名为module
的属性,我们可以用它来装饰一个类并达到相同的效果。此属性将指示编译器生成一个正确处理 COM 的WinMain
函数(即初始化、清理、注册请求等)。该属性还定义了我们的 COM 自动化对象将驻留的类型库。以下代码将添加到 NaiveApp.cpp 中,位于上一步代码之后。[module(exe, name = "NaiveAppServer", helpstring = "NaiveAppServer 1.0 Type Library", resource_name = "IDR_NAIVEAPPSERVER")] class NaiveAppSrvMod { public: void RunMessageLoop() throw(); };
请注意,上面的
由于resource_name
引用了我们用来定义注册表脚本的相同资源。我们可以不带类地使用module
属性,但这将使生成的WinMain
函数独立于我们的代码运行。即,它不会给我们任何机会将我们的代码挂钩到提供的WinMain
函数中的任何位置,以便它可以在某个时候执行。请记住,我们希望仍然调用旧的WinMain
函数,因为在实际应用程序中,它将包含应用程序的完整功能。将module
属性应用于类允许该类通过覆盖某些方法来挂钩到生成的WinMain
函数。module
属性定义为类型exe
,因此该类隐式继承自CAtlExeModuleT
,该类提供了一些方法,其中我们感兴趣的是RunMessageLoop
。此方法在 COM 初始化后被调用;其默认实现只是运行将处理 COM 消息的循环。我们在此处覆盖此方法以将执行重定向到我们旧的WinMain
函数,如下一步所示。由于我们的旧WinMain
函数也包含消息循环(与任何 Win32 应用程序一样),因此我们可以确保我们的函数能够正确处理 COM 消息以及它已经执行的所有其他工作。 - 同一个程序不能有两个
WinMain
函数,所以我们必须将旧的WinMain
函数重命名为其他名称,NaiveAppMain
。更改签名int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
toint APIENTRY NaiveAppMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
- 在已重命名的
NaiveAppMain
函数之后,添加NaiveAppSrvMod::RunMessageLoop
的以下定义。void NaiveAppSrvMod::RunMessageLoop() { NaiveAppMain(GetModuleHandle(NULL), NULL, NULL, SW_SHOWNORMAL); }
该方法实现仅执行一件事,即调用我们旧的WinMain
函数。
拥抱您的 COM 自动化服务器。
信不信由你,我们现在有了一个最小的、原始的、工作的 COM 自动化服务器。要查看此效果,请执行以下操作
- 在 Visual Studio 中构建应用程序。您会注意到构建生成了两个新文件:一个名为 vc100.idl,另一个名为 vc100.tlb(根据您使用的 Visual C++ 版本,它们可能有其他名称)。前者是 IDL 文件,其中包含我们从之前添加的
module
属性生成的类型库定义;该文件还将包含我们稍后可能添加的其他定义,正如我们稍后将看到的。好消息是您不必为此文件烦恼;它每次构建应用程序时都会自动生成。后者 vc100.tlb 是类型库文件。 - 注册服务器。打开命令行并键入
NaiveApp /RegServer
那么那个奇怪的选项 /RegServer 是什么意思?还记得我们添加的神奇module
属性以及我们的类从中继承的关联的CAtlExeModuleT
类吗?该类提供的两个好处是选项:/RegServer 和 /UnRegServer。它们分别用于注册和取消注册 COM 服务器。 - 在 OLE/COM 对象查看器中检查 COM 服务器类型库;这是一个随 Visual Studio 一起安装的工具,位于随附的 Windows SDK 文件夹下。我们的库名为“NaiveAppServer 1.0 Type Library (Ver 1.0)”。
但是等等,COM 自动化对象在哪里?
如果您选择在 OLE/COM 对象查看器中查看类型库,您会注意到它实际上是空的,不包含任何 COM 对象或接口。
我们实际上还没有添加任何 COM 自动化对象;我们迄今为止所做的只是准备应用程序以使用 COM 并提供 COM 对象,而没有指定这些对象。现在是时候这样做了。
- 切换到 Visual Studio 中的类视图并添加一个新类。在“添加类”向导中,选择 ATL Simple Object。
然后填写 ATL Simple Object 向导的详细信息(大多数信息实际上是从类的简短名称生成的,所以您可能只需要填写它)。
在“选项”屏幕上,将“线程模型”设置为“Single”,将“聚合”设置为“No”。单个线程模型意味着 COM 消息将由我们的主线程消息循环处理。聚合是我们目前不需要的复杂性。
砰,发生了一系列事情。一个名为 NaiveAppServer.h 的新文件在编辑器中打开,其中包含接口和类定义。此外,类视图现在显示了一个新接口INaiveAppServer
和一个新类CNaiveAppServer
;这些是我们的应用程序将在请求时提供的接口及其关联的 COM 对象。不幸的是,生成的代码不能完全达到预期的效果。我们必须添加
library_block
属性以确保接口在 IDL 文件中生成**在类型库内部**(而不是在外部)。我们必须更改// INaiveAppServer [ object, uuid("E4782C67-CB9D-406D-AEA4-BAD8E970AB1B"), dual, pointer_default(unique) ] __interface INaiveAppServer : IDispatch { };
to// INaiveAppServer [ object, uuid("E4782C67-CB9D-406D-AEA4-BAD8E970AB1B"), dual, pointer_default(unique), library_block ] __interface INaiveAppServer : IDispatch { };
请注意 `library_block` 属性已附加到属性列表中。 - 虽然不是必需的,但为对象附加一个单例工厂仍然有用,这样最多只能创建一个自动化对象的实例。将
DECLARE_CLASSFACTORY_SINGLETON(CNaiveAppServer);
添加到包含DECLARE_PROTECT_FINAL_CONSTRUCT()
的行之后。 - 现在到了我们努力达到的最后一步,向我们的对象添加一个方法。为了简单和演示目的,我们将实现一个非常简单的方法,该方法仅计算数字的平方。在类视图中,向
INaiveAppServer
**接口**(而不是类)添加一个方法。该方法接受一个数字并返回其平方。
如您所见,向导负责许多事情,例如在接口和类中生成正确的函数原型,以及在相应的 NaiveAppServer.cpp 文件中生成定义。 - 不幸的是,NaiveAppServer.cpp 在当前形式下无法编译(您可以自己尝试一下)。我们必须在文件顶部添加一些头文件和宏,类似于我们之前在准备步骤中添加的。将以下行添加到 NaiveAppServer.cpp 的顶部附近(但在包含 NaiveAppServer.h 之前)。
#define _ATL_APARTMENT_THREADED #define _ATL_ATTRIBUTES #include <atlbase.h> #include <atlcom.h>
- 填写方法的实现,使其读起来像这样
STDMETHODIMP CNaiveAppServer::GetSquare(SHORT num, LONG* res) { *res = num * num; return S_OK; }
- 构建应用程序,注册 COM 服务器(如前所述),并在 OLE/COM 对象查看器中查看 COM 服务器的外观;您可以看到类型库现在具有 COM 接口
INaiveAppServer
和类CNaiveAppServer
,以及每个接口中的一个方法。
您甚至可以尝试通过展开“自动化对象”下的“CNaiveAppServer 对象”节点来实例化 COM 服务器中的一个对象;这将打开我们应用程序窗口的一个实例(因为我们的应用程序最初是一个 GUI 应用程序)。应用程序必须运行,以便它可以在其主线程消息循环中处理 COM 相关消息。 - 这标志着我们的目标达成,因为我们现在拥有一个能够提供 COM 自动化对象的 COM 服务器。您可以继续尝试从 C#、Python 等其他语言使用此 COM 服务器来创建 COM 对象并调用其方法。这将让您体会到如何从其他程序中使用 COM 自动化对象。
关注点
这些只是关于可以改进的内容以及我们如何进一步前进的一些亮点。
-
有些人,包括我自己,可能不喜欢通过包含 COM 头文件即可访问所有 COM 符号的事实。事实上,所有 COM 类和类型都定义在
但是请注意,一旦使用此宏,您将立即遇到大量编译错误,因为使用的属性会透明地转换为使用定义在ATL
命名空间中。但是,一旦我们开始包含 COM 头文件,所有 ATL 命名空间类型和类都会被导入。为了防止这种行为,您可以定义宏_ATL_NO_AUTOMATIC_NAMESPACE
,例如#define _ATL_NO_AUTOMATIC_NAMESPACE
,然后才能包含任何 COM 头文件。ATL
命名空间中的许多 COM 类型和类的代码。您必须仔细检查这些消息,以查看需要使用 using 语句导入哪些符号,例如using ATL::_ATL_REGMAP_ENTRY;
。 - 您可能会认为 vc100.idl 不是一个非常有意义的名称(我非常同意)。我们不必坚持这个名字,幸运的是有一种方法可以更改生成的 IDL 文件的名称。在项目属性的“嵌入式 IDL”属性页中,在链接器设置下,有一个“合并 IDL 基本文件名称”设置,您可以将其更改为您认为有意义的任何名称。
- 您始终可以在源文件中检查属性为您使用的代码。当您想知道由于您刚刚添加的某个属性而使用了哪些类和方法时,这总是有帮助的。在项目属性的“输出文件”属性页中,在 C/C++ 设置下,将“展开属性源”更改为“是 (/Fx)”,您就可以在一个单独生成的源文件中查看您的属性扩展到什么代码。
- 请记住,在重新构建服务器后,每次对 COM 服务器的 COM 类进行更改(如添加/删除/修改 COM 接口/类/方法)时,都必须重新注册服务器。有些人甚至选择将此操作添加为构建后钩子,以便在每次构建应用程序后自动执行。
-
如果您在应用程序中处理命令行选项,您应该为应用程序做好支持 -Embedding 选项的准备,因为当外部实体从您的 COM 服务器请求 COM 对象时,COM 基础结构会通过此选项调用您的程序。支持此选项的一种简单方法是让您的应用程序忽略它,除非您希望您的应用程序根据它是被用户交互调用还是被 COM 基础结构调用而表现不同。
无论您如何决定,请记住要考虑它。一些应用程序通常会编写成拒绝未知选项,并在检测到它们时简单地停止,另一些更糟糕的甚至可能崩溃。所以至少确保您的应用程序在看到该选项时不会发生故障。 - 我们添加的方法是一个不切实际的简单方法。您可能需要添加许多,许多其他方法(以及可能的其他 COM 接口和类)来公开应用程序的更多功能。需要注意的一点是,您很可能会遇到需要重构代码才能提供或公开某些功能的情况。
如果您的应用程序使用 GUI,并且功能与 GUI 控件的事件处理程序紧密耦合,那么这一点就更加重要了。在这种情况下,您必须将收集和验证 GUI 控件信息与处理这些信息的逻辑分开,如果您的应用程序从一开始就遵循关注点分离原则进行设计,那么这种做法会容易得多。
- 同样,您可能会发现您想要公开的一些功能可能涉及调用一些 UI 窗口或对话框(例如显示打开文件的对话框或显示一个必须关闭的信息消息框)。通常在自动化模式下,您希望隐藏这种 GUI 交互,并在自动化请求时预先提供所需信息。有几种方法可以实现这一点,例如使用布尔标志来区分交互模式和自动化模式,并相应地显示/隐藏适当的 GUI 元素。另一种方法是将可能的 GUI 显示方法放入抽象类中,并从中派生两个具体类:一个用于自动化模式(它将简单地为查询对话框返回预编程的回复,或对信息性对话框不做任何操作),另一个用于正常 GUI(它将简单地解析为典型的 GUI 方法)。
参考文献
- 演练:使用文本编辑器创建 COM 服务器
- ATL 注册表组件(注册表)
- 属性教程
- ATL 参考
- 单线程单元
- COM 属性
- 声明和实现 COM 接口的宏 - The Old New Thing - Site Home - MSDN Blogs
历史
04-21-2014:
- 原文
04-24-2014:
- 更新引用
- 添加进一步改进提示
06-29-2014:
- 更新引用
- 添加更多关于注册表脚本修复的详细信息
- 添加关于查看属性生成代码的提示