程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C/C++:構建你自己的插件框架

C/C++:構建你自己的插件框架

編輯:C++入門知識

 

本文譯自Gigi Sayfan在DDJ上的專欄文章。Gigi Sayfan是北加州的一個程序員,email:[email protected].

本文是一系列討論架構、開發和部署C/C++跨平台插件框架的文章的第一篇。第一部分探索了一下現狀,調查了許多現有的插件/組件庫,深入研究了二進制兼容問題,並展現了一些該方案必要的一些屬性。

後續的文章用一個例子展示了可用於Window、Linux、Mac OS X並易於移植到其他系統的,具有工業級強度的插件框架。與其他類似框架相比,該框架有一些獨一無二的屬性,並且被設計為靈活、高效、易於編程、易於創建新插件,且同時支持C和C++。同時還提供了多種部署選項(靜態或動態庫)。

我將開發一個簡單的角色扮演游戲,可以自己增加非玩家角色的插件。游戲的引擎加載插件並無逢地集成他們。游戲展示了這些概念並且展示能夠實際運行的代碼。

誰需要插件?

插件是你想開發一個成功的動態系統所需要的一種方式。基於插件的擴展性是當前擴展&進化一個系統的最具有實踐意義的安全方式。插件使得第三方開發人員可以為系統做增值工作,也可以使其他開發人員增加新的功能而不破壞現有的核心功能。插件能夠促進將關注點分開,保證隱藏實現細節,將測試獨立開來,並最具有實踐意義。

類似Eclipse的平台實際上就是一個所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java開發環境)僅僅是一系列掛在核心框架上的插件。

 

為什麼選擇C++

眾所周知,當用於插件時,C++不是一個容易適應新環境的東西。它非常依賴於編譯器和平台。C++標准沒有指定任何應用程序二進制接口,這說明由不同的編譯器編譯出的庫甚至不同版本的庫是不兼容的。加上C++沒有動態加載的概念,且每個平台提供了自己的與其他平台不兼容的解決方案,你就能夠了解。有少許重量級的解決方案試圖說明不僅僅是插件和對一些額外的運行時的支持的依賴。但當要求高性能系統時,C/C++依然是僅有的實際可行的選項。

 

那裡有什麼?

在著手一個全新的框架之前,檢查現有的庫或者框架是值得的。我發現既有重量級的解決方案,如M$的COM和Mozilla的XPCOM(Cross-platform COM),或者只提供相當基礎功能的如QT的插件以及少許關於創建C++插件的文章。一個有趣的庫,DynObj,聲稱能解決二進制兼容的問題(在相同的約束下)。也有一個由Daveed Vandervoorde提出,作為一個原生的概念給C++添加插件的提案。那是一個有趣的讀物,但感覺怪怪的。

 

沒有一個基礎的解決方案闡述了與創建工業級強度的基於插件的系統相關的大量的問題,如錯誤處理,數據類型,版本控制,與框架代碼以及應用代碼的分離。在進入解決方案前,讓我們理解這個問題。

 

二進制兼容問題

再次強調,沒有標准的C++ ABI。不同的編譯器(甚至同一編譯器的不同版本)產生不同的目標文件和庫。最明顯的表現是,不同編譯器實現不同的name mangling(譯注:這個術語我沒有找到合適的翻譯,意思是編譯時給函數名字加上一些標識,功能之一就是區分重載函數)。這表明,通常情況下,你只能鏈接完全由同一個編譯器(版本號也要相同)編譯出來的目標文件。甚至有很多編譯器沒有完全實現C++98標准中的功能。

 

對於這個問題有一些局部的解決方案。例如,如果你訪問一個C++對象時僅僅是通過虛擬指針(譯注:不知道說的是什麼意思,很費解,原文如下:if you access a C++ object only through a virtual pointer and call only its virtual methods you sidestep the name mangling issue)並只調用其虛函數九可以回避name mangling問題。然而,不能保證所有編譯器編譯出來的代碼運行時在內存中有相同的VTABLE布局,盡管它更穩定(譯注:應該指的是VTABLE在內存中的布局各編譯器的實現更傾向於一致)。

 

