Visual Studio 2005 中的托管代码(ZZ)

发布日期: 2006-6-26 | 更新日期: 2006-6-26

下载本文的代码: CatWork2006_06.exe (262KB)

*
本页内容
较小的语言更改较小的语言更改
迁移到托管环境迁移到托管环境
运行ManWrap运行ManWrap
使用一个新符号 ^使用一个新符号 ^
两个较小的注意事项两个较小的注意事项

很多人对于升级到 Visual Studio® 2005 的过程并不持怀疑态度,因此我想,现在应该介绍一下我自己对新编译器的体验。是什么工作花费了这么长时间呢?嗨,我不是一个很麻利的人!但迟做总比不做好!

关于 Visual Studio 2005,您首先将注意到的是,它有一个查看项目并决定启动哪个版本的版本管理器。您可以安装 Visual Studio 2005 和 Visual Studio .NET 2003;它们可以很好地在计算机中共存,如果您想在闲暇时升级项目,这是非常理想的。如果打开以前的项目,Visual Studio 2005 会提示您在进行转换前保存一个副本,然后生成一个描述发现问题的 XML 报告。

较小的语言更改

为了体现 Visual Studio 2005 的改动,我从过去的专栏中选了一些项目进行编译。它们都需要进行少量编辑,以适应少数语言一致性的更改,这些更改让 Visual Studio 2005 成为了一个现代化的 C++ 编译器。多数"新的"规则成为 C++ 的标准已有一段时间了,但现在只有 Visual Studio 强制推行它们。我看到的两个最常见的语言变化是 for-loop 范围和默认的 int 类型。

循环中的局部变量不再作用于循环之外。以前的编码可能是:

for (int i=0; i0) {
   // do something else
}

在该代码片段中,变量 i 是在 for 语句中定义,而在循环外使用的。在正式的情况下,C++ 并不允许该操作,因此您必须按照以下方式重写代码:

int i; // move outside for loop
for (i=0; i0) {
   // do something else
}

未声明的静态变量(局部和全局)不再默认为整型。以前,您可以编写以下代码

const BUFLEN=255;

而且编译器将隐式分配 BUFLEN 类型 int。隐式 int 现在已被禁止。您必须按以下方式声明类型:

   const int BUFLEN=255;

这适用于所有类型的变量 - 静态、全局、数据成员和函数返回类型。如果遗漏了 int,将得到消息"error C4430: missing type specifier - int assumed. Note: C++ does not support default-int"。

另一大类 C/C++ 更改与新的 Safe C 和 Safe C++ 库相关。这些库为用户已知且偏爱的很多旧式 C 运行时 (CRT) 函数提供了更安全的版本:strcpy、fopen 等。我计划在将来的专栏中编写关于 Safe C++ 的更多内容。如果您急于了解相关内容,请阅读 Martyn Lovell 编写的优秀文章"Safe! Repel Attacks on Your Code with the Visual Studio 2005 Safe C and C++ Libraries",摘自 2005 年 5 月刊(MSDN.microsoft.com/MSDNmag/issues/05/05/SafeCandC)。

C++ 的内容就是这样,那么 MFC 呢?Visual Studio 2005 没有对 MFC 进行较大的更改,正如我之前提到的,这是件好事。这意味着 MFC 是稳定的。但我注意到,CWnd::OnNcHitTest 的返回类型已经从 UINT 变为 LRESULT。还可能还有其他小的调整,但不会有任何会中断 MFC 应用程序的大调整。

迁移到托管环境

C++ 和 MFC 是稳定的编程系统,因此我预计这里不会有任何升级方面的问题。Visual Studio .NET 2003 和 Visual Studio 2005 之间的较大更改就在托管代码中。这里的新 C++/CLI 语法您已经有所了解 - 可将它当作为 Managed Extensions V2。如果您还未将思路转移到跟踪句柄上,则可使用 /clr:oldSyntax 继续处理以前的托管扩展。这就是默认情况下将一个托管/混合项目从 Visual Studio .NET 2003 升级到 Visual Studio 2005 时您得到的内容。

