本文討論:
- 不借助 /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。在構造對象時,你得有一點付出,但至少你不必為此付出雙份。 下面是我概括的一些規則,有了這些規則,你可非常輕松地編寫包裝器;蛘吒M一步,編寫一些宏將我做 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 小例子,F在,我們集中精力來對它進行包裝?蚣苤惺俏,而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 塊中捕獲基本異常類,這樣才不至于有疏漏而導致你的應用程序崩潰。
包裝數組
包裝完集合、委托和異!,F在該輪到數組了。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 并直接調用框架即可。 |