翻译|其它|编辑:郝浩|2006-03-07 09:37:00.000|阅读 1577 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
下载源代码:WrappersSrc.exe (248KB)
本文讨论:
本文使用下列技术:C++ 和 .NET 框架
C++ 托管扩展使得自由地混合本机代码和托管代码成为可能,即便是在相同的模块中也能如此。是啊!这的确是一件好事情。但是用 /clr
编译可能会带来你不想要的结果。比如强制多线程并屏蔽了一些有用的运行时检查。妨碍 MFC 的 DEBUG_NEW,并且某些 .NET Framework
类有可能与你的名字空间冲突。此外,如果你的应用程序使用的是老版本的编译器,不支持 /clr
开关怎么办?有没有什么方法能不借助于托管扩展而进入框架?答案是肯定的。
在本文中,我将向你展示如何以本机方式包装框架类,以便你能不借助 /clr 而在任何 C++/MFC
应用程序中使用它们。在我的测试案例中,我将在一个DLL中包装.NET框架中的 Regex 类,并实现三个使用该包装类的 MFC 程序。你可以用
RegexWrap.dll 在自己的 C++/MFC 应用程序中添加正则表达式支持,或者用 ManWrap 工具来包装自己喜爱的框架类。
一个简单问题
一切都源于读者 Anirban Gupata 给我提的一个简单问题:有没有可以在其 C++
应用程序中使用的正则表达式库?我的回答是“当然有,而且不止一个,但 .NET 已经具备一个 Regex
类,为什么不用呢?”正则表达式如此有用(参见本文对正则表达式的简介),它们的威力最终会让顽固的 C++ 爱好者渴望.NET 框架。因此我写了一个小程序叫
RegexTest 来说明 Regex 能做些什么。程序运行画面如 Figure 1 所示。你输入一个正则表达式和一个字符串,按下按钮,RegexTest
便会显示 Matchs、Groups 和 Captures 结果。这一切都发生在一个叫做 FormatResults 的单独的函数中(参见 Figure
2),当用户按下 OK 按钮,该函数便格式化一个大的 CString。FormatResults 是 RegexTest
中唯一一个调用框架的函数,所以很容易将它放入用 /clr 编译的宿主模块中。
Figure 1 RegexTest
如果我仅仅是写一个 RegexTest,到这里也就结束了。但编写 RegexTest
的时候我在想:真正的应用程序需要控制其对象较长的时间,而不仅仅是在函数调用期间。假设我想在窗口类中存储我的正则表达式该怎么做呢?想法是不错,但不幸的是,你无法在非托管内存中存储
__gc 指针啊:
class CMyWnd ... {
protected:
Regex* m_regex; // 这里行不通!
};
上面方法行不通,你需要 GCHandle 或者其模板化过的堂兄弟
gcroot: class CMyWnd ... {
protected:
gcroot<Regex*> m_regex; //
swell!
};
GCHandle 和 gcroot 在文档以及相关资料中都有详尽描述(参见 Tomas Restrepo 在 MSDN
杂志2002年二月刊上的文章:“Tips and Tricks to Bolster Your Managed C++ Code”),我在本文中要讨论的是
gcroot 借助了模板和 C++ 操作符重载,使得句柄样子和行为都类似指针。你可以拷贝、赋值和转换;此外,gcroot 的反引用 operator->
使你可以用指针语法来调用你的托管对象:m_regex = new Regex("a+");
Match* m = m_regex->Match("S[aeiou]x");
托管对象、C++和你聚集在一个幸福的家庭里和睦相处,还有什么可在乎的呢?
这种情况下唯一可能的抱怨是使用 gcroot,你需要 /clr,即便编译器知道何为 gcroot/GCHandle
又怎样呢?并不是说你的代码非得要托管;你可以用#pragma非托管产生本机代码。但正像我前面提到的那样,用 /clr
会带来负面影响。它强制多线程(破坏某些函数如: ShellExecute 的正常运行),同时它与类似 /RTC1
这样的选项不兼容,而这些选项产生有用的运行时堆栈和缓冲错误检查(参见 John Robbins 2001年八月刊的 Bugslayer 专栏)。如果你使用
MFC,你可能已经遭遇 /clr 和 DEBUG_NEW 的问题。你还可能碰到名字空间与一些函数如 MessageBox 之间的冲突问题,这些函数存在于
.NET 框架、MFC 和 Windows API 中。
在我一月份的专栏中,我示范了如何创建一个项目,在这个项目中只有一个使用 /clr 的模块。当你的框架调用位于几个函数(如 FormatResults)中,并且这些函数又在单独的文件里时,该项目的运行会很正常,但是,如果你广泛地使用带有
gcroot 成员的类时,则会出现问题,因为太多的模块 #include 你的类。所以如果你轻率地使用 /clr
开关——用不了多长时间——你的整个应用被定向到托管地带。并不是说 /clr
有多可怕,而是很多时候你可能更喜欢呆在本机。能不能让你的框架类和本机代码也共处一室呢?答案是肯定的,但需要一个包装器。
ManWrap
ManWrap
是我建立的一组工具集,专门用来在本机C++类中包装托管对象。思路是创建若干类,这些类在内部使用托管扩展以调用框架,但向外界输出的是纯粹的本机接口。如
Figure 3 所示。
Figure 3 ManWrap
你需要托管扩展建立包装器本身,使用该包装器的应用则需要框架来运行,但该应用本身是以本机方式编译的,不需要 /clr。那么 ManWrap
是如何完成这个壮举的呢?答案是用轻松愉快的心情去包装,然后看看发生了什么。既然每一个.NET对象都派生自 Object,我就从那里开始:
class CMObject {
protected:
gcroot<Object*> m_handle;
};
CMObject 是一个本机 C++ 类,这个类操控着一个托管对象句柄。为了使之有所作为,我需要某些标准的构造函数和操作符,接着,我将包装
Object::ToString,它迟早派得上用场。Figure 4 是我的第一步。CMObject 有三个构造函数:缺省构造、拷贝构造和来自 Object*
的构造。还有一个来自 CMObject 的赋值操作符和一个返回底层 Object 对象的 ThisObject 方法。反引用 operator->
将使用该方法。这些就是包装器类需要具备的最基本的方法。包装器方法本身很简单(本文中是 ToString):
CString CMObject::ToString() const
{ return (*this)->ToString();
}
这里只有一行代码,但所发生的事情比眼见的要多得多:(*this)-> 调用 gcroot 的反引用 operator-> ,它将底层的
GCHandle.Target 强制转换成一个 Object*, 该对象的 ToString 方法返回托管 String。托管扩展和 IJW(It Just
Works)互用机制神奇地将字符串转换为 LPCTSTR,然后编译器用此 LPCTSTR 在堆栈上自动构造一个 CString,因为 CString
本身就有一个用 LPCTSTR 的构造函数。难道 C++ 不是一种真正令人惊奇的语言吗?
到此,CMObject 毫无用处,因为只能用它创建空对象和拷贝它们。这有多大用处呢?但 CMObject
不是设计用来无所事事的;它被设计用来作为更多包装器的基类。让我们来尝试另一个类。框架的 Capture
类是一个非常简单的类,用它来表示正则表达式中一个单一的子表达式匹配。它有三个属性:Index、Value 和
Length。为了包装它,一些显而易见的事情是必须要做的:Capture 派生自Object,所以我要从 CMObject 派生出 CMCapture:
class CMCapture : public CMObject {
// now what?
};
CMCapture 从 CMObject 继承 m_handle,但 m_handle 是 gcroot<Object*>,而非 gcroot<Capture*>。所以,我需要一个新的句柄吗?不要。Capture
从 Object 派生,所以 gcroot<Object*> 句柄也能操控 Capture 对象。
class CMCapture : public CMObject {
public:
// 调用基类构造函数初始化
CMCapture(Capture* c) : CMObject(c) { }
};
CMCapture 需要与 CMObject 完全相同的构造函数和操作符,并且我必须重写 ThisObject 和 operator-> 返回新的类型。
Capture* ThisObject() const
{
return static_cast<Capture*>((Object*)m_handle);
}
static_cast 是安全的,因为我的接口保证底层对象只能是 Capture 对象。包装新的属性也不难。例如:
int CMCapture::Index() const
{
return (*this)->Index;
}
隐藏托管机制
至此一切都很顺利,我已可以用看似笨拙的方法在C++中包装托管对象。但我的C++类仍然需要 /clr
来编译。我的最终目的是建立一个本机包装器以便使用该包装器的应用程序不必再需要 /clr。为了摆脱对 /clr
的需要,我必须向本机客户端隐藏所有托管机制。例如,我必须隐藏 gcroot 句柄本身,因为本机代码不知道
GCHandle 为何物。怎么做呢?
我曾有过一位数学教授,他说过这么一句话:每一个证明要么是一个糟糕的笑话,要么是一个廉价的窍门。显然我要描述的属于后者——廉价的窍门。ManWrap
的关键是特别的预编译符号 _MANAGED,当用 /clr 编译时,其值为 1,否则无定义。_MANAGED 使得隐藏句柄易如反掌:
#ifdef _MANAGED
# define GCHANDLE(T) gcroot<T>
#else
# define GCHANDLE(T) intptr_t
#endif
现在我们可以象下面这样修正 CMObject:
class CMObject {
protected:
GCHANDLE(Object*) m_handle;
...
};
这样用 /clr 编译的模块(即包装器自己)能看到 gcroot<T> 句柄。不用 /clr 的 C++
应用只能看到一个原始整数(有可能是64位)。非常聪明,不是吗?我告诉过你它是一个廉价的窍门来的!如果你奇怪为什么 intptr_t
专门设计用来操作整数,那是因为 gcroot 仅有的一个数据成员,它的 GCHandle 所带的 op_Explicit 负责在整型和 IntPtr
之间来回转换。intptr_t 只不过是 C++ 中 IntPtr 的等价物,所以不管用哪种方式编译 CMObject(本机或托管),在内存中都有相同的大小。
大小是很重要的一件事情,除此之外,还有很多要涉及到本机。至于其它的托管机制,如“使用托管类型签名”的方法(如 Figure 4 所示),我可以用
_MANAGED 来隐藏它们:
#ifdef _MANAGED
// managed-type methods here
#endif
所谓“托管类型方法”指的是其署名使用托管类型。把它们放在 #ifdefs 中使得它们对本机客户端不可见。在本机区域,这些函数不存在。它们类似参数类型为 X
的构造函数,这里 X 是托管的,并且本机代码无法理解和编译 operator->,也用不上它。我只要求这些方法在包装器自己内部——它需要用 /clr 编译。
我隐藏了句柄和所有“托管类型”函数。还有什么别的吗?拷贝构造函数和 operator= 呢?它们的署名使用本机类型,但其实现存取 m_handle:
class CMObject {
public:
CMObject(const CMObject& o) :
m_handle(o.m_handle) { }
};
假设我有一个 CMObject 对象 obj1,并且我这样写:CMObject obj2=obj1。则编译器调用我的拷贝构造函数。这在 m_handle 为
gcroot<Object*> 的托管代码中行得通,但在本机代码中 m_handle 是 intptr_t,所以编译器拷贝原始整数。啊!如果是一个整数你是无法拷贝
GCHandle 的。你必须通过适当的渠道对 CHandle 的 Target 进行重新赋值,或者让 gcroot
为你做。问题是我的拷贝构造函数是内联定义。我只要让它成为一个真正的函数,并将其实现移到.cpp文件即可:
// in ManWrap.h
class CMObject {
public:
CMObject(const CMObject& o);
};
// in ManWrap.cpp
CMObject::CMObject(const CMObject& o)
: m_handle(o.m_handle) {
}
现在,当编译器调用拷贝构造函数时,调用进入 ManWrap.cpp,此处所有的执行都是托管模式,并且将 m_handle 以 gcroot<Object*>
其真面目对待,而不是低级的本机客户端见到的 intptr_t,gcroot 设置 GCHandle 的 Target。同样,operator=
和包装器函数本身也如法炮制,如:CMObject::ToString 或 CMCapture::Index。任何存取 m_handle
的成员函数必须是真函数,而非内联。你要负责函数调用完全为本机模式。(生活就是如此,我知道)你无法面面俱到,开销问题是顾不上了,除非你要求性能是第一位的。如果你需要实时处理
1.7x106 亿个对象,那么千万别用包装器!如果你只是想不依靠 /clr 而存取几个 .NET 框架类,那么这时调用所产生的开销是可忽略的。
Figure 5 是 ManWrap 最终的 CMObject。一旦你理解了 CMObject 的工作原理,要创建新的包装器易如反掌,只一个克隆过程:从
CMObject 派生,添加标准构造函数和操作符,用 _MANAGED
隐藏涉及使用托管类型的部分,然后将其余的实现为真函数。派生对象的唯一不同是你可以让拷贝构造函数和 operator=
为内联,因为它们可以调用自己的基类,不必直接存取 m_handle:
class CMCapture : public CMObject {
public:
CMCapture(const CMCapture& o) : CMObject(o) { }
};
CMCapture 的拷贝构造可以为内联,因为它只传递其本机形参到 CMObject。在构造对象时,你得有一点付出,但至少你不必为此付出双份。
下面是我概括的一些规则,有了这些规则,你可非常轻松地编写包装器。或者更进一步,编写一些宏将我做 ManWrap 的整个过程流水线化。以下是最终的
CMCapture,它在 RexexWrap.h 文件中:
class CMCapture : public CMObject
{
DECLARE_WRAPPER(Capture, Object);
public:
// wrapped properties/methods
int Index() const;
int Length() const;
CString Value() const;
};
上面代码段使用了在 ManWrap.h 中定义的宏 DECLARE_WRAPPER,为了节省键盘敲入。另外一个宏 IMPLEMENT_WRAPPER
负责相应的实现(参见源代码)。这两个宏声明并实现所有我描述过的基基础构造函数和操作符。不知你是否注意到,宏的名称有意设计成 MFC
程序员熟悉的形式。DECLARE/IMPLEMENT_WRAPPER 假设你遵循我的命名规范:CMFoo 即为托管 Foo 对象的本机包装器名。(我曾用
CFoo,但那样会与 MFC 用于Object 的 CObject 冲突,所以我添加了一个 M 为 CM,M 意为 Managed)。Figure 6 是
DECLARE_WRAPPER 的代码,IMPLEMENT_WRAPPER 与之类似,具体细节请下载源代码。
细心的读者可能已经注意到了,到目前为止,我只编写了缺省构造函数、拷贝构造函数以及带有托管类型指针的构造函数。最后针对本机代码进行隐藏,所以本机客户端好象只能创建空对象(Null)和进行拷贝。那有什么用呢?缺乏构造函数对我的类来说是个令人遗憾的。你无法通过自身来创建
Object,并且 Capture 对象只能来自其它对象,如 Match 或 Group。但是 Regex 有一个真实的构造函数,它带一个 String
参数,所以 CMRegex 象下面这样来包装:
// in RegexWrap.h
class CMRegex : public CMObject {
DECLARE_WRAPPER(Regex,Object);
public:
CMRegex(LPCTSTR s);
};
// in RegexWrap.cpp
CMRegex::CMRegex(LPCTSTR s)
: CMObject(new Regex(s))
{ }
此处再次重申构造函数必须是真函数,因为它调用“new Regex”,它需要托管扩展和 /clr。通常,DECLARE/IMPLEMENT_WRAPPER
仅声明和实现规范的构造函数和操作符,你需要使用它们以类型安全方式操作包装器对象。如果你包装的类有“真实的”构造函数,你必须自己包装它们。DECLARE_WRAPPER
很酷,但它没有透视力。
如果你包装的方法返回某种其它类型的托管对象,那么你还得包装那个类型,因为显然你不能将托管对象直接返回给本机代码。例如,Regex::Match 返回
Match*,所以包装 Regex::Match 的同时还需要包装 Match:
CMMatch CMRegex::Match(LPCTSTR input)
{
return CMMatch((*this)->Match(input));
}
这是用托管类型指针构造对象的一个例子,就像编译器自动将 String 从 Object::ToString 转换为 CString 一样,此处将
Regex::Match 返回的 Match* 转换为 CMMatch 对象的过程也是自动的,因为 CMMatch 具备相应的构造函数(由 DECLARE/IMPLEMENT_WRAPPER
自动定义的)。所以,虽然本机代码无法看到构造函数用托管类型指针构造对象的过程,但它们对于编写包装器来说是不可或缺的。
标签:
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com