您的位置:首页 > 编程语言 > C语言/C++

语言增强功能和简化的 GUI 开发丰富了 C++ 应用程序

2005-03-23 04:56 363 查看
Richard Grimes

本文假设您熟悉 C++

下载本文的代码: VisualC++NET.exe (61KB)

摘要

C++ 的托管扩展是开发 Windows 服务的首选编程语言。Visual Studio .NET 2003 为设计器引进了 C++ 支持,并提供了可供其他语言开发窗体、控件、组件和数据集的全部 RAD 功能。而且,已经为用 C++ 创建可验证的程序集添加了支持。

在本文中,作者回顾了这些新增功能以及新的编译器和链接器开关,还说明了 C++ 如何在成为功能强大的 .NET GUI 开发工具的同时,保持为首要的系统语言。



本页内容



设计器



引用



Window 窗体设计器和附属程序集



本地化非窗体资源



Web 引用



可验证的库程序集



新的编译器开关



新的链接器开关



小结
在开始使用 Microsoft®.NET Framework 时,C++ 小组面临着所有语言小组面临的最困难的任务。与历史一片空白的 C# 小组不同,C++ 小组拥有功能丰富的成熟语言 — 一些功能有用,一些功能无用。他们的工作非常明确:确保作为本机代码编译和运行的所有代码都还将作为中间语言 (IL) 编译和运行。尽管这听起来很简单,但实际上这涉及到许多工作,其结果是 It Just Works (IJW) — 一个名副其实的伟大功能。但是,IJW 工作对 Visual C++® 有副作用:只是没有足够的时间来添加其他符合 Microsoft .NET 的语言中存在的华丽的特性。

使用 Visual Studio®.NET 2003 发行版,Visual C++ 小组有机会添加一些失去的优质功能以及其他新功能。托管的 C++ 始终是面向公共语言运行库 (CLR) 的一流语言,但是,它现在具有面向 CLR 的其他语言的 RAD 功能,这会让您方便地使用 C++ 的设计器和原始功能。

设计器

C++ 是执行如下操作的可供选择的语言:为效率优先的 Windows®服务(以前称作 Windows NT ®服务)、COM+ 组件和库代码开发快速高效的代码。但是,相当一部分 C++ 开发人员也使用该语言,通过直接写入 Win32?API 或者使用 MFC 来创建 GUI 应用程序。有了 1.0 版的 .NET Framework,您就可以使用 C++ 编写托管的 GUI 应用程序,但是在先前版本的 Visual Studio .NET 中没有裁掉的一个功能就是对设计器的支持。这样做的结果是,尽管在 C# 和 Visual Basic® .NET 中工作的开发人员可以使用 RAD 功能从工具箱或服务器资源管理器拖放组件和控件,但是 C++ 开发人员必须手动完成所有这些工作。

这在 Visual Studio .NET 2003 中已经改变,Visual Studio .NET 2003 现在支持 Windows 窗体设计器、控件设计器、组件设计器和 XML 架构(数据集)设计器。设计器允许您通过以 WYSIWYG 方式拖放窗体和复合控件来创建它们。复合控件作为由 CodeDOM 生成的代码序列化到您的项目中。Visual Studio .NET 2003 有一个面向 C++ 的 CodeDOM(位于 MCppCodeDomProvider 和 MCppCodeDomParser 程序集中),这意味着设计器可以分析和生成 C++。图 1 显示了一个 C++ Windows 窗体项目,在该项目中,窗体通过使用 Windows 窗体设计器进行编辑。



图 1 用 Windows 窗体设计器进行编辑

在第一版的 Visual Studio .NET 中,所编写的 C++ 设计器符合由 C# 和 Visual Basic .NET 使用的设计器体系结构;在这些语言中,在单个文件中实现了一个类。因此,尽管 C++ 项目向导会生成一个 .cpp 文件,它也会将类代码放在头文件中。当设计器为事件处理程序生成新方法时,它会将实现放在头文件中。但是,作为 C++ 开发人员,您习惯于将实现与接口分离。您可以从头文件中删除对方法的实现,并将它们放在相关的 .cpp 文件中。在使用 InitializeComponent 方法时,最有可能这样做。正如对于 C# 一样,使用该方法,可以针对您通过设计器添加的组件执行初始化。如果您将该实现移到 .cpp 文件中,设计器将通过继续将初始化代码放在方法体中来识别该移动。



