UML软件工程组织

使用 ManWrap 库在本机 C++ 代码中调用.NET
来源:VC知识网 原著:Paul DiLascia
本文讨论:
  • 不借助 /clr,从本机 C++ 代码中使用托管类;
  • GCHandle,gcroot 以及创建混合模式的 DLLs;
  • .NET 框架中的正则表达式;

本文使用下列技术: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 自动定义的)。所以,虽然本机代码无法看到构造函数用托管类型指针构造对象的过程,但它们对于编写包装器来说是不可或缺的。

RegexWrap

  为祝贺 MSDN 杂志二十周年纪念,现在我解释了 ManWrap,接下来是做一些包装的时候了!我用 ManWrap 将 .NET 的 Regex 类包装在一个叫做 RegexWrap.dll 的 DLL 中。如 Figure 7 所示,一个经过删节的头文件。因为细节很琐碎,我就不作全面解释了,以下是一个典型的包装器:

CString CMRegex::Replace(LPCTSTR input, LPCTSTR replace)
{
  return (*this)->Replace(input, replace);
}

  实际上在每一个案例中,实现就一行:调用底层的托管方法并让编译器转换参数。interop(互用性)不是很好玩吗?即便参数为另一个包装类它也照样工作,就象我在 CMRegex::Match 中已经解释的那样。
  当然,并不是所有的东西都琐碎。我在创建 RegexWrap 的过程中确实也碰到过一些不顺和阻碍:集合(collections)、委托(delegates)、异常(exceptions)、数组(arrays)和枚举(enums)。下面我将一一描述是如何处理它们的。

集合处理

  框架中集合无处不在。例如,Regex::Matches 将所有匹配作为 MatchCollection 返回,Match::Groups 返回的所有 Groups 是 GroupCollection。我处理集合的第一个想法是将它们转换为包装对象的 STL 容器。接着我认识到这是个坏主意。为什么要创建一组已经在集合里的指向对象的新句柄呢?虽然 .NET 的 Collections 在某些方面类似 STL 容器,但它们并不完全相同。例如,你可以通过整数索引或字符串名来存取某个 GroupCollection。
  与其使用 STL vector 或 map,还不如简单一点,使用我已经建立的系统,即 ManWrap。如 Figure 8 所示,我展示了如何包装 GroupCollection。它正是你所期望的,只是新加了一个宏,DECLARE_COLLECTION,它与
DECLARE_WRAPPER 所做的事情一样,此外还添加了三个所有集合都固有的方法:Count、IsReadOnly 和 IsSynchronized。自然少不了 IMPLEMENT_COLLECTION 来实现这些方法。既然 GroupCollection 让你用整数或字符串来索引,那么包装器有两个 operator[] 重载。
  一旦我包装了 Match、Group 和 CaptureCollections,我便可以包装使用它们的方法。Regex::Matches 返回 MatchCollection,所以包装器如下:

CMMatchCollection CMRegex::Matches(LPCTSTR input)
{
  return (*this)->Matches(input);
}

  CMMatch::Groups 和 CMGroup::Captures 完全相同,再次重申,编译器默默地完成所有类型转换。我爱C++ 和 interop!

处理委托

  在编程历史上最重要的革新之一是回调概念。这种调用机制使你调用的某个函数直到运行时才知道。回调为虚拟函数以及所有形式的事件编程提供了基础。但在托管世界,人们不说“回调”,而是说“委托”。例如,Regex::Replace 的形式之一允许传递 MatchEvaluator:

MatchEvaluator* delg = // create one
String *s = Regex::Replace("\\b\\w+\\b",
  "Modify me.", delg);		

  Regex::Replace 针对每个成功的 Match 调用你的 MatchEvaluator 委托。你的委托返回替代文本。稍后,我会展示一个使用 MatchEvaluator 小例子。现在,我们集中精力来对它进行包装。框架中是委托,而C++中称回调。为了使其交流,我先得需要一个 typedef:

class CMMatch ... {
public:
  typedef CString (CALLBACK* evaluator)(const CMMatch&, void* param);
};

  CMMatch::evaluator 是一指向函数的指针,它有两个参数:CMMatch 和 void* param,并返回 CString。将 typedef 放在 CMMatch 完全是风格使然,没有其它意图,但这样做确实避免了全局名字空间的混乱。void* param 为本机调用者提供了一种传递其状态的途径。委托总是要与某个对象关联(如果该方法为静态,则对象可为空),但在 C/C++ 中则始终都是一个函数指针,所以回调接口通常都加一个 void* 以便能传递状态信息。完全是低级C的风格。有了新的 typedef 以及将这些评论了然于心,我可以象这样声明 CMRegex::Replace:

class CMRegex ... {
public:
  static CString Replace(LPCTSTR input, 
    LPCTSTR pattern, 
    CMMatch::evaluator me, 
    void* param);
};

我的包装器类似实际的 Replace 方法(都是静态的),带额外参数 void* param。那么我如何实现它呢?

CString CMRegex::Replace(...)
{
  MatchEvaluator delg = // how to create?
  return Regex::Replace(..., delg);
}

  为了创建 MatchEvaluator 我需要一个 __gc 类,这个类要具备一个方法,该方法调用调用者的本机回调函数,而回调函数带有调用者的 void* 参数。我写了一个小托管类:WrapMatchEvaluator,专做此事(详情请参考代码)。为了节省键盘输入,WrapMatchEvaluator 有一静态 Create 函数,返回一新的 MatchEvaluator,所以 CMRegex::Replace 仍然只有一行:

CString CMRegex::Replace(LPCTSTR input, 
  LPCTSTR pattern,
  CMMatch::evaluator me, 
  void* lp)
{
  return Regex::Replace(input, pattern, 
    WrapMatchEvaluator::Create(me, lp));
}

  好了,源文件中只有一行,这里是为了便于美观和印刷的原因而将其分行了。既然本机代码用不着 WrapMatchEvaluator(它是一个 __gc 类),在 RegexWrap.cpp 内实现,而非头文件。

处理异常

  .NET 框架迟早会抱怨你的所为粗鲁,我知道,如果你传给 Regex 一个糟糕的表达式,你有何指望?本机代码无法处理托管异常,所以我还得做一些事情。在 CLR 调试器中 Dump 用户信息当然不会让我觉得光彩,所以我也得包装 Exceptions。我会在边界捕获它们并在它们流窜到本机区域之前让它们裹上其包装。捕获并包装是个单调乏味的活,但又不得不做。Regex 的构造函数可以丢出异常,所以我需要修订我的包装器:

Regex* NewRegex(LPCTSTR s)
{
  try {
    return new Regex(s);
  } catch (ArgumentException* e) {
    throw CMArgumentException(e);
  } catch (Exception* e) {
    throw CMException(e);
  }
}

CMRegex::CMRegex(LPCTSTR s) : CMObject(NewRegex(s))
{
}

  基本套路是在包装器内捕获异常,然后用包装好的形式再重新丢出它。之所以引入 NewRegex 是因为这样做我能使用初始化语法,而不用构造函数中对 m_handle 赋值(那样效率不高,因为要赋值 m_handle 两次)。一旦我捕获并包装好 Exceptions,本机代码便能以本机方式处理它们.下面示范了当用户敲入坏表达式时 RegexTest 是如何应对的:

// in FormatResults
try { 
  // create CMRegex, get matches, build string
} catch (const CMException& e) {
  result.Format(_T("OOPS! %s\n"), e.ToString());
  MessageBeep(0);
  return result;
}		

  在包装异常时有一点要考虑,即是否需要包装每一种异常丢出。对于 Regex 而言,只有 ArgumentException,但 .NET 有一大堆异常类型。包装哪一个以及要添加多少 catch 块依赖于你的应用程序需要多少信息。无论你做什么,都要保证在最后的 catch 块中捕获基本异常类,这样才不至于有疏漏而导致你的应用程序崩溃。

包装数组

  包装完集合、委托和异常。现在该轮到数组了。Regex::GetGroupNumbers 返回整型数组,而 Regex::GetGroupNames 返回字符串数组(String)。将它们传递到本机区域之前,我必须将托管数组转换为本地类型。C-风格数组是一种选择,但有 STL 存在,便没有理由使用 C-风格的数组。ManWrap 有一个模板函数,用来将 Foo 托管对象数组转换成 CMFoo 类型的 STL vector。CMRegex::GetGroupNames 使用它,正像你下面所看到的:

vector<CString> CMRegex::GetGroupNames()
{
  return wrap_array<CString,String>((*this)->GetGroupNames());
}

  又是只有一行代码。另一个 wrap_array 转换整型数组,因为编译器需要 __gc 说明符来断定本机和托管整型数组之间的差别,具体细节你去琢磨源代码吧。

封装枚举

  终于轮到封装枚举了,这是 RegexWrap 一系列要解决的问题中最后一个。其实也不是什么问题,只是解决令人头疼的键盘敲入。某些 Regex 方法允许用 RegexOptions 来进行行为控制。例如,如果你想忽略大小写,可以用 RegexOptions::IgnoreCase 调用 Regex::Match。为了让本机应用存取这些选项,我用相同的名称和值定义了自己的本地枚举,如 Figure 7 所示。为了节省键盘输入和消除错误,我写了一个小实用工具 DumpEnum,它为任何.NET框架枚举类生成 C 代码。

建立混合模式的 DLLs

  解决了所有的编程问题,最后一步是将 RegexWrap 打包成一个DLL。此时你的所有类通常都得用__declspec(dllexport) 或 __declspec(dllimport)处理(而我是宏来简化的),同时在生成托管DLL时,你还得玩点技巧。托管DLLs需要专门的初始化,因为它们不能用常规的 DllMain 启动代码,它们需要 /NOENTRY 以及手动初始化。详情参见 2005 年二月的《C++ At Work》专栏。RegexWrap 的底线是使用 RegexWrap.dll,你必须实例化一个专门的 DLL----在全局范围的某个地方初始化类,就像如下的代码行这样:

// 在应用程序的某个地方
CRegexWrapInit libinit;

  调试期间我还遇到一个小问题。为了在你的本机应用程序中调试包装器DLLs,你需要在项目的调试(Debug)设置中将“调试器类型(Debugger Type)”设置为“混合模式(Mixed)”。默认(自动)加载哪个调试器要依赖 EXE。对于 ManWrap 来说,EXE 是本机代码,所以IDE使用本机调试器,那么你就无法跟踪到托管代码。如果你选择“调试类型”为“混合模式”,那么IDE两个调试器都加载。
  一旦你摆平了这些小麻烦,RegexWrap 便会像任何其它 C++ DLL 工作。客户端包含头文件并链接导入库。自然,你需要在PATH中加入 RegexWrap.dll 的路径,并且 .NET 框架要运行起来。典型的客户端应用(如 RegexTest)其文件及模块之间的关系如图 Figure 9 所示。


Figure 9 文件和模块的关系

RegexWrap 趣事

  随着 Regex 的最后包装,现在该消遣一下了!我写 RegexWrap 的缘由是为了将正则表达式的威力带给本机 MFC 程序。
  我做的第一件事情是用 RegexWrap 将我原来所写的混合模式的 RegexTest 程序及其托管函数 FormatResults 移植为纯粹本机版本。每个 Regex、Match、Group 和 Capture 指针现在都成了 CMRegex、CMMatch、CMGroup 或 CMCapture 对象。集合的情况可入法炮制(详情请下载源代码)。重要的是现在 RegexTest 完全本地化了,在其项目文件或make文件里你找不到 /clr。如果你是正则表达式新手,那么 RegexTest 是你开始探究它们的最好途径。
  接下来的例子是一个有趣的程序,这个程序将英语变成难以理解的乱语。语言学家长期以来都在观察下面这这种古怪的现象:如果你打乱某个句子中每个单词中间的字母,而保留开始和结尾处的字母,那么结果比你想象的更可读。显然,我们的脑子是通过扫描单词开始和结尾处的字母并填充其余部分来阅读的。我用 RegexWrap 实现了一个 WordMess 程序,它演示了这种现象。敲入一个句子后,WordMess 向所描述的那样打乱它,程序运行如 Figure 10 所示。这里是 WordMess 以本自然段的第一句为例:“my nxet sapmle is a fun prgaorm taht tnurs Ensiglh itno smei-reabldae gibbiserh.”


Figure 10 WordMess

WordMess 使用 MatchEvaluator 委托形式的 Regex::Replace(当然是通过其包装器):

// in CMainDlg::OnOK 
static CMRegex MyRegex(_T("\\b[a-zA-Z]+\\b"));
CString report;
m_sResult = MyRegex.Replace(m_sInput, &Scrambler, &report);

  MyRegex 为匹配单词的静态 CMRegex 对象,也就是说,打乱环绕单词的一个或多个字母的顺序。(用C++编写正则表达式最难的部分是每次都要记住两个你想得到的反斜线符号的类型。)所以 CMRegex::Replace 针对输入句子中每个单词调用我的 Scrambler 函数一次。Scrambler 函数如 Figure 11 所示。看看用 STL 字符串和 swap 以及 random_shuffle 算法使问题的解决变得多么容易。如果没有 STL,那么将面临编写大量的代码。Scrambler 将  CString 作为其 void* param 参数,所做的每次替换都添加到这个 CString。WordMess 将报告添加到其结果显示区域,如 Figure 10 所示。多神奇啊!
  我的最后一个应用,我选择更认真和实用的东西,这个程序叫做 RegexForm,验证不同类型的输入:邮编、社会保险号、电话号码以及 C 符号(tokens)。有关 RegexForm 的讨论参见本月的 C++ At Work 专栏。

结论

  好了,包装就讲到这里!希望你已经和我一起分享了包装 Regex 的乐趣,同时我希望你能找到用 ManWrap 包装其它框架类的方法,从而你能从本机代码中调用。ManWrap 并不适合每一个人:只有当你想保持本机代码而又想调用框架时才需要它,否则用 /clr 并直接调用框架即可。

 

版权所有:UML软件工程组织