程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 編寫、加載和存取插件程序(Plug-Ins)

編寫、加載和存取插件程序(Plug-Ins)

編輯:關於VC++

在 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 <mscorlib.dll>
#using <MyLib.dll>
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<Object* > 類型的數組。如果你在 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<ITextPlugin*>(obj);
  plugin->DoSomething();
  }
}

(PLUGINLIST 是一個 typedef,用於 vector<gcroot<Object*>>)。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 插件。正像你所看到的,它十分簡單。有關細節請參考本文的源代碼。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved