1.為什麼需要外掛化系統
“程式設計就是構建一個一個自己的小積木, 然後用自己的小積木搭建大系統”。
但是程式還是會比積木要複雜, 我們的系統必須要保證小積木能搭建出大的系統(必須能被組合),有必須能使各個積木之間的耦合降低到最小。
傳統的程式結構中也是有模組的劃分,但是主要有如下幾個缺點:
a: c++二進位制相容
b: 模組對外暴露的東西過多,使呼叫者要關心的東西過多
c: 封裝的模組只是作為功能的實現者封裝,而不是介面的提供者
d: 可替換性和可擴充套件性差
而外掛式的系統架構就是為了解決這樣的問題。外掛化設計的優點?外掛化設計就是為了解決這些問題的,所以以上的缺點就是我們的優點
2.外掛化系統的原理
指導性原則:“面向介面程式設計而不是實現程式設計”
其介面的定義為interface, 其實轉換一下的意思是面向純虛類程式設計,當然也可以包裝成面向服務和元件程式設計。
如我可以這樣定義一個介面(interface)
interfacecptf IRole{ virtual cptf ::ulong getHealth() = 0; virtual cptf ::ulong getHurt() = 0; virtual wstring getName() = 0; };
外掛的目標就是實現IRole, 業務層的目標就是呼叫IRole, 業務層不知道IRole具體是如何實現的,而實現者也不用關心業務層是如何呼叫的。
3.外掛化系統的目標
1). 使用者能通過規範,開發自己的外掛,實用已有的外掛,外掛又能控制對外暴露的內容。
2). 執行時候能動態安裝、啟動、停在、解除安裝
3). 每一個外掛提供一個或多個服務,其他外掛是根據介面來獲取服務提供者
4. 一個外掛化系統應該是怎麼構成的
OSGI,Java中影響力最大的外掛化系統就是OSGI標準
OSGI的定義:The dynamic module system for java
借鑑osgi對外掛系統的定義,我認為一個典型的外掛系統應該有如下幾個方面構成:
“基礎庫+微核心+系統外掛+應用外掛”
其中微核心 負責如下功能:
1、 負責外掛的載入,檢測,初始化。
2、 負責服務的註冊。
3、 負責服務的呼叫。
4、 服務的管理。
5. 一個簡單場景的隨想
比如設計下如下的遊戲場景:一個RPG遊戲, 玩家控制一個英雄,在場景中有不同的怪物,而且隨著遊戲的更新,
英雄等級的提升又會有不同的怪物出現, 這裡就想把怪物設計為外掛。
首先工程是這樣的佈局的
首先要在做的是定義介面, 這裡我需要一個英雄的介面,有需要一個怪物的介面。
interfacecptf IHero : public cptf ::core:: IDispatch , public IRole { virtual cptf ::ulong attack() = 0; }; interfacecptf IOgre : public cptf ::core:: IDispatch , public IRole { };
然後作為外掛我需要實現一個Hero, 和多個Ogre
class Hero : public ServiceCoClass<Hero > , public ObjectRoot <SingleThreadModel> , public cptf ::core:: IDispatchImpl<IHero >{ class Wolf : public ServiceCoClass<Wolf > , public ObjectRoot<SingleThreadModel > , public cptf::core ::IDispatchImpl< IOgre> class Tiger : public ServiceCoClass<Tiger > , public ObjectRoot<SingleThreadModel > , public cptf::core ::IDispatchImpl< IOgre>
最後,在主工程用我要用到這些外掛
void BattleMannager ::run() { hero_ = static_cast<IHero *>(serviceContainer_. getService(Hero_CSID , IHero_IID)); if (!hero_ )return; printHero(hero_ ); list<IService *> services = serviceContainer_ .getServices( IOgre_IID); list<IOgre *> ogres = CastUtils::parentsToChildren <IService, IOgre>(services ); for_each(ogres .begin(), ogres.end (), bind(&BattleMannager ::printOgre, _1)); services = serviceContainer_ .getServices( IHumanOgre_IID); list<IHumanOgre *> hummanOgres = CastUtils::parentsToChildren <IService, IHumanOgre>(services ); for_each(hummanOgres .begin(), hummanOgres.end (), bind(&BattleMannager ::printHumanOgre, _1)); }
以上, 因為邏輯層和外掛實現層都已經好了, 整個流程也已經跑通,但是還是的疑問:服務是怎麼載入的?
6. 如何進行外掛的載入以及服務的註冊
借鑑OSGI, 我這裡把系統設計為bundle+service的組合。 bundle是service的容器,service是功能的具體實現者。
在windows下,bundle用dll來表示。
那bundle在windwos下載入就很簡單了LoadLibrary Api就行了
但是再c++中dll的介面還必須要考慮的一個問題就是c++的二進位制相容性:現在沒有標準的 C++ ABI。這意味著,不同編譯器(甚至同一編譯器的不同版本)會編譯出不同的目標檔案和庫。這個問題導致的最顯而易見的問題就是,不同編譯器會使用不同的名稱改寫演算法。這樣對外掛的介面來說是致命的。當然我們可以用c api來作為介面,但是這樣勢必會對整體的設計產生影響,而且作為一個裝B的c++程式設計師,我們怎麼能容忍要借用低階語言的特性來實現我們的功能呢。當然幸虧還有另外一種方式,那就是虛表。當然不是所有的c++編譯器對虛表的實現也是不一樣的(好吧~~),但是至少主流(多主流~~不能確定)的編譯器虛表都是在物件的第一個位置。好吧,現在決定用虛表來對外掛介面的實現了,所以我們就可以用這樣的方式來計算具體實現類的地址了
#define CPTF_PACKING 8 #define cptf_offsetofclass (base, derived) \ (( cptf::ulong )(static_cast< base*>((derived *)CPTF_PACKING))- CPTF_PACKING)
哇,好神奇的程式碼, 這個是為什麼呢。 這個就需要對c++記憶體物件模型需要深入得了解了,可能需要拜讀<c++記憶體物件模型>,這裡篇幅有限這裡就不解釋了。但是如果有看官想要問“你為什麼這麼天才能想出這樣的寫法?”,雖然我很想說我很天才,但是其實正是情況是我參考的atl中的原始碼,而且整個外掛載入過程我都是山寨了atl中的相關程式碼的。
但是還是有一個問題, 在GameMain中,認識的是IHero, 根本不知道有個Hero的實現,所有可能有這樣的程式碼IHero* hero = New Hero() 這樣動作。
那我們要如何進行這樣的new動作。 當然我們說Hero是在Role dll中的, 在dll被載入的時候可以new Hero, 然後把hero物件的地址放到某個堆中,標誌讓GameMain使用。作為一個轉換的偽設計人員, 我也是認為這樣會有效能問題的, 我不僅要做到載入, 還要做到懶載入。
那如何做到懶載入呢?
感謝微軟,在vc++中有機制幫我們做到,在其他的編譯器中也會有其他的實現,但是這裡我們只做了vc++中的實現。
首先宣告一個自己的段,段名可以叫cptf:
#pragma section ("CPTF$__a", read, shared ) #pragma section ("CPTF$__z", read, shared ) #pragma section ("CPTF$__m", read, shared )
然後在編譯的時候,把具體實現的類的Create函式地址放到這個段中
#define CPTF_OBJECT_ENTRY_AUTO (class) \ __declspec(selectany ) AutoObjectEntry __objMap_##class = {class::clsid (), class:: creatorClass_::createInstance }; \ extern "C" __declspec( allocate("CPTF$__m" )) __declspec(selectany ) AutoObjectEntry* const __pobjMap_ ##class = &__objMap_ ##class; \ CPTF_OBJECT_ENTRY_PRAGMA(class )
最後在載入的時候,變數這個段,如果csid命中,則呼叫Create方法
inline bool cptfModuleGetClassObject( const CptfServiceEntities * cpfgModel , const cptf::IID & csid , const cptf::IID & iid , void** rtnObj) { bool rtn (false); assert(cpfgModel ); for (AutoObjectEntry ** entity = cpfgModel->autoObjMapFirst_ ; entity != cpfgModel ->autoObjMapLast_; ++entity) { AutoObjectEntry* obj = *entity; if (obj == NULL) continue; if (obj ->crateFunc != NULL && csid == obj-> iid){ rtn = obj ->crateFunc( iid, rtnObj ); break; } } return rtn ; }
總結下流程:
1. GameMian使用的是IHero,
2. Hero是IHero的實現者,在編譯的規程中,把Create Hero的方法編譯到固定段中
3. GameMian進行new的時候其實呼叫的是Dll固定段中的函式地址
4. 利用 上面的cptf_offsetofclass 巨集實現對IHero的
7. 服務的管理
每一個服務都需要一個id來標誌它, 這裡就用guid, 命名為IID---interface id
每一個服務的實現者也必須要有id來標誌, 這也是一個guid, 命名為csid
我們把服務和服務實現者的管理資訊用配置檔案管理起來,services.xml, 對Hero的定義
<service> <bundle>Role.dll</bundle> <csid>500851c0-7c2a-11e3-8c28-bc305bacf447</csid> <description>hero</description> <name>Hero</name> <serviceId>99f9dd8f-7c1a-11e3-9f9d-bc305bacf447</serviceId> <serviceName>IHero</serviceName> </service>
當然一個外掛的管理器也是必須的, 管理Service的註冊,快取,析構、獲取,查詢等。這裡用ServiceContainer實現
8. 基於外掛的架構
基於外掛系統的架構:
interfacecptf IService{ virtual cptf ::ulong addRef() = 0; virtual cptf ::ulong release() = 0; virtual bool queryInterface( const cptf ::IID& iid, void**rntObj ) = 0; };
其實外掛的核心並不複雜,複雜的是對外掛介面的定義和封裝,如何根據不同的業務場景抽象出不同的interface。
9. 原始碼
8. 今後改進的方向