要在托管代码上测试新的编译器,我使用了 ManWrap 库,这个库是为我在 2005 年 4 月的文章"Wrappers: Use Our ManWrap Library to Get the Best of .NET in Native C++ Code"而专门创建的。ManWrap 由一个包装 DLL (RegexWrap.dll) 和以下三个测试程序组成:RegexTest、RegexForm 和 WordMess(请参见图 1)。ManWrap 的功能很强大:虽然很小,但它能够完成一些相当复杂的操作 - 例如,在一个 DLL 中混合托管和非托管代码。RegexWrap 是一个包装公共语言运行库 (CLR) Regex 类的本机 DLL。


图 1 测试托管代码

我保持以前的语法,让编译器继续运行……过了一会儿,我的屏幕上滚动出现了几个错误:"C3395 ... __declspec(dllexport) 无法应用于具有 __clrcall 调用约定的函数。"这是什么东西?

ManWrap 是一个允许在纯本机 C++ 中包装托管类的库,因此您可从未使用 /clr 编译的本机 C++ 代码中调用 CLR。例如,假设您有一个使用以前的 Visual C++® 6.0 编译器的旧式应用程序,现在您想添加一个调用 CLR 的功能。您无法在不打开 /clr 开关的情况下直接从 C++ 调用托管类(而且 Visual C++ 6.0 没有要打开的开关!)。因此,使用托管类的唯一方法是将它们包装到一个具有本机入口点的 DLL 中。ManWrap 提供了一个用于编写此类包装的通用机制。

ManWrap 的核心技术是使用特殊的预定义预处理器符号 _MANAGED 来生成在包装类内、外使用的不同代码。每个包装类都保留单个数据成员,以及针对托管对象的句柄:

#ifdef _MANAGED
# define GCHANDLE(T) gcroot
#else
# define GCHANDLE(T) intptr_t
#endif

包装类使用 GCHANDLE声明其对象句柄,如下所示:

// wrapper for managed Object
class CMObject { 
   GCHANDLE(Object) m_handle;
};

具有 CMObject 的头文件以两种方式编译。生成包装 DLL 时要使用 /clr 进行编译,因此定义 _MANAGED,而且该编译器将 m_handle 看作 gcroot。生成调用包装的本机应用程序时不使用 /clr 进行编译,因此不定义 _MANAGED,而且编译器将 m_handle 看作 intptr_t。这可以正常工作,原因是 gcroot 确保与 intptr_t 具有相同的大小。只有包装 DLL 知道句柄是什么。对于外部(本机)世界,m_handle 只是一个神奇的 Cookie - 类似于 HWND、HINSTANCE 或任何其他类型的句柄。唯一的难题是,复制构造函数和赋值运算符必须是真实的函数,而不是内联函数 - 因此它们调用包装,该包装将句柄作为真实的对象进行操作。(您无法复制 intptr_t 句柄,需要遍历 gcroot。)

因此除了保留这些句柄,ManWrap 包装定义用于构造和复制对象的常用方法。每个包装类也定义一个构造函数和操作符 ->,这样,该包装可以从其相应的托管类型访问本机包装对象。例如,有一个构造函数用于从托管 Regex 创建本机 CMRegex。这些包装类在内部使用该构造函数和操作符 ->。图 2 显示 ManWrap.h 中的代码片段,其中的托管方法在 #ifdef _MANAGED 块中。注意,整个类通过 WREXPORT 导出,后者在生成 DLL 时扩展为 __declspec(dllexport)。这就是引起 C3395 错误的原因。无法使用 __declspec(dllexport) 导出托管方法(具有托管参数的方法),因为本机和托管函数使用不同的调用约定。嗯,这很有意义 - 为什么我首先尝试从本机 DLL 导出托管函数呢?但我并未真正导出它们。这些 DLL 都是以内联方式定义的。本机接口中并不需要托管方法,它也看不到这些方法;但编译器并不知道这一点。显然,Visual Studio 2005 没有以前的编译器(允许我 dllexport 整个类)智能。或者说,它太过智能,因为在多数情况下,从本机代码导出托管方法是毫无意义的。问题是,如果 Visual Studio 2005 具有任何托管方法,那么它不会允许您导出一个类。

