天天看點

用C++實作插件體系結構

本文讨論一種簡單卻有效的插件體系結構,它使用C++,動态連結庫,基于面向對象程式設計的思想。

首先來看一下使用插件機制能給我們帶來哪些方面的好處,進而在适當時候合理的選擇使用。

1, 增強代碼的透明度與一緻性:因為插件通常會封裝第三方類庫或是其他人編寫的代碼,需要清晰地定義出接口,用清晰一緻的接口來面對所有事情。你的代碼也不會被轉換程式或是庫的特殊定制需求弄得亂七糟。

2, 改善工程的子產品化:你的代碼被清析地分成多個獨立的子產品,可以把它們安置在子工程中的檔案組中。這種解耦處理使得建立出的元件更加容易重用。

3, 更短的編譯時間:如果僅僅是為了解釋某些類的聲明,而這些類内部使用了外部庫,編譯器不再需要解析外部庫的頭檔案了,因為具體實作是以私有的形式完成。

4, 更換與增加元件:假如你需要向使用者釋出更新檔,那麼更新單獨的插件而不是替代每一個安裝了的檔案更為有效。當使用新的渲染器或是新的單元類型來擴充你的遊戲時,能過向引擎提供一組插件,可以很容易的實作。

5, 在關閉源代碼的工程中使用GPL代碼:一般,假如你使用了GPL釋出的代碼,那麼你也需要開放你的源代碼。然而,如果把GPL元件封裝在插件中,你就不必釋出插件的源碼。

介紹

先簡單解釋一下什麼是插件系統以及它如何工作:在普通的程式中,假如你需要代碼執行一項特殊的任務,你有兩種選擇:要麼你自己編寫,要麼你尋找一個已經存在的滿足你需要的庫。現在,你的要求變了,那你隻好重寫代碼或是尋找另一個不同的庫。無論是哪種方式,都會導緻你架構代碼中的那些依賴外部庫的代碼重寫。

現在,我們可以有另外一種選擇:在插件系統中,工程中的任何元件不再束縛于一種特定的實作(像渲染器既可以基于OpenGL,也可以選擇Direct3D),它們會從架構代碼中剝離出來,通過特定的方法被放入動态連結庫之中。

所謂的特定方法包括在架構代碼中建立接口,這些接口使得架構與動态庫解耦。插件提供接口的實作。我們把插件與普通的動态連結庫區分開來是因為它們的加載方式不同:程式不會直接連結插件,而可能是在某些目錄下查找,如果發現便進行加載。所有插件都可以使用一種共同的方法與應用進行聯結。

常見的錯誤

一些程式員,當進行插件系統的設計時,可能會給每一個作為插件使用的動态庫添加一個如下函數類似的函數:PluginClass *createInstance(const char*);

然後它們讓插件去提供一些類的實作。引擎用期望的對象名對加載的插件逐個進行查詢,直到某個插件傳回,這是典型的設計模式中“職責鍊”模式的做法。一些更聰明的程式員會做出新的設計,使插件在引擎中注冊自己,或是用定制的實作替代引擎内部預設實作:

Void dllStartPlugin(PluginManager &pm);

Void dllStopPlugin(PluginManager &pm);

第一種設計的主要問題是:插件工廠建立的對象需要使用reinterpret_cast<>來進行轉換。通常,插件從共同基類(這裡指PluginClass)派生,會引用一些不安全的感覺。實際上,這樣做也是沒意義的,插件應該“默默”地響應輸入裝置的請求,然後送出結果給輸出裝置。

在這種結構下,為了提供相同接口的多個不同實作,需要的工作變得異常複雜,如果插件可以用不同名字注冊自己(如Direct3DRenderer and OpenGLRenderer),但是引擎不知道哪個具體實作對使用者的選擇是有效的。假如把所有可能的實作清單寫死到程式中,那麼使用插件結構的目的也沒有意義了。

假如插件系統通過一個架構或是庫(如遊戲引擎) 實作,架構師也肯定會把功能暴露給應用程式使用。這樣,會帶來一些問題像如何在應用程式中使用插件,插件作者如何引擎的頭檔案等,這包含了潛在的三者之間版本沖突的可能性。

單獨的工廠

接口,是被引擎清楚定義的,而不是插件。引擎通過定義接口來指導插件做什麼工作,插件具體實作功能。我們讓插件注冊自己的引擎接口的特殊實作。當然直接建立插件實作類的執行個體并注冊是比較笨的做法。這樣使得同一時刻所有可能的實作同時存在,占用記憶體與CPU資源。解決的辦法是工廠類,它唯一的目的是在請求時建立另外類的執行個體。如果引擎定義了接口與插件通信,那麼也應該為工廠類定義接口:

template<typename Interface>

class Factory {

  virtual Interface *create() = 0;

};

class Renderer {

  virtual void beginScene() = 0;

  virtual void endScene() = 0;

typedef Factory<Renderer> RendererFactory;

選擇1: 插件管理器

接下來應該考慮插件如何在引擎中注冊它們的工廠,引擎又如何實際地使用這些注冊的插件。一種選擇是與存在的代碼很好的接合,這通過寫插件管理器來完成。這使得我們可以控制哪些元件允許被擴充。 

class PluginManager {

  void registerRenderer(std::auto_ptr<RendererFactory> RF);

  void registerSceneManager(std::auto_ptr<SceneManagerFactory> SMF);

當引擎需要一個渲染器時,它會通路插件管理器,看哪些渲染器已經通過插件注冊了。然後要求插件管理器建立期望的渲染器,插件管理器于是使用工廠類來生成渲染器,插件管理器甚至不需要知道實作細節。

插件由動态庫組成,後者導出一個可以被插件管理器調用的函數,用以注冊自己:

void registerPlugin(PluginManager &PM);

插件管理器簡單地在特定目錄下加載所有dll檔案,檢查它們是否有一個名為registerPlugin()的導出函數。當然也可用xml文檔來指定哪些插件要被加載。 

選擇 2: 完整地內建Fully Integrated

除了使用插件管理器,也可以從頭設計代碼架構以支援插件。最好的方法是把引擎分成幾個子系統,建構一個系統核心來管理這些子系統。可能像下面這樣:

class Kernel {

  StorageServer &getStorageServer() const;

  GraphicsServer &getGraphicsServer() const;

class StorageServer {

  //提供給插件使用,注冊新的讀檔器

  void addArchiveReader(std::auto_ptr<ArchiveReader> AL);

  // 查詢所有注冊的讀檔器,直到找到可以打開指定格式的讀檔器

  std::auto_ptr<Archive> openArchive(const std::string &sFilename);

class GraphicsServer {

  // 供插件使用,用來添加驅動

  void addGraphicsDriver(std::auto_ptr<GraphicsDriver> AF);

  // 擷取有效圖形驅動的數目

  size_t getDriverCount() const;

 //傳回驅動

  GraphicsDriver &getDriver(size_t Index);

這裡有兩個子系統,它們使用” Server”作為字尾。第一個Server内部維護一個有效圖像加載器的清單,每次當使用者希望加載一幅圖檔時,圖像加載器被一一查詢,直到發現一個特定的實作可以處理特定格式的圖檔。另一個子系統有一個GraphicsDrivers的清單,它們作為Renderers的工廠來使用。可以是Direct3DgraphicsDriver或是OpenGLGraphicsDrivers,它們分别負責Direct3Drenderer與OpenGLRenderer的建立。引擎提供有效的驅動清單供使用者選擇使用,通過安裝一個新的插件,新的驅動也可以被加入。

版本

在上面兩個可選擇的方法中,不強制要求你把特定的實作放到插件中。假如你的引擎提供一個讀檔器的預設實作,以支援自定義檔案包格式。你可以把它放到引擎本身,當StorageServer 啟動時自動進行注冊。

現在還有一個問題沒有讨論:假如你不小心的話,與引擎不比對(例如,已經過時的)插件會被加載。子系統類的一些變化或是插件管理器的改變足以導緻記憶體布局的改變,當不比對的插件試圖注冊時可能發生沖突甚至崩潰。比較讨厭的是,這些在調試時難與發現。 幸運的是,辨認過時或不正确的插件非常容易。最可靠的是方法是在你的核心系統中放置一個預處理常量。任何插件都有一個函數,它可以傳回這個常量給引擎:

// Somewhere in your core system

#define MyEngineVersion 1;

// The plugin

extern int getExpectedEngineVersion() {

  return MyEngineVersion;

}

在這個常量被編譯到插件後,當引擎中的常量改變時,任何沒有進行重新編譯的插件它的 getExpectedEngineVersion ()方法會傳回以前的那個值。引擎可以根據這個值,拒絕加載不比對的插件。為了使插件可以重新工作,必須重新編譯它。當然,最大的危險是你忘記了更新常量值。無論如何,你應該有個自動版本管理工具幫助你。