如果你試圖動態加載C++代碼將面對另一個問題——沒有直接的方法從Linux或者Mac OS X的動態庫來加載並實例化C++的類(在Windows上,VC++支持)。

 

解決方案是使用一個具有C linkage的函數(因此編譯器不會對其進行name mangling操作)作為一個工廠方法,來返回一個透明的handle給調用方。然後調用方將其轉換成正確的類(通常是一個純抽象基類)。當然,這需要一些協作,而且僅當應用和庫所用編譯器的VTABLE內存布局一致時才能工作。

 

解決兼容性的終極方法就是忘記C++,並使用純C的API。在實際中,C對於所有的編譯器實現都是兼容的。後面我會戰士如何在C的兼容性基礎上達成C++編程模型。

 

基於插件的系統的體系結構

一個基於插件的系統可以分成三個部分:

 

領域相關系統(譯注:應用程序的邏輯部分)

插件管理器

插件

領域相關系統通過插件管理器加載插件並創建其對象。一旦創建了插件對象且系統有某種指針或引用指向它,它就可以像其他對象一樣使用。我們將看到,這通常會需要一些特殊的析構/清除工作。

 

插件管理器是相當通用的一段代碼。它管理插件的生命期並且將他們暴露給主系統。它能找到並加載插件,初始化它們,注冊工廠函數並能夠卸載插件。它還應當能夠讓主系統遍歷已加載的插件或注冊的插件對象。

 

插件自身需要順應插件管理器的協議並提供適應主系統期望的對象。

 

實際上,你很少會看見如此清晰的分解(總之,在基於C++的插件系統上如此)。插件管理器經常與領域相關系統緊密耦合。這是有很好的原因的。插件管理器需要提供某種類型的插件對象的最終實例。而且,插件的初始化經常需要傳遞一些領域相關的信息和/或回調函數/服務。這可以由通用插件管理器輕松地做到。

 

插件部署模型

插件通常以動態庫的形式部署。動態庫允許插件的很多優勢如熱交換(重新加載一個插件的新實現而無需關閉系統),而且需要更少的鏈接時間。然而,在某些情況下靜態庫是插件的最好選擇。例如,僅僅因為某些系統不支持動態庫(很多嵌入式系統)。在其他的情況下,出於安全考慮,不允許加載陌生的代碼。有時,核心系統會與一些預先加載好插件一起部署,而且靜態加載到主系統中使得它們更健壯(因此用戶不會無意中刪除它們)。

 

底線是,好的插件系統應當同時支持靜態和動態插件。這可以讓你在不同的環境下,不同的約束下部署同一個基於插件的系統。

 

插件編程接口

所以關於插件的問題都是關於接口(譯注:要注意這裡說的接口,不是C#和JAVA的接口概念,理解為signature更合適)的。基於插件的系統的基本觀念是:有某個中央系統,通過定義良好的接口和協議,其在加載插件時不知道任何關於與插件通信的問題。

 

定義一系列函數作為插件導出的接口(動態庫及靜態庫)是幼稚的方法。這種方法在技術上是可行的,但在概念上是有瑕疵的(譯注:作者說話分量還是輕些)。原因是,插件應當支持兩種接口且只能有一套從插件導出的函數。這表明這兩種接口會被混合在一起。

 

第一層接口(及協議)是通用的插件接口。它使得中央系統可以初始化插件,並使插件可以在中央系統中注冊一系列的用於創建和銷毀對象以及全局的清理函數。通用插件接口不是與領域相關的,且可以被指定和實現為可服用的庫。第二層接口是由插件對象實現的功能性的接口。該接口是領域相關的,且世紀的插件必須非常謹慎的對其進行設計和實現。中央系統應當知道該接口並能通過其與插件對象進行交互。

 

列表1是一個指定了通用插件接口的頭文件。沒有深入細節並解釋所有事情之前,讓我們看看它提供了什麼。

 

#ifndef PF_PLUGIN_H

 

#define PF_PLUGIN_H

 

 

 

#include <apr-1/apr_general.h>

 

 

 

#ifdef __cplusplus

 

extern "C" {

 

#endif

 

 

 

typedef enum PF_ProgrammingLanguage

 

{

 

  PF_ProgrammingLanguage_C,

 

  PF_ProgrammingLanguage_CPP

 

} PF_ProgrammingLanguage;

 

 

 

struct PF_PlatformServices_;

 

 

 

typedef struct PF_ObjectParams

 

{

 

  const apr_byte_t * objectType;

 

  const struct PF_PlatformServices_ * platformServices;

 

} PF_ObjectParams;

 

 

 

typedef struct PF_PluginAPI_Version

 

{

 

  apr_int32_t major;

 

  apr_int32_t minor;

 

} PF_PluginAPI_Version;

 

 

 

typedef void * (*PF_CreateFunc)(PF_ObjectParams *);

 

typedef apr_int32_t (*PF_DestroyFunc)(void *);

 

 

 

typedef struct PF_RegisterParams

 

{

 

  PF_PluginAPI_Version version;

 

  PF_CreateFunc createFunc;

 

  PF_DestroyFunc destroyFunc;

 

  PF_ProgrammingLanguage programmingLanguage;

 

} PF_RegisterParams;

 

 

 

typedef apr_int32_t (*PF_RegisterFunc)(const apr_byte_t * nodeType, const PF_RegisterParams * params);

 

typedef apr_int32_t (*PF_InvokeServiceFunc)(const apr_byte_t * serviceName, void * serviceParams);

 

 

 

typedef struct PF_PlatformServices

 

{

 

  PF_PluginAPI_Version version;

 

  PF_RegisterFunc registerObject;

 

  PF_InvokeServiceFunc invokeService;

 

} PF_PlatformServices;

 

 

 

typedef apr_int32_t (*PF_ExitFunc)();

 

 

 

typedef PF_ExitFunc (*PF_InitFunc)(const PF_PlatformServices *);

 

 

 

#ifndef PLUGIN_API

 

  #ifdef WIN32

 

    #define PLUGIN_API __declspec(dllimport)

 

  #else

 

    #define PLUGIN_API

 

  #endif

 

#endif

 

 

 

extern

 

#ifdef  __cplusplus

 

"C"

 

#endif

 

PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params);

 

 

 

#ifdef  __cplusplus

 

}

 

#endif

 

#endif /* PF_PLUGIN_H */

 

列表1

 

首先你應當注意到這是一個C文件。這允許插件框架可以由純C系統編譯使用並可用來寫純C插件。但是,它不僅僅局限在C上,且實際上大多數情況下用在C++中。

 

枚舉類型PF_ProgrammingLanguage允許插件聲明到用C++實現的插件管理器中。

 

PF_ObjectParams是一個抽象的結構體,創建插件時用於傳遞參數給插件對象。

 

PF_PluginAPI_Version被用於商討版本問題,並保證插件管理器只加載合適版本的插件。

 

函數指針PF_CreateFunc和PF_DestroyFunc(由插件來實現)允許插件管理器來創建和銷毀插件對象(每個插件注冊這樣的函數到插件管理器中。)

 

PF_RegisterParams結構體包含初始化時插件必須提供給插件管理器的所有信息。(版本信息,創建/銷毀函數,編程語言)

 

PF_RegisterFunc(由插件管理器實現)允許每個插件為每種它所支持的對象類型注冊一個PF_RegisterParams結構體。注意這個方案允許一個插件注冊一個對象的不同版本和多個對象類型。

 

PF_InvokeService函數指針是一個通用的函數,查檢可以用其來調用主系統提供的服務如日志、事件通知及錯誤報告。其簽名(signature)包括服務名稱和指向一個參數結構體不透明的指針。插件應當知道可用的服務以及如何調用它們(或者,如果你希望使用PF_InvokeService,你可以自己實現服務。)

 

PF_PlatformServices結構體聚集了我剛剛提到所有的由平台提供給插件的服務(版本,注冊對象,執行服務函數)。該結構體在初始化時傳遞給每個插件。

 

PF_ExitFunc定義了插件退出函數,由插件來實現。

 

PF_InitFunc定義了插件初始化函數指針。

 

PF_initPlugin是動態插件(由動態鏈接庫/共享庫來部署的插件)實際的初始化函數的簽名(signature)。從動態插件中導出它的名字,因此插件管理器可以在加載插件時調用它。它接受一個指向PF_PlatformServices結構體的指針,因此所有的服務在初始化時立刻可用(這是注冊對象的正確時機)並返回一個指向退出函數的指針。

 

注意靜態插件(實現在靜態庫中且直接連接到主執行體中)應當實現一個有C linkage的init函數,但禁止將其命名為PF_initPlugin。原因是如果有多個靜態插件,他們都將有一個同樣的初始化函數名字,你的編譯器痛恨這個。

 

靜態插件的初始化有所不同。他們必須顯式地由主執行體初始化。主執行體將通過PF_InitFunc的簽名調用它們的初始化函數。很不幸,這意味著每當一個新的靜態插件加入/移出系統時,主執行體需要被修改,並且各種各樣的init函數的名字必須是對等的(coordinated)。

 

有一種試圖解決該問題的技術叫做“自動注冊”。自動注冊通過靜態庫中一個全局的對象來達到目的。該對象在main()事件啟動之前被構建。該全局對象可以請求插件管理器來初始化靜態插件(通過傳遞插件的init()函數指針)。不幸的是,這種方案在VC++中不能工作。

 

撰寫插件

撰寫插件意味著什麼?插件框架是非常generic,並且不提供任何可以與你的應用交互的切實的對象。你必須在插件框架上構建你自己的應用程序模型。這意味著你的應用程序(加載插件)以及插件自身必須同意並通過某種模型來協作。通常這表明應用程序期待插件提供暴露某種特定API的某種類型的對象。插件框架將提供注冊、枚舉及加載這些對象的基礎設施。示例1是一個叫做IActor的C++接口的定義。它有兩個操作——getInitialInfo()和play()。注意該接口不是充分的,因為getInitialInfo()期望一個指向名為ActorInfo的結構體的指針,且play()期望一個指向另一個叫做ITurn接口的指針。這是實際的一個案例,你必須設計並指定整個對象模型。

 

struct IActor

 

{

 

  virtual ~IActor() {}

 

  virtual void getInitialInfo(ActorInfo * info) = 0;

 

  virtual void play( ITurn * turnInfo) = 0;

 

};

 

示例1

 

每個插件可以注冊多個實現了IActor接口的類型。當應用程序決定示例化一個由插件注冊的對象,它將調用注冊的,由插件實現的PF_CreateFunc函數。插件負責創建一個合適的對象並將其返回給應用程序。返回類型指定為void *是因為對象創建操作是通用插件框架的一部分,該部分不知道任何關於特定IActor接口的信息。應用程序隨後將void *轉換到IActor *,然後就可以在整個接口中使用它,好像它是一個正常的對象。當應用程序使用完IActor對象後,它執行注冊的由插件實現的PF_DestroyFunc函數,然後插件銷毀actor對象。目前不好考慮虛擬的析構函數,我會在後面的部分討論它。

 

編程語言支持

在二進制兼容性部分我解釋了你可以利用C++的vtable一級的兼容性,如果你的編譯器滿足的話。你也可以使用C一級的兼容性,這樣你就可以使用不同的編譯器來構建應用程序和插件,但你將被局限在C的交互上。你的應用程序對象模型必須是基於C的。你不能使用好的C++接口如IACTOR,但你必須設計一個相似的C接口。

 

純C