怎么办?我想做的是通知编译器,"导出除这三个方法以外的整个类。"即,我希望有一种方法能够针对特定的方法禁止 __declspec(dllexport)。唉,可惜没有这样的选项。在这样的情况下,我可以想到两个解决方案:从类声明中删除 WREXPORT 并将其添加到每个本机方法,或者完全移除令人厌恶的方法。第一种方法更简单,而且它正是我选择的方法。将 WREXPORT 移动到方法声明比较单调且易于出错,因为添加新方法时很容易忘记 WREXPORT。但如果您真的忘了,编译器会提醒您。

如果您真想 dll 导出整个类,第二种方法将绕过令人讨厌的方法:托管复制构造函数和取消引用操作符->。然后不用编写

// inside wrapper class for MClass
(*this)->ManagedMethod();

它以静默方式调用现在失效的操作符 ->,则必须编写:

(static_cast((Object*)m_handle))->ManagedMethod();

好极了!您可以引入一个宏来保存键入信息:

THISOBJ(MClass*)->ManagedMethod();

但是它有点单调,还需要加工一下构造函数。如果您已经完全糊涂了(我想很多读者都有同感),不必担心,我选择了比较简单的第一种方法。图 3 显示修改后的代码,其中的所有方法都带有 WREXPORT。当我进行该更改之后,ManWrap 可进行正常的编译。

运行ManWrap

通过编译器的编译是一回事,而使代码运行又是另一回事了。在 dbgheap.c 中采用 ASSERT 的形式的确让我为难(我都想砸了计算机):

ASSERTE(_CrtIsValidHeapPointer(pUserData));

呀!堆栈跟踪几乎没什么用,都是些系统 DLL 而没有调试符号。这是最糟糕的错误,它最难以跟踪:程序在系统中某处中断了,而您却丝毫不知道原因。嗯,几乎没有线索。进一步查看堆栈跟踪显示出约 50 个帧,表明我的代码试图访问一个名为 g_Allocator 的静态 ATL 变量。哈哈!这就是中断了该示例的细微线索。

g_Allocator 是一个静态全局变量。初始化 C++ 静态变量一直是一项细致的工作,尤其是在 DLL 中。编译器必须生成代码以调用 CRT 初始化函数,该函数在调用 DllMain 之前初始化静态变量。每个操作在本机模式中都可以正常工作,但如果您的 DLL 调用托管类,那么您就可能遇到加载器锁问题:Windows® 尝试加载您的 DLL,后者尝试加载 CLR,CLR 进而尝试加载您的 DLL - 应用程序由于所谓的加载器锁问题慢慢停下来。通常,在加载 DLL 的同时无法加载(要求)一个 DLL。当尝试调用 ::MessageBox 显示来自 DllMain 或静态对象构造函数的诊断时,通常就会发生这种情况 - 它无法工作。

要避免加载器锁,Visual Studio .NET 2003 要求托管的 DLL 应该是 /NOENTRY DLL(不具有 DllMain 入口点的 DLL)。这确保避免了加载器锁的问题,但这无疑于因噎废食:现在静态变量并不初始化,原因是您的 DLL 未得到 _DllMainCRTStartup(这是一个神奇的 CRT 函数,它对静态变量进行初始化)。我在 2005 年 2 月刊的专栏中详述了这一难点 (MSDN.microsoft.com/MSDNmag/issues/05/02/CATWork)。不具有静态对象是个大问题,因为 ATL 和 MFC 都需要它们。如果没有 DllMain,您无法编写使用 ATL 或 MFC 的混合 DLL。由于这是不可接受的,因此友好的 Redmondtonians 提供包含函数 __crt_dll_initialize 和 __crt_dll_terminate() 的特殊文件 <_vcclrit.h>,您可调用这些函数来初始化静态变量。