返回页首

引用

工具箱包含可拖到设计器表面的控件和组件。这些组件将在许多不同的程序集中实现,它们甚至可以是 COM 组件。当您将某个组件拖到设计器上时,设计器将更改项目的设置,使编译器能够访问该组件的程序集(或类型库)的类型信息,并使最后的输出程序集能够在运行时访问该程序集(或 COM 服务器)。

这是通过 References 文件夹实现的,您可以在解决方案资源管理器中查看该文件夹。托管的 C++ 通过 #using 语句访问程序集的元数据。C++ 小组决定对于放到设计器上的组件不使用 #using,而是选择使用等效命令行 /FU。/FU 开关可在编译器命令行上使用多次,并为编译器提供指向将使用的每个元数据文件的路径。References 文件夹中的所有项都将在项目的默认编译器命令行中添加 /FU 开关。References 文件夹可以包含位于当前解决方案下项目中的程序集、硬盘上另一个位置中的程序集、列在“添加引用”对话框的“.NET”选项卡上的标准程序集以及类型库。其中的每个程序集都需要不同的处理。

在默认情况下,对于解决方案中的每个项目,Visual Studio .NET 2003 会将配置的输出文件夹设置为同一个文件夹。这意味着,在运行时,所有的项目输出都将位于同一个文件夹中,而且将不存在程序集查找问题。如果您从另一个位置选择某个程序集,“添加引用”对话框会将该程序集复制到当前配置的输出文件夹中(当您为另一个配置构建解决方案时,此过程将重复执行),以便该程序集与使用它的程序集位于同一个文件夹中。在这两种情况下,用于 /FU 的路径将是输出文件夹中程序集的路径。

“添加引用”对话框还将列出标准程序集。这些通常是可在全局程序集缓存 (GAC) 中找到的框架程序集。IDE 实际上会在注册表中的以下位置查找注册表项:

HKLM\Software\Microsoft\VisualStudio\7.1\AssemblyFolders


对于企业级结构设计版,使用 7.1Exp(而非 7.1)。

每个子项的默认值都是元数据文件的位置。对于通过该选项卡添加的程序集,其 /FU 项的路径将在 AssemblyFolders 下的项中给出。框架程序集通常安装到全局程序集缓存中,并且通常作为本机映像与 ngen 存储在一起。但是,在 %systemroot%\Microsoft.NET 下的 current framework 文件夹中将存在这些程序集的其他副本,这允许您访问它的元数据。如果您向 References 文件夹中添加这样的程序集,那么,将在 /FU 中使用 %systemroot%\Microsoft.NET 下副本的路径。

类型库会带来另一个问题。不能向 /FU 开关传递类型库;相反,“添加引用”对话框将使用 interop 程序集。例如,如果您添加指向 Microsoft.mshtml 类型库的引用,那么,/FU 的路径将是:

C:\Winnt\assembly\Gac\Microsoft.mshtml\7.0.3300_b03f5F7f11d50a3a
\\Microsoft.mshtml.dll


这是在安装 Visual Studio 时用 tlbimp 生成、由 Microsoft 签名并插入到 GAC 中的主要的 interop 程序集。如果您添加自己的 COM 服务器,该对话框将使用 tlbimp 创建 interop 程序集,该程序集将复制到项目配置的输出文件夹中。这意味着,即使 C++ 可以通过 IJW 访问 COM 服务器,也将通过 COM interop 执行实际的访问。

请注意,模块 (.netmodule) 和 .obj 文件中的类型可以通过 #using 来访问。对于模块来说,这与使用 linker /addmodule 开关相同;对于 .obj 文件来说,这与向链接器命令行中添加 .obj 文件相同。但是,“添加引用”对话框不允许您添加模块或 .obj 文件,因此,它将只适用于程序集。



返回页首

Window 窗体设计器和附属程序集