在純C的編程模型中你只需要用C開發插件。當你實現PF_CreateFunc函數時你返回一個在你的應用程序C對象模型中與其它C對象交互的C對象。所有的話題都是關於C對象和C對象模型的。所有人都知道C是一個過程語言,沒有對象的概念。然而C提供了足夠的抽象機制來實現對象以及多態(在此處是必須的)並支持面向對象的編程泛型。實際上,最初的C++編譯器是一個C編譯器的事實上的一個前端(front-end)。它根據C++代碼產生C代碼,然後使用一個普通的C編譯器來編譯該C代碼。它的名字Cfront說明了一切。

 

使用包含函數指針的結構體(譯注:就可以獲得OO特性)。每個函數的簽名應當接受它所屬結構體作為第一個參數該結構體也可以包含其它的數據成員。這樣提供了(與C++類有關的簡單的土語)如:封裝(狀態和行為捆綁)、繼承(通過將基結構體的對象作為第一個數據成員)以及多態(通過設置不同的函數指針。)(譯注:沒錯,這就是用C來編寫OO程序的基本要求和方法,我也用C寫過OO程序)。

 

C不支持析構函數、函數及操作符重載,名字空間,因此你定義接口時只有很少的選項。這也許是“塞翁失馬,焉知非福”,因為接口應該被可能掌握C++另一個子集的其它人所使用。減少語言的范圍可能會提升你的接口的簡單性和可用性。

 

我將在插件框架的後續文章中探究OO的C。列表2包含了陪伴該文章系列(僅僅是投你所好)的示例游戲的C對象模型。如果你快速浏覽一下你會看見它甚至支持集合以及遍歷。

 

#ifndef C_OBJECT_MODEL

 

#define C_OBJECT_MODEL

 

 

 

#include <apr-1/apr.h>

 

 

 

#define MAX_STR 64 /* max string length of string fields */

 

 

 

typedef struct C_ActorInfo_

 

{

 

  apr_uint32_t id;

 

  apr_byte_t   name[MAX_STR];

 

  apr_uint32_t location_x;

 

  apr_uint32_t location_y;

 

  apr_uint32_t health;

 

  apr_uint32_t attack;

 

  apr_uint32_t defense;

 

  apr_uint32_t damage;

 

  apr_uint32_t movement;

 

} C_ActorInfo;

 

 

 

typedef struct C_ActorInfoIteratorHandle_ { char c; } * C_ActorInfoIteratorHandle;

 

typedef struct C_ActorInfoIterator_

 

{

 

  void (*reset)(C_ActorInfoIteratorHandle handle);

 

  C_ActorInfo * (*next)(C_ActorInfoIteratorHandle handle);

 

 

 

  C_ActorInfoIteratorHandle handle;

 

} C_ActorInfoIterator;

 

 

 

typedef struct C_TurnHandle_ { char c; } * C_TurnHandle;

 

typedef struct C_Turn_

 

{

 

  C_ActorInfo * (*getSelfInfo)(C_TurnHandle handle);

 

  C_ActorInfoIterator * (*getFriends)(C_TurnHandle handle);

 

  C_ActorInfoIterator * (*getFoes)(C_TurnHandle handle);

 

 

 

  void (*move)(C_TurnHandle handle, apr_uint32_t x, apr_uint32_t y);

 

  void (*attack)(C_TurnHandle handle, apr_uint32_t id);

 

 

 

  C_TurnHandle handle;

 

} C_Turn;

 

 

 

typedef struct C_ActorHandle_ { char c; } * C_ActorHandle;

 

typedef struct C_Actor_

 

{

 

  void (*getInitialInfo)(C_ActorHandle handle, C_ActorInfo * info);

 

  void (*play)(C_ActorHandle handle, C_Turn * turn);

 

 

 

  C_ActorHandle handle;

 

} C_Actor;

 

 

 

#endif

 

列表2

 

純C++

