MSDN Mag 2006.06.
原文出处:The .NET Wrap
翻译:Steven Xiong
翻译时间:2006年5月29日
其他MSDN文章翻译
MSDN2006年6月刊 通过C++ Interop把Windows窗体集成到MFC应用程序中
MSDN2006年5月刊 压力测试
NETTING C++
.NET包装(The .NET Wrap)
下载这篇文章的代码:NettingC2006_06.exe(165KB)
本月,我们把专栏名称由Pure C++改为Netting C++,以便更好地反映我们把注意力集中在使用C++/CLI进行Microsoft®.NET编程上。C++/CLI是Visual C++®的.NET扩展,它在Visual Studio® 2005中得到了支持。在第一个Netting C++专栏和一系列后续的文章中,我将使用Visual C++ 2005的C++/CLI语言扩展,通过一系列逐步的变换,把一个可运行的本机C++程序迁移到.NET中。程序是处理自然语言文本的文本查询语言(TQL),它是在1996年为我的C++ Primer第三版而首次开发的。它使用标准模板库(STL)和串操作,很好地验证了面向对象设计(OOD)。它应当是深入基于.NET编程的好工具。
假想的场景是:我的公司有一个运营中的本机C++应用程序,它是商业的基础,TQL。我们公司认为,它需要被扩展成Web-sensitive的,并提供对XML的支持。而且,它现在还是一个控制台应用程序(它最初是一个UNIX命令行应用程序),我们想把它改造成为具有先进的GUI界面,并为它增加用户提出的更多功能。
然而,在做所有这些很棒的东西之前,程序必须被变换成.NET程序。因为这个程序是可工作的,而且一些报告证明它工作得很好,所以我并不想改动它,至少当从改动中不能获取利益时,我不会改动它。所以,netting TQL的第一步就是把它编译成为一个.NET可执行程序。第一部分,“把本机代码编译成.NET”,就着眼于此。
尽管我描述的过程使TQL能在.NET下运行,但是它并不使用任何.NET运行时服务,也不提供与其他.NET兼容语言的互操作性。我将在第二部分“包装本机代码,在.NET下发布”中,讲述使用C++/CLI代理类包装TQL,为增加和替代现存的TQL部分程序提供必要的施展空间。
后续的专栏将关注如何把本机类型映射成由通用类型系统(CTS)支持的.NET类型,以及在程序的变换过程中,不断地检查其性能特性。我们还将查看运行时的可用类型信息,使用ildasm命令探究所有基于.NET语言编译成的通用中间语言(CIL)。
所有这些做完以后,我们将探究多线程,Web服务,与一个C# ASP.NET前端的跨语言互用性(cross-language interoperability),XML支持,以及与Windows Vista™的集成。所以,我们有活干了。(注意 ,本专栏的所有开发工作是通过Visual C++ IDE完成的。因此,我并没有直接使用编译器开关,但是我会在项目上做特别说明。)
把本机代码编译成.NET
我们的终极目标是把本机代码编译成一个.NET可执行程序。我从新建项目(New Project)向导中选中“Visual C++ | CLR | CLR控制台程序”作为开始,生成以下代码:
// 项目向导生成的代码
#include "stdafx.h"
using namespace System;
int main()
{
Console::WriteLine(L"Hello World");
return 0;
}
(我把它稍作了简化,移除了存储命令行参数的数组,因为我还不准备使用它。)
TQL程序有三个部分:
1.Query类层次(class hierarchy)支持用户指定的布尔查询,例如与(&&),或(||)和非(!),而不只是用户指定的文本。(图1展示了一个用户查询会话(session)的例子。)
2.UserQuery类处理交互的用户文本查询会话的所有方面。
3.TextQuery类处理实际的文本,从自文件中读取到生成一个内部的规范化结构,用于显示搜索结果。
每一个部分分别对应一组文件:Query.h和Query.cpp;UserQuery.h和UserQuery.cpp;以及TextQuery.h和TextQuery.cpp。这是六个本机C++文件,我将把它们导入项目中,理想地,我不需要对代码做重大的改变就可以编译。至少,这是C++/CLI扩展背后的主要想法之一。
当然,一种策略是一次导入所有这些文件,点击“生成”,然后看发生的事情。尽管在少量文件的情况下,这种方式也许可行,但是通常来说这会产生大量的警告和错误。我宁愿逐步地增加文件,一次一个。而且我更愿意首先编译头文件,因为通常你需要通过包含语句才能工作。除此之外,编译头文件通常比编译程序文件更为容易。
因此,我使用的过程就是这样,它基本上是完全“无痛的”,除了比较符号的(signed)和无符号的(unsigned)整数带来的两个编译器警告以外。
// 从TQL中增加存在的头文件,一次一个
// 1.没有编译错误...
#include "Query.h"
// 2.两个警告:符号/无符号比较
#include "TextQuery.h"
// 3.再一次,两个警告:符号/无符号比较
#include "UserQuery.h"
警告是从这一行程序中产生的:
if ( _paren < _current_op.size() )
这里,_current_op是一个STL栈类,函数size()返回一个无符号整数,而_paren是一个int型的计数器,它表示如"alice | ( hair & ( magical | fiery) )"的用户查询中的括号个数。编译器给出了正确的劝告:
警告C4018 : '<' : 符号/无符号不匹配
然而,从程序本身来看,_paren既不是负数,也不至于大到使比较无效,所以在这个比较中没有语义危险(semantic danger),只是可能有把int提升为unsigned的性能代价。当我们关注性能问题时,我们会确定这一情况是否真的会产生。现在,我们保留这些警告,我们的想法只是转变代码。
一旦所有的头文件编译通过以后,我一次一个地增加程序文件。产生的唯一错误是由于我没有增加预编译头支持,例如:
// 4. 增加Query.cpp
// compiler message:
// fatal error C1010: unexpected end of file while looking for
// precompiled header. Did you forget to add '#include "stdafx.h"' to your source?
事实上,这正是我所做的事情,不只是Query.cpp,TextQuery也一样:
// 5. 增加TextQuery.cpp
// a. same fatal error C1010 ... and same solution ...
// b. compiled fine ...
令人高兴的是,到处理UserQuery.cpp时,我已经被训练出来了:
// 6. 增加UserQuery.cpp和#include "stdafx.h"
要是移植有如此简单就好了!Visual C++团队做出了一个设计决定,使得引入到语言中的新关键字的潜在影响最小化,让关键字是上下文的(contextual),而不是保留关键字。只有在特定的程序上下文中,一个上下文关键字(contextual keyword)才有特定的意义。例如,在普通的程序中,sealed被当作一个普通的标识符。然而,当sealed出现在类或虚函数的声明部分时,只有在声明的上下文中时,它才被当作是一个关键字。
spaced keyword是一种特殊的上下文关键字。从字面上来说,它是由一个现存的关键字和一个上下文修饰符配对,由空格符进行分隔。一对关键字,例如“ref class”,被当作一个独立的整体,而不是两个分开的关键字。例如:
ref class Foo // spaced contextual keyword
{ ... } ^ref; // non-contextual use as identifier
在这种场合,第一个ref表明Foo是.NET引用类(reference class)的类型。第二个ref是Foo对象的标识符。帽子符号(^)表明对象类型是.NET引用类型。
既然源代码编译完成了,下一步是用程序的调用来替代刻板的Hello, World问候语,如下所示:
#include "stdafx.h"
#include "Query.h"
#include "TextQuery.h"
#include "UserQuery.h"
using namespace System;
int main()
{
TextQuery tq;
tq.build_up_text();
tq.query_text();
return 0;
}
编译和执行这个程序,它工作得很好,这相当cool。图1中的代码展示了稍做清理后的会话,红色代表用户的输入。
Visual C++的这种把本机代码转换为.NET,而不需要对源代码做重大修改的能力,既是2001年随Visual Studio .NET发布最初的托管扩展(Managed Extensions)的主要需求,也是Visual Studio 2005中替代托管扩展的C++/CLI扩展的主要需求。
包装本机代码,在.NET下发布
现在,程序已经可以在.NET下运行了,但是它的类型层次(type hierarchy)对运行时和其他语言来说都不透明,因为最初的源代码并不是在CTS中构建的。所以,下一步就是把一些托管代码集成到程序中。
我想做的是把TQL向.NET开放,但是我不想在程序员资源和时间上花费太多。首选的策略是把本机程序打包到CTS引用类的类型中。在TQL的案例中,这意味着写一个包含TextQuery对象的引用类。
所以,第一个问题是,如何在一个引用类类型中声明一个本机类对象的成员变量?答案是,必须非常小心!一个本机类的实际对象并不能直接存储在一个引用类类型中,你只能存储一个指向本机类对象的指针。所以,最初的类如下所示:
#include "TextQuery.h"
ref class TQL {
private:
// 本机对象,通过它来调用我们的程序
TextQuery *pTQuery;
};
TextQuery指针的成员变量,是本机C++的,最好随着构造函数进行初始化;同样地,在析构中释放,如下所示:
ref class TQL {
public:
// 构造函数在本机堆上创建一个TextQuery对象
// 析构函数释放对象
TQL() { pTQuery = new TextQuery; }
~TQL() { delete pTQuery; }
};
这种策略——在初始化时请求资源,在析构中释放资源——对ISO-C++程序员来说很熟悉。尽管由于.NET托管堆处理垃圾收集(garbage collection)的方式,事情会稍微复杂一些。我将下一个专栏探讨这一问题。
到目前为止,类只涉及到在托管引用类中管理本机类对象的生命期。本机应用程序现在需要的是在.NET中以某种方式公开自己的接口。开始,我只提供对TQL用户可用的公共操作(public operations)的一一映射。为了使事情简单化,只包括了两个主要的函数:
ref class TQL {
public:
// 我们希望在.NET下发布的operations
// 这些通常会被编译器内联(inline)
void build_up_text(){ pTQuery->build_up_text(); }
void query_text() { pTQuery->query_text(); }
};
在实践中,一一映射可能会太昂贵,因为每一次调用代表了.NET和本机代码间的边界跨越(boundary crossing)。但是再重复一次,我们的目标只是让代码可运行。
TQL类的定义位于名为TQL.h的头文件中,它将替代include文件列表中的TextQuery.h。现在,我将修改main函数,让它使用TQL类而不是本机TextQuery类,如下:
// 让我们的托管TQL包装者运行起来。。。
int main()
{
TQL ^tq = gcnew TQL;
tq->build_up_text();
tq->query_text();
return 0;
}
请注意gcnew关键字。当你想从.NET托管堆中分配对象时,你应当在C++/CLI中使用gcnew(但是,在本机堆中分配非CTS类型时,继续使用new)。声明
TQL ^tq
定义tq为一个指向TQL引用类对象的追踪句柄(tracking handle)。“追踪”这个词强调了引用类型位于托管堆中,因此在垃圾收集堆压缩过程中可以无错误地移动位置。追踪句柄在程序执行过程中,由运行时(runtime)自动更新。
注意,追踪句柄在调用成员函数或存取类数据成员时,使用箭头(->)成员选择操作符(member selection operator),而不是点(.)成员选择操作符。编译并运行这个程序,运行结果和第一部分描述的完全一样。
这就是包装!我拿了一个10年前的古老的本机程序,并轻松地把它移植到了.NET中。这是好的消息。不那么好的消息是,这种实现还存在一些细节的问题。我将在下一专栏中进行详细的说明。
Send your questions and comments for
Stanley B. Lippman began working on C++ with its inventor, Bjarne Stroustrup, in 1984 at Bell Laboratories. Later, Stan worked in feature animation both at Disney and DreamWorks and served as a Software Technical Director on Fantasia 2000. He has since served as Distinguished Consultant with JPL, and an Architect with the Visual C++ team at Microsoft.
please enter file name: alice_emma
Enter a query -- please separate each item by a space.
Terminate query (or session) with a dot( . ).
==> fiery .
fiery ( 1 ) lines match.
Location(s): [ { 3, 3 } { 3, 9 } ]
Requested query: fiery
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
Enter a query -- please separate each item by a space.
Terminate query (or session) with a dot( . ).
==> beautiful && fiery .
beautiful ( 1 ) lines match.
Location(s): [ { 3, 8 } ]
fiery ( 1 ) lines match.
Location(s): [ { 3, 3 } { 3, 9 } ]
beautiful && fiery ( 1 ) lines match.
Location(s): [ { 3, 8 } { 3, 9 } ]
Requested query: beautiful && fiery
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
Enter a query -- please separate each item by a space.
Terminate query (or session) with a dot( . ).
==> fiery || magical .
fiery ( 1 ) lines match.
Location(s): [ { 3, 3 } { 3, 9 } ]
magical ( 1 ) lines match.
Location(s): [ { 4, 1 } ]
fiery || magical ( 2 ) lines match.
Location(s): [ { 3, 3 } { 3, 9 } { 4, 1 } ]
Requested query: fiery || magical
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
( 4 ) magical but untamed. "Daddy, shush, there is no such creature,"