我將開發(fā)一個(gè)簡(jiǎn)單的角色扮演游戲,可以自己增加非玩家角色的插件。游戲的引擎加載插件并無逢地集成他們。游戲展示了這些概念并且展示能夠?qū)嶋H運(yùn)行的代碼。
列表1是一個(gè)指定了通用插件接口的頭文件。沒有深入細(xì)節(jié)并解釋所有事情之前,讓我們看看它提供了什么。
#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
首先你應(yīng)當(dāng)注意到這是一個(gè)C文件。這允許插件框架可以由純C系統(tǒng)編譯使用并可用來寫純C插件。但是,它不僅僅局限在C上,且實(shí)際上大多數(shù)情況下用在C++中。
枚舉類型PF_ProgrammingLanguage允許插件聲明到用C++實(shí)現(xiàn)的插件管理器中。
PF_ObjectParams是一個(gè)抽象的結(jié)構(gòu)體,創(chuàng)建插件時(shí)用于傳遞參數(shù)給插件對(duì)象。
PF_PluginAPI_Version被用于商討版本問題,并保證插件管理器只加載合適版本的插件。
函數(shù)指針PF_CreateFunc和PF_DestroyFunc(由插件來實(shí)現(xiàn))允許插件管理器來創(chuàng)建和銷毀插件對(duì)象(每個(gè)插件注冊(cè)這樣的函數(shù)到插件管理器中。)
PF_RegisterParams結(jié)構(gòu)體包含初始化時(shí)插件必須提供給插件管理器的所有信息。(版本信息,創(chuàng)建/銷毀函數(shù),編程語(yǔ)言)
PF_RegisterFunc(由插件管理器實(shí)現(xiàn))允許每個(gè)插件為每種它所支持的對(duì)象類型注冊(cè)一個(gè)PF_RegisterParams結(jié)構(gòu)體。注意這個(gè)方案允許一個(gè)插件注冊(cè)一個(gè)對(duì)象的不同版本和多個(gè)對(duì)象類型。
PF_InvokeService函數(shù)指針是一個(gè)通用的函數(shù),查檢可以用其來調(diào)用主系統(tǒng)提供的服務(wù)如日志、事件通知及錯(cuò)誤報(bào)告。其簽名(signature)包括服務(wù)名稱和指向一個(gè)參數(shù)結(jié)構(gòu)體不透明的指針。插件應(yīng)當(dāng)知道可用的服務(wù)以及如何調(diào)用它們(或者,如果你希望使用PF_InvokeService,你可以自己實(shí)現(xiàn)服務(wù)。)
PF_PlatformServices結(jié)構(gòu)體聚集了我剛剛提到所有的由平臺(tái)提供給插件的服務(wù)(版本,注冊(cè)對(duì)象,執(zhí)行服務(wù)函數(shù))。該結(jié)構(gòu)體在初始化時(shí)傳遞給每個(gè)插件。
PF_ExitFunc定義了插件退出函數(shù),由插件來實(shí)現(xiàn)。
PF_InitFunc定義了插件初始化函數(shù)指針。
PF_initPlugin是動(dòng)態(tài)插件(由動(dòng)態(tài)鏈接庫(kù)/共享庫(kù)來部署的插件)實(shí)際的初始化函數(shù)的簽名(signature)。從動(dòng)態(tài)插件中導(dǎo)出它的名字,因此插件管理器可以在加載插件時(shí)調(diào)用它。它接受一個(gè)指向PF_PlatformServices結(jié)構(gòu)體的指針,因此所有的服務(wù)在初始化時(shí)立刻可用(這是注冊(cè)對(duì)象的正確時(shí)機(jī))并返回一個(gè)指向退出函數(shù)的指針。
注意靜態(tài)插件(實(shí)現(xiàn)在靜態(tài)庫(kù)中且直接連接到主執(zhí)行體中)應(yīng)當(dāng)實(shí)現(xiàn)一個(gè)有C linkage的init函數(shù),但禁止將其命名為PF_initPlugin。原因是如果有多個(gè)靜態(tài)插件,他們都將有一個(gè)同樣的初始化函數(shù)名字,你的編譯器痛恨這個(gè)。
靜態(tài)插件的初始化有所不同。他們必須顯式地由主執(zhí)行體初始化。主執(zhí)行體將通過PF_InitFunc的簽名調(diào)用它們的初始化函數(shù)。很不幸,這意味著每當(dāng)一個(gè)新的靜態(tài)插件加入/移出系統(tǒng)時(shí),主執(zhí)行體需要被修改,并且各種各樣的init函數(shù)的名字必須是對(duì)等的(coordinated)。
有一種試圖解決該問題的技術(shù)叫做“自動(dòng)注冊(cè)”。自動(dòng)注冊(cè)通過靜態(tài)庫(kù)中一個(gè)全局的對(duì)象來達(dá)到目的。該對(duì)象在main()事件啟動(dòng)之前被構(gòu)建。該全局對(duì)象可以請(qǐng)求插件管理器來初始化靜態(tài)插件(通過傳遞插件的init()函數(shù)指針)。不幸的是,這種方案在VC++中不能工作。
撰寫插件
撰寫插件意味著什么?插件框架是非常generic,并且不提供任何可以與你的應(yīng)用交互的切實(shí)的對(duì)象。你必須在插件框架上構(gòu)建你自己的應(yīng)用程序模型。這意味著你的應(yīng)用程序(加載插件)以及插件自身必須同意并通過某種模型來協(xié)作。通常這表明應(yīng)用程序期待插件提供暴露某種特定API的某種類型的對(duì)象。插件框架將提供注冊(cè)、枚舉及加載這些對(duì)象的基礎(chǔ)設(shè)施。示例1是一個(gè)叫做IActor的C++接口的定義。它有兩個(gè)操作——getInitialInfo()和play()。注意該接口不是充分的,因?yàn)?/span>getInitialInfo()期望一個(gè)指向名為ActorInfo的結(jié)構(gòu)體的指針,且play()期望一個(gè)指向另一個(gè)叫做ITurn接口的指針。這是實(shí)際的一個(gè)案例,你必須設(shè)計(jì)并指定整個(gè)對(duì)象模型。
struct IActor
{
virtual ~IActor() {}
virtual void getInitialInfo(ActorInfo * info) = 0;
virtual void play( ITurn * turnInfo) = 0;
};
示例1
每個(gè)插件可以注冊(cè)多個(gè)實(shí)現(xiàn)了IActor接口的類型。當(dāng)應(yīng)用程序決定示例化一個(gè)由插件注冊(cè)的對(duì)象,它將調(diào)用注冊(cè)的,由插件實(shí)現(xiàn)的PF_CreateFunc函數(shù)。插件負(fù)責(zé)創(chuàng)建一個(gè)合適的對(duì)象并將其返回給應(yīng)用程序。返回類型指定為void *是因?yàn)閷?duì)象創(chuàng)建操作是通用插件框架的一部分,該部分不知道任何關(guān)于特定IActor接口的信息。應(yīng)用程序隨后將void *轉(zhuǎn)換到IActor *,然后就可以在整個(gè)接口中使用它,好像它是一個(gè)正常的對(duì)象。當(dāng)應(yīng)用程序使用完IActor對(duì)象后,它執(zhí)行注冊(cè)的由插件實(shí)現(xiàn)的PF_DestroyFunc函數(shù),然后插件銷毀actor對(duì)象。目前不好考慮虛擬的析構(gòu)函數(shù),我會(huì)在后面的部分討論它。
編程語(yǔ)言支持
在二進(jìn)制兼容性部分我解釋了你可以利用C++的vtable一級(jí)的兼容性,如果你的編譯器滿足的話。你也可以使用C一級(jí)的兼容性,這樣你就可以使用不同的編譯器來構(gòu)建應(yīng)用程序和插件,但你將被局限在C的交互上。你的應(yīng)用程序?qū)ο竽P捅仨毷腔?/span>C的。你不能使用好的C++接口如IACTOR,但你必須設(shè)計(jì)一個(gè)相似的C接口。
純C
在純C的編程模型中你只需要用C開發(fā)插件。當(dāng)你實(shí)現(xiàn)PF_CreateFunc函數(shù)時(shí)你返回一個(gè)在你的應(yīng)用程序C對(duì)象模型中與其它C對(duì)象交互的C對(duì)象。所有的話題都是關(guān)于C對(duì)象和C對(duì)象模型的。所有人都知道C是一個(gè)過程語(yǔ)言,沒有對(duì)象的概念。然而C提供了足夠的抽象機(jī)制來實(shí)現(xiàn)對(duì)象以及多態(tài)(在此處是必須的)并支持面向?qū)ο蟮木幊谭盒?。?shí)際上,最初的C++編譯器是一個(gè)C編譯器的事實(shí)上的一個(gè)前端(front-end)。它根據(jù)C++代碼產(chǎn)生C代碼,然后使用一個(gè)普通的C編譯器來編譯該C代碼。它的名字Cfront說明了一切。
使用包含函數(shù)指針的結(jié)構(gòu)體(譯注:就可以獲得OO特性)。每個(gè)函數(shù)的簽名應(yīng)當(dāng)接受它所屬結(jié)構(gòu)體作為第一個(gè)參數(shù)該結(jié)構(gòu)體也可以包含其它的數(shù)據(jù)成員。這樣提供了(與C++類有關(guān)的簡(jiǎn)單的土語(yǔ))如:封裝(狀態(tài)和行為捆綁)、繼承(通過將基結(jié)構(gòu)體的對(duì)象作為第一個(gè)數(shù)據(jù)成員)以及多態(tài)(通過設(shè)置不同的函數(shù)指針。)(譯注:沒錯(cuò),這就是用C來編寫OO程序的基本要求和方法,我也用C寫過OO程序)。
C不支持析構(gòu)函數(shù)、函數(shù)及操作符重載,名字空間,因此你定義接口時(shí)只有很少的選項(xiàng)。這也許是“塞翁失馬,焉知非福”,因?yàn)榻涌趹?yīng)該被可能掌握C++另一個(gè)子集的其它人所使用。減少語(yǔ)言的范圍可能會(huì)提升你的接口的簡(jiǎn)單性和可用性。
我將在插件框架的后續(xù)文章中探究OO的C。列表2包含了陪伴該文章系列(僅僅是投你所好)的示例游戲的C對(duì)象模型。如果你快速瀏覽一下你會(huì)看見它甚至支持集合以及遍歷。
#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++開發(fā)你的插件。插件編程接口函數(shù)可以被實(shí)現(xiàn)為靜態(tài)成員函數(shù)或者普通的靜態(tài)/全局函數(shù)(畢竟C++主要是C的超集)。(這句不好翻?。?/span>The object model can be your garden variety C++ object model.) 列表3包含示例游戲的C++對(duì)象模型。它基本上與列表2種的C對(duì)象模型相似。
#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++二重奏
在這個(gè)編程模型中你可以使用C或者C++來開發(fā)插件。當(dāng)你注冊(cè)你的對(duì)象時(shí)要指定編程語(yǔ)言。如果你創(chuàng)建一個(gè)平臺(tái)并且你想提供給第三方開發(fā)者最終的自由是他們可以選擇自己的編程語(yǔ)言及模型,混合并匹配C和C++插件時(shí),這個(gè)模型將非常有用。
插件框架支持它,但實(shí)際的工作在于為你的應(yīng)用設(shè)計(jì)一個(gè)既支持C又支持C++對(duì)象模型。每個(gè)對(duì)象類型需要同時(shí)實(shí)現(xiàn)C和C++的接口。這意味著你將有一個(gè)有著標(biāo)準(zhǔn)VTABLE布局的C++類以及一系列與虛擬方法相關(guān)的函數(shù)指針。這種結(jié)構(gòu)非常復(fù)雜,我將不演示它。
要注意的是從插件開發(fā)人員的角度來說這種方法不會(huì)帶來額外的復(fù)雜性。他們永遠(yuǎn)可以使用C或C++接口來開發(fā)C或C++的插件。
C/C++混合體
在該模型中,你必須在C對(duì)象模型的蓋子之下用C++開發(fā)插件。這就包括了C++包裹類的創(chuàng)建,該包裹類實(shí)現(xiàn)了C++對(duì)象模型并包裹(wrap)相應(yīng)C對(duì)象的。插件開發(fā)人員在這層上編程,將每個(gè)調(diào)用、參數(shù)及返回值在C和C++之間翻譯。當(dāng)實(shí)現(xiàn)你的應(yīng)用程序?qū)ο竽P蜁r(shí)這需要額外的工作,但通常很直接。好處是對(duì)于插件開發(fā)這來說提供了一個(gè)有著完整C級(jí)兼容性的好的C++編程模型。我不會(huì)在示例游戲的上下文中演示它。
語(yǔ)言-鏈接矩陣
圖1顯示了各種不同的部署模型組合的利與弊(靜態(tài)庫(kù) vs. 動(dòng)態(tài)庫(kù))以及編程語(yǔ)言的選擇(C vs. C++)。

圖1
為了本次討論,如果使用C++插件,C/C++二重奏模型有C++限制和所需的先決條件,對(duì)于C插件,有C的限制和所需的先決條件。而且,C/C++混合模型不過是C模型,因?yàn)?/span>C++層被隱藏在插件實(shí)現(xiàn)后面。這些都讓人迷惑,但底線是你有選項(xiàng)了,且插件框架允許你做出自己的決定,采用你自己認(rèn)為合適的折中。它沒有強(qiáng)迫你使用某個(gè)特定的模型,也沒有瞄準(zhǔn)最小公分母。
Copyleft (C) 2007, 2008 raof01. 本文可以用于除商業(yè)用途外的所有用途。若用于非商業(yè)用途,請(qǐng)保留此權(quán)利聲明,并以超鏈接形式標(biāo)明文章原始出版、作者信息和本聲明;若要用于商業(yè)用途,請(qǐng)與作者聯(lián)系,否則作者將使用法律來保證權(quán)利。