在純C++編程模型中你僅僅需要用C++開發你的插件。插件編程接口函數可以被實現為靜態成員函數或者普通的靜態/全局函數(畢竟C++主要是C的超集)。(這句不好翻啊:The object model can be your garden variety C++ object model.) 列表3包含示例游戲的C++對象模型。它基本上與列表2種的C對象模型相似。

 

#ifndef OBJECT_MODEL

 

#define OBJECT_MODEL

 

 

 

#include "c_object_model.h"

 

 

 

typedef C_ActorInfo ActorInfo;

 

 

 

struct IActorInfoIterator

 

{

 

  virtual void reset() = 0;

 

  virtual ActorInfo * next() = 0;

 

};

 

 

 

struct ITurn

 

{

 

  virtual ActorInfo * getSelfInfo() = 0;

 

  virtual IActorInfoIterator * getFriends() = 0;

 

  virtual IActorInfoIterator * getFoes() = 0;

 

 

 

  virtual void move(apr_uint32_t x, apr_uint32_t y) = 0;

 

  virtual void attack(apr_uint32_t id) = 0;

 

};

 

 

 

struct IActor

 

{

 

  virtual ~IActor() {}

 

  virtual void getInitialInfo(ActorInfo * info) = 0;

 

  virtual void play( ITurn * turnInfo) = 0;

 

};

 

 

 

#endif

 

列表3

 

C/C++二重奏

在這個編程模型中你可以使用C或者C++來開發插件。當你注冊你的對象時要指定編程語言。如果你創建一個平台並且你想提供給第三方開發者最終的自由是他們可以選擇自己的編程語言及模型,混合並匹配C和C++插件時,這個模型將非常有用。

 

插件框架支持它,但實際的工作在於為你的應用設計一個既支持C又支持C++對象模型。每個對象類型需要同時實現C和C++的接口。這意味著你將有一個有著標准VTABLE布局的C++類以及一系列與虛擬方法相關的函數指針。這種結構非常復雜,我將不演示它。

 

要注意的是從插件開發人員的角度來說這種方法不會帶來額外的復雜性。他們永遠可以使用C或C++接口來開發C或C++的插件。

 

C/C++混合體

在該模型中,你必須在C對象模型的蓋子之下用C++開發插件。這就包括了C++包裹類的創建,該包裹類實現了C++對象模型並包裹(wrap)相應C對象的。插件開發人員在這層上編程,將每個調用、參數及返回值在C和C++之間翻譯。當實現你的應用程序對象模型時這需要額外的工作,但通常很直接。好處是對於插件開發這來說提供了一個有著完整C級兼容性的好的C++編程模型。我不會在示例游戲的上下文中演示它。

 

語言-鏈接矩陣

圖1顯示了各種不同的部署模型組合的利與弊(靜態庫vs. 動態庫)以及編程語言的選擇(C vs. C++)。

 

 


\

 

圖1

 

為了本次討論,如果使用C++插件,C/C++二重奏模型有C++限制和所需的先決條件,對於C插件,有C的限制和所需的先決條件。而且,C/C++混合模型不過是C模型,因為C++層被隱藏在插件實現後面。這些都讓人迷惑,但底線是你有選項了,且插件框架允許你做出自己的決定,采用你自己認為合適的折中。它沒有強迫你使用某個特定的模型,也沒有瞄准最小公分母。

 

Copyleft (C) 2007, 2008 raof01. 本文可以用於除商業用途外的所有用途。若用於非商業用途,請保留此權利聲明,並以超鏈接形式標明文章原始出版、作者信息和本聲明;若要用於商業用途,請與作者聯系,否則作者將使用法律來保證權利。

 

 

 

本文是關於開發跨平台C++插件系列的第二篇。第一篇詳細描述了問題,探索了一些解決方案,並介紹了插件框架。本部分描述了架構以及構件在插件框架上,基於插件的系統的設計,插件的生命期,以及通用插件框架的內部。小心:代碼遍布文章各個部分。

基於插件系統的架構  基於插件的系統可以分廠三個松散耦合的部分:有自己特有對象模型的主系統或應用;插件管理器;以及插件本身。插件遵從插件管理器的接口和協議,並實現對象模型接口。讓我們用一個實際的例子來展示。主系統是一個基於回合的游戲。游戲發生在一個有著各種各樣怪獸的戰場上。英雄與怪獸搏斗知道他或者所有的怪獸死掉。列表以是英雄類的定義:

#ifndef HERO_H

#define HERO_H

#include <vector>

#include <map>

#include <boost/shared_ptr.hpp>

#include "object_model/object_model.h"

 

class Hero : public IActor

{

public:

  Hero();

  ~Hero();

  // IActor methods

 

  virtual void getInitialInfo(ActorInfo * info);

  virtual void play(ITurn * turnInfo);

private:

};

#endif

Listing one

 

BattleManager是驅動該游戲的引擎。它負責初始化hero和monster並且將他們安置在戰場上。然後每個回合中,它通過調用每個actor(Hero或者Monster)的play()方法來攻擊。

Hero和monster實現了IActor接口。Hero是一個內建的,有著預訂義行為的游戲對象。另一方面,monster是由插件來實現的。這就允許游戲能夠擴展為更多的新monster,並將新的monster的開發和游戲引擎的開發分離開來。PluginManager的工作就是抽象掉monster是由插件來產生並將他們展現給BattleManager 的事實,就像hero。這個方案允許一些內建的monster隨同游戲一起發布,這些monster被靜態鏈接進去,不是用插件實現的。BattleManager 甚至不應該知道有插件這樣一回事。它應當在C++對象一級進行操作。這也使得其非常易於測試,因為你可以在測試代碼中創建一些假的monster而不用寫一個完整的插件。

PluginManager本身可以是通用的也可以是特定的。 通用的插件管理器不知道特定的底層對象模型。當一個C++的PluginManager實例化一個在插件中實現的新對象,它必須返回一個通用的接口,這樣調用方必須將該實例轉換成實際的接口。這看起來是有點丑,但很有必要。一個定制的插件管理器知道你的對象模型並且能夠從底層的對象模型方面來操作。例如,一個為我們的游戲定制的PluginManager可以有返回IActor 接口的CreateMonster()方法。我所展示的PluginManager是通用的,但我將展示將對象模型特定的一層放到它上面會有多麼簡單。這是標准的實踐,因為你不希望你的應用程序代碼來處理顯式類型轉換。

插件系統生命期

現在該弄明白插件系統的生命期了。應用程序,PluginManager以及插件根據一個嚴格的協議參與到這個復雜的活動中。好的消息是,通用的插件框架大部分都能很好的安置這些東西。當需要的時候,應用獲取插件的訪問權,插件僅僅需要實現一些到時候會被調用的函數。

靜態插件的注冊

由靜態庫部署並靜態鏈接到應用程序中的插件就是靜態插件。其注冊可以自動的完成,前提是庫定義了一個全局的注冊者對象並且該對象的構造函數被自動調用。然而它不能在所有的平台上工作,如M$的Windows。可選的方法是通過傳遞一個專門的初始化函數來顯式告訴PluginManager來初始化靜態插件。因為所有的靜態插件都靜態地鏈接到了主程序,因此每個init()都應當有一個惟一的名字且必須是PF_InitPlugin類型的。一個好的慣例是將其命名為:<Plugin Name>_InitPlugin()。下面是靜態插件的init()函數的原型:

extern "C" PF_ExitFunc

StaticPlugin_InitPlugin(const PF_PlatformServices * params)

顯式初始化在主程序和靜態插件之間創建了一個緊密耦合的關系,因為主程序需要在編譯期知道什麼插件被鏈接近來以便初始化他們。如果所有的靜態插件遵從某種約定從而使得構建(build)過程能夠找到他們並產生相應的初始化代碼,那麼這個過程可以作為構建(build)過程的一部分自動執行。

一旦初始化完成,靜態插件將會注冊其所有的對象類型到PluginManager中。

動態插件的加載