在第一版的 Visual Studio 中,Windows 窗体设计器总是最明显的设计器。托管的 C++ 失去了 .NET Framework 的这个首波工具,但是,在很大程度上,这不是个问题,因为所有的控件及其事件都充分记录在 MSDN® Library 中。这意味着,您可以在没有设计器的情况下开发窗体和控件,尽管它不如进程那样快。

Windows 窗体设计器非常适用于开发附属程序集。与使用附属程序集的程序集(只具有与区域设置无关的资源)相反,附属程序集包含本地化资源。附属程序集是库程序集,它提取中性程序集的简短名称,并附加 .resources。附属程序集名称的区域性部分定义了它为其保存资源的区域性的名称,而且,由于 Windows 文件系统只识别程序集的简短名称,因此,每个附属程序集都应当存储在带有区域性名称的子文件夹中。在运行时,窗体代码使用 ResourceManager 对象,该对象会搜索和加载适用于当前线程的 CurrentUICulture 设置的附属程序集,并访问与窗体相关联的资源。

Windows 窗体设计器允许您使用附属程序集来开发本地化窗体。每个窗体都有一个 Language 属性,当您更改它的值时,设计器将加载与指定区域性相关联的 .resx 文件。.resx 文件是包含本地化资源的 XML 文件,任何二进制资源都先进行序列化,然后存储在该文件中。这意味着,如果二进制资源(最有可能是图标)发生更改,那么,必须通过“属性”窗口添加资源,以便再次序列化它们。您将只需更改所支持的每个区域性的窗体属性,即可开发自己的附属程序集;IDE 在生成时为您执行所有的繁杂工作。

生成进程在幕后使用 resgen 编译每个 .resx 文件,并按 .resx 文件命名编译的资源文件(即,区域性是编译资源名称的一部分)。附属程序集是使用程序集链接器工具 (al.exe) 创建的,可向 al.exe 传递编译资源文件,以及用于窗体 Language 属性的区域性。不幸的是,您无法对生成过程进行太多控制;您唯一可以更改的生成属性就是通过 .resx 文件的属性页更改编译资源文件的名称。

的确,该名称会在由“新建项目”向导生成的代码中有所暗示,它会警告您,如果您(从 Form1)更改类名,您将负责编辑资源的文件名属性。我的解决方案就是,从项目中删除 Form1.h 文件,并使用“添加新项”向导创建名称更有意义的窗体(完整的说明位于可从本文开头链接处下载的代码中)。我喜欢为项目中的第一个窗体创建一个更具创意的名称(可能会基于项目名称)。将第一个窗体称为 Form1 只会增加更多的工作,而这些工作不是向导应当完成的。



返回页首

本地化非窗体资源

一些资源与窗体无关。例如,如果您决定遵循 Microsoft 建议并使用 EventLog 类将消息记录到 Windows NT 事件日志中,那么,您将负责本地化它所记录的消息。相反,我建议您通过平台调用或 IJW 来使用 ReportEvent。这是由于在设计上,事件日志会在消息被读取时执行本地化(当读取器的区域设置是已知时),但是,在设计上,EventLog 类在消息被记录且消息的读取器未知时执行本地化。

要创建本地化资源,您应当使用 name.culture.resx 这一命名方案,其中,name 是传递到 ResourceManager 的构造函数中的资源的名称,culture 是本地化资源的区域性。对于英国英语,将使用 en-GB;对于中性资源,将忽略这一部分名称。生成过程将假设,名称中具有区域性的任何 .resx 文件将用于生成附属程序集,任何没有区域性的资源将作为中性资源嵌入。但是,在编写(发行候选版 1)时,由于您不能完全控制用于创建资源的命令行,会产生两个问题。

第一个问题很小。当“添加新项”向导添加 .resx 文件时,它会为编译资源文件的名称提供以下内容,其中,culture i是已使用的区域性标识符:

$(IntDir)/$(RootNamespace).$(SafeParentName).<culture>.resources


第二个问题在于使用 Visual Studio .NET 2003 中新增的两个宏($(RootNamespace) 和 $(SafeParentName))。$(RootNamespace) 将是在其中定义窗体的命名空间(对于非窗体资源,使用项目的名称)。$(SafeParentName) 由 MSDN Library 描述为直接父的名称 — 通常是窗体名称。但是,对于通过“添加新项”向导添加的文件,该宏将是静态字符串“ResourceFiles”。因此,传递到 ResourceManager 的构造函数中的名称应当是二者的组合。例如:

// project called Test
ResourceManager __gc* rm;
rm = __gc new ResourceManager(
S"Test.ResourceFiles", Assembly::GetExecutingAssembly());


解决方案就是,编辑 Resource File Name(资源文件名)属性并删除 $(SafeParentName) 宏。而且,由于资源名称总是被假设为根命名空间,因此,如果在同一个区域性中有多个资源,它们都将编译为同一个文件名 — 每次新编译都改写最后一个文件。



返回页首

Web 引用

References 文件夹上下文菜单还为您提供了添加 Web 引用的选项,但奇怪的是,当您添加引用时,它并不实际添加到 References 文件夹中。相反,每个 Web 引用都将在解决方案资源管理器中的顶层获得一个节点。该节点中的项是一个 .discomap,其中包含有关可用于 Web 服务以及 .disco 和 .wsdl 文件的发现文档的信息。

针对 .discomap 文件执行 wsdl.exe 工具,可以为 Web 服务生成 Web 代理。在 Visual Studio .NET 2002 中,也会执行相似的过程,但是,会要求使用 Web 服务描述语言 (WSDL) 来创建 C# 文件,该文件在生成时编译为 .netmodule 并链接到程序集。正如我以前提到的那样,Visual Studio .NET 2003 有一个 C++ CodeDOM,这会通过 wsdl.exe 工具的 /language 开关传递到该工具,以便 wsdl.exe 创建托管的 C++ 代理。有意思的是,生成过程会将该代码编译为程序集,并向它赋予与所生成的代理源文件相同的名称。因此,如果您是在本地计算机上访问 Web 服务,那么,在解决方案资源管理器中生成的节点将被称作 localhost,代理源文件将被称作 localhost.h,代理程序集将被称作 localhost.h.dll。您可以更改节点的名称,但是不能直接访问所生成的代理程序集的名称,因此 .h 将总是以它的简短名称出现。



返回页首

可验证的库程序集

第一版的 Visual Studio .NET 的一个限制性方面就是它缺乏对 C++ 程序集的验证能力。当 CLR 加载某个程序集时,它执行某些测试,以检查该程序集是否未被按照某种会对计算机产生不良影响的方式破坏或修改。该过程的一部分涉及到对代码的分析,该分析确保它具有有效的 IL,还确保在每个操作码被调用之前正确设置了堆栈。在该过程中,还执行某些检查,以确保该代码有效,这与确保跳转只在方法内部执行一样。但是,某些有效的代码可能会执行不安全的操作,因此只能在受信任的上下文中执行。这些较为复杂的检查称为验证。

使库变得可验证涉及到几个步骤;最重要的步骤涉及到编写不会验证失败的代码。因此,您不能使用 __nogc 类型或非托管的指针。在代码中使用内部指针应当谨慎,这是因为,尽管您可以分配和取消引用它们,但是在针对它们执行算法时,该代码将无法被验证。异常必须是托管对象,因此您不能引发基元类型。

当然,可验证的程序集不应当使用任何类型的非托管代码。这意味着,尽管您可以使用平台调用,也不应当通过静态库引入代码。这还会影响对 CRT 的使用,因此您必须使用链接器开关 /noentry。还需要谨慎执行转换:不能使用 static_cast<> 在类类型之间向下转换,而且决不要使用 reinterpret_cast<>,因为它将生成无法验证的代码。诸如 like __asm、__try 和 __except 的关键字与非托管代码一起使用,因此,是不允许使用它们的,而非托管的 #pragma 指令显然也是不允许使用的。

C++ 编译器可以优化将在“发布”配置中打开的代码。但实际上,优化器生成的其他可验证代码将无法完成验证过程,因此,在所有的配置中,您应当用 /Od 关闭优化。验证器与可移植可执行文件 (PE) 中的空数据部分不同,因此,您应当在某个源文件中添加全局变量,以便在初始化的数据部分中获得如下内容:

extern "C" int _dummy = 1;


编译器向名为 _check_commonlanguageruntime_version 的内部 C 运行库 (CRT) 函数添加一个调用,以检查它是否为 1.1 版的 CLR。但是,由于您已经通过使用 /noentry 链接器开关删除了对 CRT 的支持,因此,链接器将会抱怨它找不到 _main 符号,从而抱怨无法访问 CRT。解决此问题的方法是,使用 Visual C++ .NET 库文件夹中提供的 nochkclr.obj 文件进行链接,或者使用下一节中介绍的 /clr:initialAppDomain 进行编译。

当然,可验证程序集的目的在于可对它进行验证!在默认情况下,C++ 编译器将在您的代码中添加 [SecurityPermissionAttribute],以便通知运行库忽略验证,因此,您必须显式通知运行库用程序集级的属性来执行操作:

[assembly: SecurityPermissionAttribute(
SecurityAction::RequestMinimum,
SkipVerification=false)];


最后一步是对程序集进行标记,以指出它只包含 IL。为此,您必须修改 PE 文件的 CLR 头,并添加 COMIMAGE_FLAGS_ILONLY 标志。IDE 中没有完成此操作的机制,但是,Visual C++ .NET 为名为 silo.exe 的工具(位于 SetILOnly 项目中)提供了源代码。



返回页首

新的编译器开关

编译器有一个新的开关:/clr:initialAppDomain。为了了解此开关的目的,请考虑图 2 中的代码以及下面的代码片断:

// Compile with /LD but without /clr so an unmanaged DLL is created
typedef void (*FUNC) (void);
extern "C" __declspec(dllexport)
void ExternalFunc(FUNC f)
{
f();
}


ExternalFunc 函数在本机 DLL 中实现,它用于模拟以下情形:在将函数指针传递到程序集中的托管代码时调用本机函数。ExternalFunc 只是调用函数指针,函数指针随后将打印出应用程序域的名称。因此,在 Proc 方法中,先进行托管到本机的转换,后进行本机到托管的转换。如果您在主函数中创建 ADClass 的实例,然后调用 Proc,则应当会获得默认应用程序域的名称,该名称与过程名完全相同。

现在,让我们考虑图 2 中所示的主函数。这会在调用 Proc 之前,创建新的应用程序域,并在这个新域中创建 ADClass 的实例。在逻辑上,该代码应当在控制台上打印“new domain”。但是,第一版的 .NET Framework 存在一个问题:在调用本机代码时,thunk 代码忘记该调用源自哪个应用程序域。这意味着,当本机代码回调到托管代码时,运行库不知道要使用哪个应用程序域。在第一版的 Framework 中,运行库是一致的,并确保到托管代码的回调将转到第一个应用程序域。因此,如果您使用 1.0 版的 Framework 编译和调用该代码,您将发现在控制台上打印出第一个应用程序域的名称(过程的名称)。

为了允许本机代码调用托管代码,编译器创建一个 vtable。在编译时,这是用相应函数的元数据标记进行填充的。在运行时,CLR 执行链接地址信息并将读取 vtable 中的值,从而确定所标识方法的地址。CLR 随后将该地址放到 vtable 槽中。PE 文件在编译时需要一个实际地址,因此,编译器会创建一个简单的 thunk,该 thunk 会跳转到 vtable 中的值。当您调用 ExternalFunc 时,编译器将该 thunk 的地址传递到非托管函数。

对于 1.1 版的 .NET Framework,vtable 包含一个标志,该标志向运行库指出,如果该调用从本机代码进入程序集,所使用的应用程序域将是用于调用本机代码的最后一个应用程序域。如果您用 ILDasm 查看程序集,将会发现,有一个针对名为 .vtfixup 的 vtable 执行链接地址信息的指令。对于新行为,.vtfixup 指令将具有标志 retainappdomain,在使用 /clr 开关时,这是默认标志。

不幸的是,该标志对于 1.0 版的 CLR 无效。如果您希望对 1.0 版上运行的代码进行编译,则必须依赖于 /clr:initialAppDomain,该开关在 .vtfixup 指令上使用 fromunmanaged 标志。在 Visual Studio .NET 中,对于托管项目没有可允许您使用该开关的属性。但是,如果您在一个命令行上同时使用 /clr 和 /clr:initialAppDomain,那么,编译器将使用后者。因此,如果您用 IDE 进行编译,则可以将 /clr:initialAppDomain 放到“C/C++ 命令行项目”属性页上的“附加选项”编辑框中。



