将 ATL 服务应用程序迁移到 Visual C++.NET





5.00/5 (12投票s)
2002 年 7 月 29 日
11分钟阅读

256069

1641
将基于 ATL 的 Windows 服务应用程序迁移到 Visual C++.NET 的指南
迁移
1. 从一个国家或地区迁移到另一个国家或地区,以便居住;改变居住地;搬迁;例如,摩尔人从非洲迁移到西班牙;迁移到西部。
2. 为觅食或繁殖而周期性地从一个地区或气候移居到另一个地区或气候;——指某些鸟类、鱼类和四足动物。
Webster’s Revised Unabridged Dictionary (韦氏修订版未删节词典)
引言
本文介绍了将使用 Visual C++ 6.0 和 ATL 编写的 Windows 服务应用程序(也称为 NT 服务)迁移到 Visual C++.NET 所需的步骤。大多数 ATL 和 MFC 代码在将 Visual Studio 从 6.0 版本升级到 .NET 时只需重新编译即可。不幸的是,ATL 服务应用程序并非如此。处理 Windows 服务的 ATL7 部分已被重写,因此 Visual C++ 6.0 中 ATL 向导生成的代码大部分不再需要——实际上,代码仍然在使用,但已被移至 ATL 类,而不是由向导复制到您创建的每个服务应用程序中。此改进的缺点是它会破坏使用 ATL3(Visual C++ 6.0 附带的版本)编写的应用程序。
那么,如果您有一个在用 Visual C++ .NET 重新构建后不再工作的服务应用程序,您应该怎么办?您有三个主要选择:
- 用 C# 重写整个应用程序(嘿,是不是很酷?)
- 保留 Visual C++ 6.0 的副本——仅用于维护您的服务应用程序。
- 进行必要的更改,使您的代码与 ATL7 兼容。
第一个选项并不像乍看之下那么有吸引力。Windows 服务应用程序通常用于管理低级资源,包括硬件。其源代码经常依赖于第三方库——通常是“C”库。此类应用程序需要经过非常彻底的调试(与 UI 程序相比),一旦代码稳定,它通常会持续很长时间。
第二个选项在团队资源分配方面是最便宜的,但它在其他方面也很便宜。当您需要放弃一个问题(可能是个小问题)而不是修复它时,难道不会让您作为开发者感到恼火吗?
因此,我们将采用第三种方法:修复它!正如您将看到的,这个选择不仅可以纠正程序行为,还可以改进和简化源代码。
第一阶段。生命的意义
是的,您终于找到了生命的意义,并想告诉全世界。也许,那些读过道格拉斯·亚当斯书籍的人已经知道了答案(42,还记得吗?),但您觉得他们的数字还不够。因此,您使用 Visual C++ 6.0 和 ATL 构建了一个简单的 Windows 服务应用程序。只需几分钟,您的服务就运行起来了。我包含了一个包含 ATL 服务源代码和测试命令行应用程序的 Visual C++ 6.0 工作区。以下是该项目的亮点:
- 该服务名为
MyService1
。使用命令行提示符下的命令"net start MyService1"
和"net stop MyService1"
来启动和停止服务,或者您可以使用“控制面板”的“服务”小程序。 - 在启动服务之前,您应该通过发出命令
"MyService1 /Service"
来注册它(当 ATL 向导生成服务代码时,它不包含在自定义构建步骤中的服务注册)。 - 该应用程序还包括一个接口 (
IMeaningOfLife1
) 及其在 coclassMeaningOfLife1
中的实现。接口中唯一的方法(只读属性)是MeaningOfLife
,当然,它应该返回 42。 - 由于应用程序没有线程亲和性,因此它被实现为自由线程。
此示例服务的项目代码包含在本文档中(项目 MyService1
)。
第二阶段。问题
您的服务运行得非常好,每天有成千上万的人找到生命的意义,您有了新的想法(也许不那么哲学,但这次能给您带来收入)。您渴望新技术,并且一见钟情就爱上了 .NET。因此,您安装了 Visual Studio.NET,并重新编译了所有项目。当您准备清理一些磁盘空间并将 Visual Studio 6.0 视为最佳候选对象时,您发现有一个应用程序未能适应工具升级:您著名的 MyService1
,它为大众提供 MeaningOfLife
。您想调试该应用程序,但甚至无法将其作为服务启动!当您从测试客户端连接到 MeaningOfLife
组件时,它运行正常。服务级别肯定出问题了。
第三阶段。分析
ATL3(随 Visual Studio 6.0 一起发布)对 Windows 服务提供了完全支持,但服务处理代码确实缺乏封装。如果您运行 ATL 向导并生成了服务代码,您可能会发现以下情况:
- 头文件
"stdafx.h"
不仅包含必要的系统头文件,还包含CServiceModule
类的完整声明,该类必须(并且确实)插入在"atlbase.h"
和"atlcom.h"
的引用之间。这不是声明新类的最佳位置! - 主要的 C++ 服务文件包含超过 400 行代码,实现了
CServiceModule
函数。即使您需要自定义服务,您也不会更改其中大部分函数(您为什么要更改FindOneOf
、Lock
或LogEvent
?)您通常需要的是能够为服务初始化和终止添加自定义步骤。不幸的是,您必须直接修改CServiceModule::Run()
。我使用 ATL3 编写了几个 Windows 服务,它们都具有非常相似的CServiceModule
代码,仅在类 GUID 和CServiceModule::Run()
方法内部对自定义初始化例程的调用方面有所不同。
Visual C++.NET 中处理 Windows 服务的 ATL7 部分已完全重新设计。与 ATL 的其他类类似,服务模块已变为模板 CAtlServiceModule
,主要功能已内联在 ATL 头文件中。如果您保留旧代码不变,那么现在会发生什么情况是,您的服务将包含旧版本的 CServiceModule
代码(由 ATL 向导生成),而 ATL 头文件有其自己的——经过改进的版本。真正的问题是新实现与旧实现不兼容。幸运的是,这很容易修复。迁移您的 Windows 服务代码在很大程度上变成了删除 ATL 向导过去注入您项目的数百行代码。
第四阶段。解决方案
现在我们知道了问题的根源,并且很容易修复(至少根据承诺),让我们来做吧。我包含了一个新版本的 MyService1
应用程序(这次命名为 MyService2
,因此它们可以共存)。我重点介绍了使新版本能够工作的一些必要更改。我还列出了一些改进代码质量的简单更改。
- 从
"stdafx.cpp"
中删除以下代码#ifdef _ATL_STATIC_REGISTRY #include <statreg.h> #include <statreg.cpp> #endif
此步骤不是强制性的,只是您不再需要的代码。
- ATL 注册表类中的某些函数已被弃用(并被新的强类型函数取代)。您应该分别将
SetValue
和QueryValue
的调用替换为SetStringValue
和QueryStringValue
。
- 向
"stdafx.h"
添加几个新指令#define _ATL_NO_AUTOMATIC_NAMESPACE #define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS #define _ATL_ALL_WARNINGS using namespace ATL;
- 现在是主要步骤。从
"stdafx.h"
中删除CServiceModule
声明。让 ATL7 使用最新的!
- 从您的服务 C++ 文件中删除
CServiceModule
实现。此步骤需要一些精确度,因为您可能已经包含自定义服务初始化和清理代码(还记得CServiceModule::Run()
方法吗?)。您应该将此代码移动到 ATL7 现在提供的CServiceModule::PreMessageLoop
和CServiceModule::PostMessageLoop
方法。因此,您的服务模块类声明应如下所示:class CServiceModule : public CAtlServiceModuleT<CServiceModule, IDS_SERVICENAME> { public: DECLARE_LIBID(LIBID_MYSERVICE2Lib) DECLARE_REGISTRY_APPID_RESOURCEID(IDR_MyService2, "{E3746A83-B411-4652-A929-0E9B3C0A05A3}") HRESULT PreMessageLoop(int nShowCmd); HRESULT PostMessageLoop(); };
服务初始化和清理例程(PreMessageLoop
和PostMessageLoop
)将包含您需要的任何自定义步骤。 - 您应该开始使用新的
OBJECT_ENTRY_AUTO
宏,而不是传统的对象映射(由BEGIN_OBJECT_MAP
和END_OBJECT_MAP
包装)。因此,从您的服务实现文件("MyService2.cpp"
)中删除以下代码:BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_MeaningOfLife2, CMeaningOfLife2) END_OBJECT_MAP()
并将新代码添加到您的 coclass 头文件(在我们的例子中是"MeaningOfLife2.h"
):OBJECT_ENTRY_AUTO(__uuidof(MeaningOfLife2), CMeaningOfLife2)
- 最后但同样重要的是。您还记得您的应用程序是自由线程的吗?这是另一个陷阱:ATL7
CAtlExeModuleT::PreMessageLoop
有一个错误,它阻止服务创建自由线程组件(当应用程序作为服务启动时)。我找不到有关此主题的知识库文章,但一位 Microsoft 员工在新闻组中提供了一个变通方法(在 Google 上搜索PreMessageLoop
和CoResumeClassObjects
)。最后,您的PreMessageLoop
/PostMessageLoop
方法应如下所示:HRESULT CServiceModule::PreMessageLoop(int nShowCmd) { HRESULT hr = __super::PreMessageLoop(nShowCmd); #if _ATL_VER == 0x0700 if (SUCCEEDED(hr) && !m_bDelayShutdown) hr = CoResumeClassObjects(); #endif if (SUCCEEDED(hr)) { // Add any custom code to initialize your service } return hr; } HRESULT CServiceModule::PostMessageLoop() { // Add any custom code to free your service resources return __super::PostMessageLoop(); }
如果您现在重新构建您的服务(并使用 "MyService2 /Service"
命令注册它),它将成功启动和停止。讽刺的是,如果您然后使用提供的客户端应用程序测试 MeaningOfLife
组件,它将无法实例化它,抛出臭名昭著的 0x80080005
结果代码(“服务器执行失败”)。进一步研究使我进行了其他更改:
经过更新的(VC++.NET 兼容)示例 Windows 服务的源代码包含在本文档中(项目 MyService2
)。
第五阶段。属性化生命的意义
让我们现在面对现实。您的新代码现在看起来更优雅——它更短,它只实现特定于应用程序的函数,并且——对于欣赏它的人来说——它是面向对象的。但是既然您已经使用了 ATL7 的新功能,也许还有其他一些功能可以使您的代码更加紧凑和易于管理。是的,您的代码仍然继承了 ATL3 的源文件结构,即它包含:
- 包含接口和类型库声明的 IDL 文件;
- 包含 coclass、类型库和服务 GUID 重复的 RGS 文件;
- 包含 coclass 和服务类声明及实现的 C++ 头文件和实现文件(尽管与 ATL3 相比,大小已大大减小)。
这种结构有什么问题?当然,这不仅仅是文件数量。更难管理的是不同文件必须包含重复信息(如接口方法和 GUID)。虽然更改接口而不更新其实现会导致编译器错误(并且会被解决),但如果您忘记在所有包含的地方更新 GUID 值,您将陷入注册表地狱,这与 DLL 地狱没什么区别(毕竟地狱就是地狱!)。
欢迎来到属性化编程!通过添加 ATL 属性,您可以使代码更加紧凑和易于管理。属性非常简单——它们提供与 ATL3 中的宏类似的功能,但它们更智能。宏仅影响原地(即,无论它被插入在哪里),作为 C++ 的一个实体,它无法控制程序的其他部分,即使同时需要更新多个地方。一个属性不限于一个代码段——一个属性可以触发源代码、资源和注册表脚本的生成。因此,使用属性有助于您编写更少的代码,同时降低使其不一致的风险。
如果您有动力进行属性化编程,您现在可以对您的应用程序进行以下更改:
- 选择项目属性,突出显示 **常规** 配置属性,对于条目 **ATL 的使用**,选择 **静态链接到 ATL**。
- 将
_ATL_ATTRIBUTES
添加到预处理器定义。
- 在 **链接器** 类别中选择 **嵌入式 IDL**,并为 **合并 IDL 基本文件名** 条目指定名称。例如,如果您选择
_MyService.idl
,在生成代理/存根和类型库文件时,它将导致_MyService
被用作基本文件名。
- 选择 **MIDL** 类别,并选择 **是** 来 **生成无存根代理**。
- 从您的项目 IDL 文件和 coclass RGS 文件中删除——这些文件不再需要了!(保留服务 RGS 文件作为项目中唯一的 RGS 文件)。
- 现在修改您的服务 RGS 文件,使其看起来像这样:
HKCR { NoRemove AppID { '%APPID%' = s 'MyService3' 'MyService3.EXE' { val AppID = s '%APPID%' } } }
正如您所见,该文件不再包含 AppID GUID 值——它引用了它。
- 编辑项目 RC 文件,删除所有对类型库(TLB)文件和过时的 RGS 文件的引用。
- 重写服务类声明,使其使用 ATL 属性。这是您修改后的服务类外观:
[module(SERVICE, uuid="{3D87B677-0D7E-40ba-B0D4-D5D7C3B28BA6}", name="MyService3", helpstring="MyService3 1.0 Type Library", resource_name="IDS_SERVICENAME")] class CServiceModule { public: HRESULT PreMessageLoop(int nShowCmd); HRESULT PostMessageLoop(); };
请注意,CServiceModule
不再显式继承自CAtlServiceModuleT
模板(尽管隐含关系仍然存在)。
- 删除
_tWinMain
定义。您甚至不需要它!
- 现在重写您的 coclass 代码。首先,您需要声明接口(因为 IDL 文件不再是项目的一部分):
[ object, uuid("5D5DFA33-7F6D-41b6-BC07-3CFF781B7D4C"), dual, helpstring("IMeaningOfLife3 Interface"), pointer_default(unique) ] __interface IMeaningOfLife3 : IDispatch { [propget, id(1), helpstring("property MeaningOfLife")] HRESULT MeaningOfLife([out, retval] LONG* pVal); };
然后更新相应的 coclass 声明:[ coclass, threading("neutral"), vi_progid("MyService3.MeaningOfLife3"), progid("MyService3.MeaningOfLife3.1"), version(1.0), uuid("F5602F4B-00D6-445b-A34F-86F950CE2917"), helpstring("MeaningOfLife3 Class") ] class ATL_NO_VTABLE CMeaningOfLife3 : public IMeaningOfLife3 { public: CMeaningOfLife3() { } DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { } public: STDMETHOD(get_MeaningOfLife)(/*[out, retval]*/ long *pVal); };
您还应该删除OBJECT_ENTRY_AUTO
行——属性将处理它。
结论
我们最初的努力只是为了解决 ATL3 和 ATL7 Windows 服务应用程序之间的不兼容问题。然而,使用新的 ATL 功能也帮助我们减少了项目文件数量及其代码大小。如果我们只计算重要的项目文件(不包括不需要手动维护的自动生成文件),我们将得到以下数字:
-
MyService1
(ATL3):10 个文件,19851 字节 -
MyService2
(ATL7,无属性):10 个文件,9485 字节 -
MyService3
(ATL7,带属性):8 个文件,6987 字节
尽管我们在 Windows 服务应用程序中不得不处理 ATL 不兼容问题,但最终的结果是值得的。ATL7 为您承担了大部分保持代码一致性和可读性的工作,因此您可以将您的创造力用于更重要的任务。
参考文献
- The Hitchhiker's Guide to the Galaxy (《银河系漫游指南》),作者:Douglas Adams,Ballantine Books。ISBN: 0345391802。
- Developing Applications with Visual Studio .NET (《使用 Visual Studio .NET 开发应用程序》),作者:Richard Grimes。Addison Wesley Professional。ISBN: 0201708523。
- MSDN Library