動態插件更普遍。他們應當全部由一個專門的目錄來部署。應用程序應該調用PluginManager的loadAll()方法並傳入該目錄的路徑。PluginManager掃描該目錄下所有的文件,並加載每個動態庫。應用程序也可以調用load()方法來加載單獨的插件。

插件初始化

一旦動態庫被成功加載,PluginManager就會查找被稱為RPF_initPluginS的函數入口點。如果找到該入口點,PluginManager 通過調用該函數並傳遞一個F_PlatformServices結構體來初始化它。該結構體包含PF_PluginAPI_Version以便插件與主程序進行版本上的協商並確定是否能夠正常工作。若應用程序的版本不合適,插件可能會使初始化失敗。PluginManager志記這個問題,然後繼續加載下一個插件。從PluginManager角度來說加載或初始化某個插件失敗不是一個嚴重的錯誤。應用程序可以執行一些額外的檢查來保證枚舉已加載的插件,這樣就能檢查是否有重要的插件沒有被加載。

 

Listing Two包含了PF_initPlugin函數:

#include "cpp_plugin.h"

#include "plugin_framework/plugin.h"

#include "KillerBunny.h"

#include "StationarySatan.h"

 

extern "C" PLUGIN_API apr_int32_t ExitFunc()

{

  return 0;

}

extern "C" PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params)

{

  int res = 0;

  PF_RegisterParams rp;

  rp.version.major = 1;

  rp.version.minor = 0;

  rp.programmingLanguage = PF_ProgrammingLanguage_CPP;

  // Regiater KillerBunny

 

  rp.createFunc = KillerBunny::create;

  rp.destroyFunc = KillerBunny::destroy;

  res = params->registerObject((const apr_byte_t *)"KillerBunny", &rp);

  if (res < 0)

    return NULL;

  // Regiater StationarySatan

 

  rp.createFunc = StationarySatan::create;

  rp.destroyFunc = StationarySatan::destroy;

  res = params->registerObject((const apr_byte_t *)"StationarySatan", &rp);

  if (res < 0)

    return NULL;

   return ExitFunc;

}

Listing Two

 

對象注冊

如果版本協商成功進行了,插件就應當把其支持的所有對象類型注冊到插件管理器中去。注冊的目的是為了提供給應用程序諸如PF_CreateFunc和PF_DestroyFunc的函數以便後續的使用。這個安排允許插件控制對象實際的創建和銷毀,包括他們所使用的資源如內存,但讓應用程序來控制對象的數量和它們的生命期。當然也可以使用singleton模式來返回同一個對象的實例。

通過為每個對象類型准備注冊記錄(PF_RegisterParams)並調用PF_PlatformServices (作為參數傳遞給PF_initPlugin)結構體裡registerObject()函數指針來完成注冊。registerObject() 接受一個能惟一標識對象類型的字符串或者“*”以及PF_RegisterParams struct。我將在下一節解釋類型字符串的目的以及如何使用。需要類型字符串的原因是不同的插件可能會支持多種不同的對象類型。

一旦插件調用registerObject()控制又回到了PluginManager。PF_RegisterParams包含了一個版本以及編程語言。版本使PluginManager 能夠保證它可以與該對象類型協同工作。版本不符將導致無法注冊。這不是個嚴重錯誤,這樣可以允許相當有彈性的協商。插件試圖注冊同一類型的多個版本以便能夠利用新的接口,且當新接口失敗時回到舊接口 。如果插件管理器對PF_RegisterParams滿意,它將該結構體存放到能夠映射對象類型到該結構體的內部數據結構中。

當插件注冊完其所有的對象類型,它返回一個指向PF_ExitFunc的指針。該函數在插件被卸載前調用使得插件可以清除在其生命期內獲得的所有資源。

若插件發現其不能正常工作,它將清除所有的資源並返回NULL。這樣PluginManager 就知道插件初始化失敗,並且會刪除失敗的插件所作的所有注冊

 

作者 weiqubo

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