如果这就像一台 Rube Goldberg 计算机一样,那就对了。如果您了解到 Visual Studio 2005 修复了混合程序集加载器锁问题,我想您会很高兴。您不需要 _vcclrit.h 或 /NOENTRY,就像您能够以常规方式编译混合模式的 DLL 一样。有关详细信息,请参阅"Initialization of Mixed Assemblies",网址是 MSDN2.microsoft.com/ms173266.aspx

那么,ManWrap 是如何在 dbgheap.c 中崩溃的呢?因为我在以前的项目中仍然没使用 /NOENTRY。当然,我的程序出问题了。它没有 DllMain。我的静态对象(包括 ATL 的 g_Allocator)没有进行初始化。当我删除 NOENTRY 时,ManWrap 可以正常运行。哇!

使用一个新符号 ^

当最终修复了这些令人讨厌的错误后,我想您会感到非常开心。成功编译并运行所有这三个 ManWrap 测试程序(RegexTest、RegexForm 和 WordMess)之后,我对托管世界充满了兴趣。为什么不更进一步尝试使用新语法呢?假如您去年一直在闲着,我可以告诉您,C++/CLI 的核心内容是引入了一个称为跟踪句柄的新类型,该句柄由符号 ^ 标识。因此我屏弃了 /clr:oldSyntax(参见图 4)并将 ManWrap.h 中的一个字符从 * 更改为 ^:

#ifdef _MANAGED
# define GCHANDLE(T) gcroot
#else
# define GCHANDLE(T) intptr_t
#endif

图 4 使用新语法

进行这个小的更改之后,我将 ManWrap 添加到编译器,并在错误出现时逐一修复它们。主要就是在有托管 Mumble 的位置,将 Mumble* 更改为 Mumble^。当然,要避免一些语法上的麻烦。这里还有一个使用 ManWrap 时遇到的特殊语法列表。对于熟悉 C++/CLI 的读者而言,它们看起来有些落伍,如果您是 CLI 专家,就跳过这部分。

托管类现在必须使用引用或值来声明,而不是 __gc 或 __value。通常情况下,所有 __managed 关键字已经使用上下文敏感的关键字进行了替换。

Default 索引程序现在称为"default"而不是 Item。不要编写:

x = m->Item[name];

现在必须编写:

x = m->default[i];

甚至当您有多个索引程序时(就像 Regex 库中的 MatchCollection 一样),它也能够正常工作。

MatchCollection* mc;
mc->default[0];       // int
mc->default["alpha"]; // string

托管对象必须使用 gcnew 进行分配。无论在何处分配托管对象,请将 new 更改为 gcnew。

某些协定不再是隐式的。请考虑以下代码片段:

// native entry
void Foo(LPCTSTR lpsz)
{
   // managed ctor takes a String
   Mumble *m = new Mumble(lpsz); 
}

使用以前的托管扩展,编译器将隐式创建一个从 lpsz 初始化的新托管 String,以便构造 Mumble。使用 Visual Studio 2005,必须显式分配该 String,如下所示:

void Foo(LPCTSTR lpsz)
{
   Mumble ^m = gcnew Mumble(gcnew String(lpsz));
}

它键入的信息更多,但它也更易于理解。我喜欢 gcnew,因为它促使您了解何时从托管堆进行分配。隐式转换使该代码显得更简洁 - 但并不是真正的简洁。C++ 是相对低级的语言(我认为这是个福气),因此最好让一切保持可见而不要在后台执行太多的操作。由于 RegexWrap 到处创建 String,因此我编写了一个宏来保存键入。

#define S(s) (gcnew String(s))

这旨在模仿托管字符串的 S 修饰符,例如,S"Hello, world"。现在我可以编写:

Mumble ^m = gcnew Mumble(S(lpsz));

C++/CLI 有一个用于托管数组的新语法。不要编写

ManagedType* myarray[];

现在必须编写:

array ^ myarray;