返回页首

新的链接器开关

共有五个新的链接器开关,这些开关分为三大组:资源、程序集签名和调试信息。在这些开关中,只有一个开关 /assemblydebug 可通过 IDE 使用。如果您希望使用其他开关,将必须使用“链接器命令行项目”属性页。

正如名称所暗示的那样,/assemblylinkresource 开关允许您提供将链接到程序集的资源文件的名称。默认文件是嵌入的资源文件,以便它们成为程序集的 PE 文件的一部分。当资源文件是随其他程序集文件部署的独立文件时,将使用链接。如果您希望链接编译过的 .resx 资源文件,不要允许 IDE 生成 .resx。相反,应当将该文件从生成中排除,添加单独的自定义预链接生成步骤,也使用“链接器命令行项目”属性页来指定 /assemblylinkresource 开关。

/keyfile 和 /keycontainer 开关允许您指定文件名或者包含对程序集进行签名的密钥的加密容器。这些开关允许您在链接器命令行上提供这些信息,而不允许您通过源文件中的自定义属性来提供。的确,这些自定义属性只是传递到链接器的指令,因此,新开关不代表新行为。同样,/delaysign 链接器开关与 [AssemblyDelaySign] 属性等效,并指出最终将对程序集进行签名,但不是现在就签名。

最后一个链接器开关 /assemblydebug 很有趣。该开关可通过 Debuggable Assembly 属性在“链接器调试项目”属性页上使用。该开关与 [Debuggable] 属性等效,编译器添加该属性的目的在于,向运行库指出可对程序集进行调试。通过该属性的值通知运行库跟踪对调试器来说非常重要的信息,并关闭实时 (JIT) 优化。当您在 Visual Studio .NET 2002 中使用 /Zi 开关构建程序集时,[Debuggable] 属性将按如下方式提供:

[assembly: Debuggable(true, true)];


这对于调试配置非常适合,但是,如果您使用 /Zi 开关为“发布”配置生成符号,这意味着运行库将不使用 JIT 优化。在 Visual Studio .NET 2003 中,您必须用自定义属性或通过 /assemblydebug 开关显式指定 [Debuggable] 属性。

/assemblydebug 链接器开关将指定 [Debuggable(true, true)],并且应当用于调试版本。如果您希望为“发布”配置生成符号,则可以在“链接器调试”属性页上更改“可调试程序集”属性,以便显式指定程序集对于调试器是不友好的。例如,使用“No runtime tracking and enable optimizations (/ASSEMBLYDEBUG:DISABLE)”(不跟踪运行库并启用优化 (/ASSEMBLYDEBUG:DISABLE))。



返回页首

小结

Visual Studio .NET 2003 有几个新的托管 C++ 功能,如对设计器的支持、链接的托管资源和 C++ CodeDOM,这些新功能在第一版中不存在。这些功能增强了语言的 RAD 功能,并赶上了面向 CLR 的其他流行语言。对代码生成方式的更新意味着,对多应用程序域程序集的本机调用将处理得更好、“发布”配置程序集可以用符号创建(不影响程序集性能),并且可以创建可验证的程序集。总之,这些改进功能有助于 C++ 维持其作为系统代码最佳编写语言的口碑,同时,还推进了 C++ 迈入 GUI 开发世界的脚步。

有关相关文章,请参阅:

Managed Extensions Bring .NET CLR Support to C++

Tips and Tricks to Bolster Your Managed C++ Code in Visual Studio .NET

有关背景信息,请参阅:

http://msdn.microsoft.com/vstudio

Richard Grimes 编写有关 Microsoft .NET Framework 的文章并在会议上就 Microsoft .NET Framework 进行演讲。Richard 是 Programming with Managed Extensions for Microsoft Visual C++ .NET, 2nd Edition (Microsoft Press, 2003) 的作者,该书已针对 Visual Studio .NET 2003 进行了更新。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: