Saturday, March 05, 2005

 

Some notes on using .Net in MFC - 1

1.
示例:在MFC程序中集成.Net中的控件

By jiangsheng
From http://blog.joycode.com/jiangsheng/archive/2005/03/19/46065.aspx

从.Net Framework 1.1开始,.Net控件可以以ActiveX的方式被集成到非托管宿主中——但是官方的支持只对于使用托管C++的MFC程序。Chris Sells在2003年3月份的MSDN杂志中描述了这样一个示例(http://msdn.microsoft.com/msdnmag/issues/03/03/WindowsForms/default.aspx)。这个示例使用的代码稍微繁琐,而且没有描述如何处理控件的事件。MFC 8.0增加了一系列这方面的支持来把这个集成过程简单化(参考http://msdn2.microsoft.com/library/ahdd1h97.aspx)。这使得在MFC程序中使用.Net中的一些比较好用的类,例如System::Windows::Forms::PropertyGrid比以前容易多了。

举例来说,要在MFC的基于对话框的程序中使用System::Windows::Forms::PropertyGrid控件,首先创建一个基于对话框的程序,添加必要的引用:

#include // MFC Windows Forms support
#using
#using
#using
#using
#using

using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::ComponentModel;
using namespace Microsoft::VisualC::MFC;
using namespace stdcli::language;

之后添加代码(下面这个类是从MSDN中的充分利用.NET 框架的PropertyGrid 控件这篇文章里面借过来的,关于此控件的更加高级的使用方法也可以参考这篇文章)。
http://www.microsoft.com/china/msdn/archives/library/dndotnet/html/usingpropgrid.asp

public ref class AppSettings
{
public:
[Description("文档设置"), Category("文档设置"), DefaultValue(false)]
property Boolean saveOnClose ;
[Description("文档设置"),Category("全局设置"), ReadOnly(true),DefaultValue("欢迎使用应用程序!")]
property String^ greetingText ;
[Category("全局设置"), DefaultValue(4)]
property Int32 itemsInMRU ;
[Description("以毫秒表示的文本重复率。"), Category("全局设置"),DefaultValue(10)]
property Int32 maxRepeatRate ;
[Browsable(false), DefaultValue(false)]
property Boolean settingsChanged ;
[Category("版本"), DefaultValue("1.0"), ReadOnly(true)]
property String^ appVersion ;
AppSettings()
{
this->saveOnClose = true;
this->greetingText = gcnew String("欢迎使用应用程序!");
this->maxRepeatRate = 10;
this->itemsInMRU = 4;
this->settingsChanged = false;
this->appVersion = gcnew String("1.0");
}
};


class CPropertyGridTestDlg : public CDialog

{

//为了偷懒起见,向导生成的默认代码省略

CWinFormsControl m_wndPropertyGrid;

BEGIN_DELEGATE_MAP( CPropertyGridTestDlg )
EVENT_DELEGATE_ENTRY( PropertyValueChanged , Object, PropertyValueChangedEventArgs )
EVENT_DELEGATE_ENTRY( HandleDestroyed , Object, EventArgs )
END_DELEGATE_MAP()
public:
void PropertyValueChanged ( Object^ sender, PropertyValueChangedEventArgs ^ e )
{
TRACE("PropertyValueChanged %S\n", marshal_as(e->ToString()));
}
void HandleDestroyed( Object^ sender, EventArgs ^ e )
{
TRACE("PropertyValueChanged %S\n", marshal_as(e->ToString()));
}
};

BOOL CPropertyGridTestDlg::OnInitDialog()
{

//为了偷懒起见,向导生成的默认代码再次省略

// TODO: 在此添加额外的初始化代码
CRect rect;
GetDlgItem(IDC_PLACEHOLDER)->GetWindowRect(rect);//IDC_PLACEHOLDER是一个用来占地方的Static控件
GetDlgItem(IDC_PLACEHOLDER)->DestroyWindow();
ScreenToClient(rect);


m_wndPropertyGrid.CreateManagedControl( WS_VISIBLE|WS_CHILD, rect, this, IDC_PLACEHOLDER );
System::Windows::Forms::PropertyGrid^ pGrid=m_wndPropertyGrid.GetControl();

AppSettings^ appSettings=gcnew AppSettings;
pGrid->SelectedObject=appSettings;

pGrid->PropertyValueChanged += MAKE_DELEGATE( PropertyValueChangedEventHandler ,PropertyValueChanged );

pGrid->HandleDestroyed += MAKE_DELEGATE( System::EventHandler , HandleDestroyed );

}

在VC2005二月份的CTP中使用这个功能还有一些小问题:启动的时候输出窗口有几个警告:还有一个Assert窗口,可以简单地忽略。退出的时候有一个原因不明的内存泄漏。

题外话:尽管我确定marshal_as这个函数2004年4月就在可用(在2004年4月的全球MVP峰会上,我亲眼看见对这个函数的引用在Visual C++ 2005中通过了编译),而且这个函数在各种文档中也时有出现,但是为了赶进度,这个函数似乎被砍了。为了平时偷懒起见,我不得不自己写了一个模板函数。

template
interior_ptr marshal_as (String^ s)
{
interior_ptr txt=PtrToStringChars (s);
return interior_ptr(txt);
}

2.
Tom Archer的文章,来自
http://www.codeguru.com/columns/DotNetTips/article.php/c7857/
http://www.codeguru.com/Cpp/Cpp/cpp_managed/general/article.php/c7929/

.NET programming includes the concept of delegates and events to facilitate the Observer or Publish/Subscribe design pattern. Most times, you use events in much the same manner that callback functions have been used for years in standard Win32 API or MFC programming. The program flow works like this:

An object (the event sender) publishes the fact that it can provide information—such as a user's interaction with the system or data changing—in the form of an event.
The client code (the event receiver) then can subscribe to the desired object's event—by specifying a method to be called when the event is fired or raised—if its program logic needs to be made aware of the published event.
When the event occurs, the client's event handler is invoked.
Also note that the client code can subscribe to multiple events from multiple event senders and at any time can specify that it no longer wishes to be notified of a given event.

Because the event sender shouldn't be coupled with the event receiver (in terms of knowing which object or method will receive the event that will be raised), .NET defines a generic layer (function seam) between the sender and receiver. This layer is defined by a special .NET type (called a Delegate) that provides the functionality of a type-safe function pointer.

Here are the basic steps for subscribing to a .NET event:

Locate the desired class/event.
Define the event handler.
Subscribe to the event.
Locate the Desired Class/Event
Events are always members of a class, so let's look at an example class/event that is already defined in the .NET BCL (Base Class Library): FileSystemWatcher. Because the FileSystemWatcher class' sole objective is to asynchronously monitor the file system for various types of file and directory modifications, it provides events that allow client code to specify which types of modifications of which they need to be made aware. One such event is the FileSystemWatcher::Deleted event that is raised whenever a file or directory in the specified path is deleted from the file system (either programmatically or by a user). Therefore, if your application needed to be made aware of such an activity, it would simply subscribe to this event.

Define the Event Handler
Before you can define the event handler, you need to look at the event declaration. Here's such a declaration in Managed Extension syntax:

// Event sender's declaration of the event
public: __event FileSystemEventHandler* Deleted;

The first thing to notice is the Managed Extensions __event type. This simply tells the .NET runtime that Deleted is an event that client code can subscribe to. The second thing to notice is the type that follows it. This is the Delegate type. Your event handler must conform to the signature of this Delegate. Therefore, you would simply look up the FileSystemEventHandler signature in online-help, where you would see the following:

// Delegate declaration
public __gc __delegate void FileSystemEventHandler(
Object* sender,
FileSystemEventArgs* e
);

Armed with this information, you now know how to define the event handler. As an example of that, here's a method called OnDelete where the return value and parameter list are the same as the FileSystemEventHandler signature:

// Event handler
void OnDeleted (Object* source, FileSystemEventArgs* e)
{
// event processing logic here
}

Subscribe to the Event
At this point, you need only to subscribe to the event. To do that, first instantiate a FileSystemWatcher object. Then, use the Event class's overloaded += operator and add a new instance of a FileSystemEventHandler object, passing it the current object and the event handler (OnDelete) that will be called when the event fires:

// Client code (event receiver)
FileSystemWatcher* pWatcher = new FileSystemWatcher();
pWatcher->Deleted += new FileSystemEventHandler(this, OnDeleted);

Define Your Own Events and Delegates Too
That's all there is to it: three simple steps to subscribing to any .NET event with your own event handlers. In addition to learning how to use these types (as they will be used more and more in future releases of the BCL), I would also encourage you to start defining your own events and delegates in your code. This enables a generic means of allowing client code to be alerted to application-specific events.

Let's say that you have an MFC application with the /CLR flag set (the "Use Managed Extensions" option in the Project Settings). You then want to subscribe to the FileSystemWatcher::Deleted event so that your application is notified each time a file is deleted in the specified path.

However, if you attempt to specify an event handler within one of your MFC classes (such as the application, view, or dialog class) and then attempt to instantiate the delegate using that class, you'll receive a compiler error stating that you can't instantiate a delegate from a "non-member function or a member of an unmanaged class". Therefore, you'll need to create a managed class (__gc) wrapper for the event handler similar to the following:

#using
using namespace System::IO;

...

__gc class FileWatcherEvents
{
public: static void OnDeleted (Object* source, FileSystemEventArgs* e)
{
AfxMessageBox("A file was deleted!");
}
};

Note that I made the member method static so that I don't need to instantiate the FileWatcherEvents class. Obviously, if you're going to have instance data associated with the class (and specifically this method), you'll need to make this an instance method.

I then can subscribe to the event as follows:

#pragma push_macro("new")
#undef new
FileSystemWatcher* pWatcher = new FileSystemWatcher("d:\");
pWatcher->EnableRaisingEvents=true;
pWatcher->Deleted += new FileSystemEventHandler(
NULL, FileWatcherEvents::OnDeleted);
#pragma pop_macro("new")

You can plug this code directly into a mixed-mode application and a message box will appear any time a file is deleted from the D: root folder (specified in the FileSystemWatcher constructor). However, chances are you'll want to do something other than display a message to this effect. Therefore, the next task is figuring out how to communicate between the managed object and the desired MFC object (such as the dialog object in the download demo).

One obvious way of handling this problem would be to pass the MFC object to the handling wrapper object. However, that would couple the two classes a little too tightly for my taste. Another—more generic—way would be to pass the HWND of the client window to the wrapper object. Now, we're onto something! That would at least decouple the wrapper from a specific MFC class. We could take that notion a step further and create a base event-handling class from which all of our event-handling wrappers would derive. Then, we could instantiate each derived class with the HWND of the client window, as well as a message ID that the wrapper could use in sending a message to signal that an event had been raised. Here's an example of such a base class:

__gc class EventBase
{
public:
EventBase(HWND subscriberWindow, UINT messageID)
{
this->subscriberWindow = subscriberWindow;
this->messageID = messageID;
}
protected:
HWND subscriberWindow;
UINT messageID;
protected:
void SendEventInfo(String* info)
{
const __wchar_t __pin * path = PtrToStringChars(info);
::SendMessage(subscriberWindow, messageID, (WPARAM)path, NULL);
}
};

Note that the base class has a SendEventInfo method that allows all derived classes to send a string back to the client. (As I explained in the article, "Converting Between MFC/C++ and .NET Types," the __pin keyword and PtrToStringChars functions are used to convert a managed String object to an unmanaged char array.) Obviously, you can supply other methods if you wish to send more complex types to the client.

Now, you can derive each of your event-handling wrapper classes from the EventBase class. Using our FileSystemWatcher example, we could define the following class to handle the FileSystemWatcher::Deleted event:

__gc class FileWatcherEvents : public EventBase
{
public:
FileWatcherEvents(FileSystemWatcher* watcher,
HWND subscriberWindow, UINT messageID)
: EventBase(subscriberWindow, messageID)
{
watcher->EnableRaisingEvents=true;
}

public:
void OnDeleted (Object* source, FileSystemEventArgs* e)
{
SendEventInfo(e->FullPath);
}
};

Note how the FileSystemWatcher-specific code, such as which event to subscribe to (Deleted) and the setting of the EnableRaisingEvents property, are placed in this derived class.

The client code then would instantiate the event-handler wrapper as follows:

#pragma push_macro("new")
#undef new
FileSystemWatcher* pWatcher =
new FileSystemWatcher("d:\");

FileWatcherEvents* fileEvents =
new FileWatcherEvents(pWatcher, GetSafeHwnd(), WM_FILE_DELETED);

pWatcher->Deleted +=
new FileSystemEventHandler(fileEvents,
FileWatcherEvents::OnDeleted);
#pragma pop_macro("new")

At this point, the FileWatcherEvents will automatically send a message to the client window (as specified in its constructor) when a file is deleted from the D: root folder.

You need only define the message ID and implement a handler. Here's an example from the article's download demo:

#define WM_FILE_DELETED (WM_USER + 1)

...

BEGIN_MESSAGE_MAP(CHandlingEventsInMFCDlg, CDialog)
ON_MESSAGE(WM_FILE_DELETED, OnFileDeleted)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

...

LRESULT CHandlingEventsInMFCDlg::OnFileDeleted(WPARAM wp, LPARAM)
{
const __wchar_t * path = (const __wchar_t *)wp;

int idx = m_lstEvents.InsertItem(0, _T("Deleted"));
m_lstEvents.SetItemText(idx, 1, (CString)path);

return 0L;
}

As you can see, the handler simply converts the WPARAM first to its original unmanaged char array form and then casts it to a CString object so that it can be displayed in the demo's list control.

Easier with Practice
While not terribly intuitive at first blush, once you get the hang of handling events in a managed object and then communicating the desired information between that managed object and your unmanaged MFC objects, you'll see that subscribing and dealing with events in mixed-mode applications becomes easy. In addition, while my first swing at a generic base class is far from perfect, it should get you started in the right direction to making this task more generic for your programming needs.

3.
Invoking .NET Events from Native C++
by darwen
from http://www.codeguru.com/Csharp/.NET/net_general/patterns/article.php/c8381/

Introduction
When writing .NET wrapper classes to interface with native C++ code, there are occasions when the native code will need to raise events in the .NET wrapper class. Examples of this are either virtual overrides (for example, OnReceive and OnClose in the CAsyncSocket class) or message mapped functions on CWnd-derived classes (as in OnSize, OnMove, and so forth).

However, .NET only allows events to be raised from inside of the containing class. To do what we want, we must have a native C++ object that invokes the appropriate event via a public member function on the managed wrapper class.

The following diagram shows a virtually overridden function invoking an event on a .NET class through a public member function.

However, this isn't good design. We don't want a class anywhere in the system to be able to raise these events. We want only our native class to be able to do it. Never mind the fact it clutters up the class definition needlessly.

In native C++, the way around such issues was to use 'friends'—one class could call protected and private members on another. But .NET doesn't allow friends. So what is the solution to our problem?

Solution
The solution is to have an intermediate managed class existing in between the native class and the .NET class with the same events as the .NET class.

Thus, in code terms we have:

class CNative
{
protected:
virtual void OnEvent();
} ;

__gc class NativeInterface;
class CNativeDerived : public CNative
{
public:
CNativeDerived(NativeInterface *pInterface)
{
m_pInterface = pInterface;
}

protected:
virtual void OnEvent()
{
m_pInterface->RaiseEvent();
}

private:
gcroot m_pInterface;
} ;

__gc class NativeInterface
{
public:
NativeInterface()
{
m_pDerivedClass = new CNativeDerived(this);
}

virtual ~NativeInterface()
{
delete m_pDerivedClass;
}

__event System::EventHandler *Event;

void RaiseEvent()
{
if (Event != NULL)
{
Event(this, NULL);
}
}

private:
CNativeDerived *m_pDerivedClass;
} ;

public __gc class ManagedWrapper
{
public:
ManagedWrapper()
{
m_pInterface = new NativeInterface;
m_pInterface->Event += new EventHandler(this, OnEvent);
}

__event System::EventHandler *Event;

protected:
virtual void OnEvent(System::Object *object, System::EventArgs *args)
{
if (Event != NULL)
{
Event(this, null);
}
}

private:
NativeInterface *m_pInterface;
} ;

Therefore, if this functionality is put into an assembly in a DLL, only the ManagedWrapper class and its event will be visible.

(按:下略)

4.
使用 ManWrap 库在本机 C++ 代码中调用.NET

原著:Paul DiLascia
原文出处:http://msdn.microsoft.com/msdnmag/issues/05/04/C/default.aspx
翻译:NorthTibet
From http://www.vckbase.com/document/viewdoc/?id=1450

本文讨论:

不借助 /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 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 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,而非 gcroot。所以,我需要一个新的句柄吗?不要。Capture 从 Object 派生,所以 gcroot 句柄也能操控 Capture 对象。

class CMCapture : public CMObject {
public:
// 调用基类构造函数初始化
CMCapture(Capture* c) : CMObject(c) { }
};
  CMCapture 需要与 CMObject 完全相同的构造函数和操作符,并且我必须重写 ThisObject 和 operator-> 返回新的类型。

Capture* ThisObject() const
{
return static_cast((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
#else
# define GCHANDLE(T) intptr_t
#endif
现在我们可以象下面这样修正 CMObject:

class CMObject {
protected:
GCHANDLE(Object*) m_handle;
...
};
  这样用 /clr 编译的模块(即包装器自己)能看到 gcroot 句柄。不用 /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 的托管代码中行得通,但在本机代码中 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 其真面目对待,而不是低级的本机客户端见到的 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 CMRegex::GetGroupNames()
{
return wrap_array((*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 并直接调用框架即可。

5.
编写、加载和存取插件程序(Plug-Ins)

原著:Paul DiLascia
翻译:NorthTibet
From http://www.vckbase.com/document/viewdoc/?id=1543

在 2005 年一月刊的 MSDN 杂志文章中,你有一个例子程序的代码是用混合模式编写的。有没有可能动态加载 .NET 类或 DLL 并调用那些函数呢?假设我有一个本机 C++ 应用程序,我想允许用户在 .NET 中为该 C++ 程序编写插件。就像在 .NET 中使用 LoadLibrary 加载 DLLs 一样。

Ravi Singh
我正在用 Visual C++ 6.0 编写一个插件应用,它是一个 DLL,输出和接收纯虚拟接口指针。加载 DLL 后,EXE 便调用 DLL 中输出的 C 函数,该函数返回一个纯虚拟接口指针。然后 EXE 调用该接口上的方法,有时会传回另一个接口指针给 DLL 处理。
  目前有人要求必须用 C#,Visual Basic .NET 和其它语言编写插件。我没有什么基于 .NET 的编程经验,不懂托管和非托管代码之间的通讯问题,我找到许多有关这方面的信息,但是越看越糊涂。我如何才能让用户编写基于.NET 语言的插件? 
Daniel Godson
在 MSDN 杂志 2003 年 10 月刊中,有一篇 Jason Clark 写的一篇关于插件的文章,但我并不介意在此复习一下这个主题,尤其是因为插件本身就是 .NET 框架中举足轻重的部分(参见:Plug-Ins: Let Users Add Functionality to Your .NET Applications with Macros and Plug-Ins)。毕竟,微软 .NET 框架的主要目的之一就是为编写可重用的软件组件提供一种语言无关的系统。从第一个 “Hello,world”程序到现在,这已经成为软件 开发至高无上的准则。可重用性从拷贝/粘贴到子例程,再到静态链接库,再到 DLLs 以及更专业的 VBX,OCX 和 COM。虽然最后三个东西属于不同的主题(它们都是 本机 DLLs),.NET 框架标志着一个真正的开端,因为所有代码都被编译成微软中间语言(MSIL)。互用性成为一种不可或缺的成分,因为在公共语言运行时层面,所有代码都一样。这就使得编写支持语言中立的插件体系结构 的程序变得尤其容易。
  那么在你的 C++ 程序中如何利用这个优势呢?Daniel 的虚拟函数指针系统就是一个手工自制的 COM。它就是 COM 对象本质之所在:纯虚拟函数指针。你可以为插件模型使用 COM ,开发人员可以用任何面向 .NET 的语言编写插件,因为这个框架让你创建和使用 COM 对象。但众所周知, COM 编码非常繁杂,因为它需要考虑的细节颇多,例如注册、引用计数,类型库等等——这些东西足以使你认为 COM 简直就是“Cumbersome Object Model”(麻烦对象模型)。如果你正在编写新代码并试图简化你的日常工作,那么就用 .NET 直接实现一个插件模型吧,我现在就是在讨论这个话题。
  首先让我回答 Ray 的问题,即:在 .NET 中有没有类似 LoadLibrary 的东西,答案是:有,你可以用静态方法 System::Assembly::Load 加载任何框架程序集(就是一个包含 .NET 类的 DLL)。此外,.NET 支持反射机制。每个程序集都提供所有你需要的信息,如:该程序集有什么类,什么方法以及何种接口。不需要关心 GUIDs,注册,引用计数等诸如此类的事 情。
  在我展示更一般的插件系统之前,我将从一个简单的例子开始,Figure 1 是一个 C# 类,它提供一个静态函数 SayHello。注意与 C/C++ 不同,在 .NET 中函数不单独输出;每个函数必须属于某个类,虽然这个类可以为静态的,也就是说它不需要实例化。为了将 MyLib.cs 编译成一个库,可以这样做:csc /target:library MyLib.cs
编译器将产生一个名为 MyLib.dll 的 .NET 程序集。为了通过托管扩展从 C++ 中调用 SayHello,你得这样写:

#using
#using
using namespace MyLib;
void main ()
{
MyClass::SayHello("test1");
}
  编译器链接到 MyLib.dll 并调用正确的入口点。这一切都简单明了,它属于 .NET 的基础。现在假设你不想在编译时链接 MyLib,而是想进行动态链接,就像在 C/C++ 用 LoadLibrary 那样。毕竟,插件无非是要在运行时链接,在你已经生成并交付的应用程序之后。Figure 2 所做的事情和前述代码段一样,只不过它是动态加载 MyLib 的。关键函数是 Assembly::Load。一旦你加载了该程序集,你便可以调用 Assembly::GetType 来获得有关类的 Type 信息(注意你必须提供全限定名字空间和类名),进而调用 Type::GetMethod 来获取有关方法的信息,甚至是调用它,就像这样:

MethodInfo* m = ...; // get it
String* args[] = {"Test2"};
m->Invoke(NULL, args);
  第一个参数是对象实例(此例中为 NULL,因为 SayHello 是静态的),第二个参数是 Object (对象)数组,明白了吗?
  在继续往下讨论之前,我必须指出 Load 函数有几个,正是这一点很容易把我们搞糊涂。.NET 被设计用来解决的一个问题就是所谓的 DLL 地狱(DLL Hell)问题,当几个应用程序共享某个共公 DLL 并想要更新该 DLL 时常常会发生这个问题——它能使某些应用程序崩溃。而在 .NET 中,不同的应用程序可以加载同一个程序集/DLL的不同版本。不幸的是,DLL 地狱现在变成了 Load 地狱(Load Hell),因为加载程序集的规则是如此复杂,我都可以专门写一个专栏来描述它。
  加载并将程序集邦定到你的程序的过程称为熔接(fusion),甚至框架带有专门的程序,fuslogyw.exe (Fusion Log Viewer)来做这件事情,你可以用它确定加载了那个程序集的哪个版本。正像我说过的,要完整地描述框架是如何加载并邦定程序集,以及它是如何定义“身份”(identity)的需要几页篇幅 才能说清楚。但对于插件来说,只需考虑两个函数:Assembly::Load 和 Assembly::LoadFrom。
  Assembly::Load 的参数可以是完整的或部分的名称(例如,“MyLib”或者“MyLib Version=xxx”Culture=xxx”)。Figure 2 中的测试程序加载“MyLib”,然后显示完整的程序集名称,如 Figure 3 所示:


Figure 3 测试程序

  Assembly::Load 使用框架的发现规则来决定实际加载了哪个文件。它在 GAC(全局程序集缓冲:Global Assembly Cache)里,你的程序给出的路径以及应用程序所在的目录以及诸如此类的路径中查找。
  另一个函数 Assembly::LoadFrom 使你能从外部路径加载程序集。这里有一点模糊的是如果相同的程序集(由同一性规则确定)已经被从不同的路径加载,框架将会使用之。所以 LoadFrom 并不总是正确地使用通过该路径指定的程序集,尽管大多数时候能正确使用。晕了吧?还有另外一个方法是 Assembly::LoadFile,它总是加载请求的路径——但你几乎从来用不上 LoadFile,因为它解决不了依赖性问题,并且无法将程序集加载到正确的环境中(LoadFrom)。不用去了解所有的细节,我将对 LoadFrom 进行简单地讨论,以此说明对于简单的插件模型,它是一个很好用的函数。
  这样一个模型的基本思路是定义一个接口,然后让其他人编写实现此接口的类。你的应用程序可以调用 Assembly::LoadFrom 来加载插件,并用反射来查找实现你所定义之接口的类。不过在你动手之前,有两个重要的问题要问:你的应用程序需要在运行中卸载或重新加载插件吗?你的程序需要 考虑对插件必须使用的文件或其它资源进行安全存取吗?如果你对两个问题的答案都为 YES,那么你将需要 AppDomain。
  在框架中,没有办法直接卸载某个程序集。唯一途径是将程序集加载到单独的 AppDomain,然后卸载整个 AppDomain。每个 AppDomain 还可以有其自己的安全许可。 AppDomains 带有一个隔离的处理单元,通常由单独的进程操控,一般都用于服务器程序中,服务器基本上都是昼夜运行(24x7),并需要动态加载和卸载组件而不用重新启动。AppDomains 还被用于限制插件获得的许可,以便某个应用能加载非信任组件而不用担心其恶意行为。为了启用这种隔离,需要远程机制来使用多个 AppDomains;不同 的 AppDomains 其对象无法相互直接调用,他们必须跨 AppDomain 边界进行封送。尤其是类的共享实例必须从 MarshalByRefObject 派生。
  这就是我现在要讲的 AppDomains。接下来我将描述一个非常简单的插件模型,它不需要 AppDomains。假设你生成了一个图像编辑器,并且你想让其他开发人员编写插件来实现诸如曝光、模糊或使部分像素变绿等特效。 此外,如果你拥有数据库所有权,你想让别的开发人员编写专门的导入/导出过滤器,以便对你的数据和他们自定义的文件格式之间进行转换。在这种情况下,应用程序在启动时加载所有的插件,插件一直保留加载状态,也就是说一直到用户退出程序。该模型不需要服务器程序具备重新加载功能,插件与应用程序本身具有相同的安全许可。所以没有必要使用 AppDomains;所有插件可被加载到主应用程序域中。这是桌面应用程序典型的使用模式。
  为了真正实现这个模型,首先要定义每个插件必须实现的接口。接口实际上就像是 COM 的接口,它是一个抽象基类,在这个类中定义了插件必须实现的属性和方法。在本文的例子中,我顺便写了一个可扩展的文本编辑器,名叫 PGEdit,它带有一个插件接口 ITextPlugin(参见 Figure 4)。ITextPlugin 有两个属性,MenuName 和 MenuPrompt, 以及一个方法 Transform,该方法带一个串参数,对传入的字符串进行处理,然后返回新的串。我为 PGEdit 实现了三个具体的插件:PluginCaps,PluginLower 和 PluginScramble,其功能分别是大写,小写和打乱文本字符。如 Figure 5 所示,PGEdit 的三个插件被添加到 Edit 菜单的情形。


Figure 5 带有三个插件的 PGEdit

我编写了一个类叫 CPluginMgr,它负责管理插件(参见 Figure 6)。PGEdit 启动时调用 CPluginMgr::LoadAll 加载所有插件:

BOOL CMyApp::InitInstance()
{
...
m_plugins.LoadAll(__typeof(ITextPlugin));
}
  此处 m_plug-ins 为 CPluginMgr 的一个实例。构造函数的参数为一个子目录名(默认值是 “Plugins”);LoadAll 搜索该文件夹查找程序集,在该程序集中包含的类实现了所请求的接口。当它找到这样一个程序集,CPluginMgr 便创建一个该类的实例并将它添加到一个列表中(STL vector)。下面是关键代码段:

for (/* each type in assembly*/) {
if (iface->IsAssignableFrom(type)) {
{Object* obj = Activator::CreateInstance(type);
m_objects.push_back(obj);
count++;
}
}
  换句话说,如果类型(type)可被赋值给 ITextPlugin,CPluginMgr 则创建一个实例并将其添加到数组。因为 CPluginMgr 是一个本机类,它无法直接保存托管对象,所以数组 m_objects 实际上是一个 gcroot 类型的数组。如果你在 Visual C++ 2005 中使用新的 C++ 语法,可用 Object^ 替代。注意 CPluginMgr 是一个通用类,支持任何你设计的接口。只要实例化并调用 LoadAll 即可,并且你最终要用插件对象数组。CPluginMgr 报告它在 TRACE 流中找到的插件。如果你有多个接口,那么你可能得为每个接口使用单独的 CPluginMgr 实例,以便保持插件之间的隔离。
  在性能上,CLR 团队的 Joel Pobar 在 MSDN 杂志 2005 年7月刊里写了一篇令人恐怖的文章(Reflection: Dodge Common Performance Pitfalls to Craft Speedy Applications),在这篇文章中,他讨论了使用反射的最佳实践。他建议利用程序集层面的属性来具体说明程序集中哪个类型实现了插件接口。这样便允许插件管理器快速查找并实例化插件,而不是非得循环查找程序集中的每种类型,如果类型太多,那将是个昂贵的操作。如果你发现本期专栏里的代码在加载你自己的插件时性能很糟的话,你应该考虑改用 Joel 推荐的方法。但是对于一般的情况,这个代码足以胜任。
  一旦你加载了插件,那么如何使用它们呢?这样依赖于你的应用程序,一般你会有一些像下面这样的典型代码:

PLUGINLIST& pl = theApp.m_plugins.m_objects;
for (PLUGINLIST::iterator it = pl.begin(); it!=pl.end(); it++) {
Object* obj = *it;
ITextPlugin* plugin = dynamic_cast(obj);
plugin->DoSomething();
}
}
  (PLUGINLIST 是一个 typedef,用于 vector>)。PGEdit 的 CMainFrame::OnCreate 函数有一个类似这样的循环,添加每个插件的 MenuName 到 PGEdit 的 Edit 菜单。CMainFrame 指定命令 IDs 从 IDC_PLUGIN_BASE 开始。Figure 7 示范了视图是如何使用 ON_COMMAND_RANGE 来处理命令的。具体细节请下载源代码。

void CMyView::OnPluginCmdUI(CCmdUI* pCmdUI)
{
CEdit& edit = GetEditCtrl();
int begin,end;
edit.GetSel(begin,end);
pCmdUI->Enable(begin!=end);
}
  我已展示了 PGEdit 是如何加载和存取插件的,但你要如何实现插件呢?那是很容易的事情。首先生成一个定义接口的程序集——本文的例子中就是 TextPlugin.dll。该程序集不实现任何代码或类,仅仅定义接口。记住,.NET 是语言中立的,所以没有源代码,与 C++ 头文件完全不同。相反,你生成定义接口的程序集并将它分发给编写插件的开发人员。插件与该程序集链接,于是他们从你提供的接口派生。例如,下面的 C# 代码:

using TextPlugin;
public class MyPlugin : ITextPlugin
{
... // implement ITextPlugin
}
  Figure 8 展示了用 C# 编写的 PluginCaps 插件。正像你所看到的,它十分简单。有关细节请参考本文的源代码。

顺祝编程愉快!



<< Home

This page is powered by Blogger. Isn't yours?