第二个 ^ 首先引起我的注意。如果不考虑它,您将这样一条消息"error C3149: cannot use this type here without a top-level '^'",它看起来有些神秘(顶级的什么?),但当您了解规则后,就会发现它的重大意义。我不确信为什么需要顶级 ^(顶部 ^ 吗?),这是因为编译器知道该数组由"array"关键字托管 - 但我确信这些语言特征肯定有其原因,此外它肯定具有某种意义。托管内容通常具有 ^。对于托管的主要类型数组,您也需要顶部 ^ 吗?例如:

// managed array of ints
array^ foo;

使用 Length(而非 Count)获取数组长度。

如果您需要查看托管 NULL 指针,请使用 nullptr 而非 NULL。

模板可以更好地使用托管类。这是使用 C++/CLI 的最重要原因之一。由于每个托管和本机类都有它们自己的私有语法(而不是共享重载的 *),因此模板可以轻松区分它们。

还有更多我没有提到的语法更改。您自己会发现它们。关于概述,请参阅 Stan Lippman 的"Hello C++/CLI",本文出自我们 MSDN Magazine 的 Visual Studio 2005 专刊(请参阅 MSDN.microsoft.com/MSDNmag/issues/06/00/PureC)。我也推荐 Stan 的文章"A Baker's Dozen: Thirteen Things You Should Know Before Porting Your Visual C++ .NET Programs to Visual Studio 2005",请参阅(MSDN.microsoft.com/library/en-us/dnvs05/html/BakerDozen.asp)。

不要对新语法感到奇怪。一旦我将 * 更改为 ^,并将 /clr:oldSyntax 更改为 /clr,剩下的就是修复编译器提出的每个错误。当 ManWrap 获取该编译器的允许之后,它可以独立运行。Redmondtonians 应该以此为荣 - 每次编译器都可以确保正确性,这就是胜利。顺利地转换为 ^ 让我兴奋不已,我想将 GCHANDLE 重命名为 MANHANDLE。

嗯。

两个较小的注意事项

总体而言,从 Visual Studio .NET 2003 迁移到 Visual Studio 2005 十分省力。对于 IDE,我没什么可说的,因为我是一个彻头彻尾的文本黑客,我不考虑使用少于 Emacs 的任何内容编辑我的代码。我说过,对于 Visual Studio 2005 有两个较小的注意事项。第一,它坚持使用我从未用过或需要的各种资料文件夹来填充"My Documents"。通过浏览,我能够找到某些注册表键,以便将除了几个文件夹以外的所有文件夹重新映射到一个眼不见心不烦的 TEMP 目录。我按照自己喜欢的方式精心组织了我的文件夹,因此当强制程序占据我的磁盘时,让我非常苦恼。让我们以此为戒:如果您的应用程序需要文件夹,请确保让用户决定他们要访问的位置。

我的另一个抱怨是,Visual Studio 2005 不再支持声音架构。唉!我并不非常喜欢吵闹的应用程序,但这里声音是非常有用的。使用 Visual Studio .NET 2003,我会启动一个生成,然后转到另一个窗口中工作。当编译器完成后,我从愉快的吱喳声或爆裂声中可以立即知道我的生成是否成功。使用 Visual Studio 2005,我必须切实读取输出屏幕 - 太烦人了!这里还有另一个原因:绝不要移除功能。

除这些较小的注意事项以及 /NOENTRY 问题之外,升级到 Visual Studio 2005 是比较容易的。如果您尚未进行该转换,赶快进行吧 - 特别是当您要编写混合的/托管程序集时。新语法更好。至于任何更改,它只进行了较小的调整。但是当您使用 ^ 时,它们就变得非常好用了。

您可以从 MSDN Magazine Web 站点下载最新的 ManWrap。该下载由以下三个完整的版本组成:Visual Studio .NET 2003 的一个原始版本,以及两个使用以前的语法以及 C++/CLI 语法的 Visual Studio 2005 新版本。尽情享受编程的乐趣吧!

Paul DiLascia 是一个自由职业的软件咨询师和 Web/UI 设计专家。他是 Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992) 的作者。闲暇时间,Paul 开发了 PixieLib,它是一个 MFC 类库,位于他的 Web 站点 www.dilascia.com。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值