转:自己动手写插件框架(2)
转自:http://www.devbean.net/2012/03/building-your-own-plugin-framework-2/
插件编程接口
所谓插件,其实就是基于接口的设计。基于插件的系统最基本的一点就是,要有一个中心系统,用于加载未知的插件,并且能够使用预先定义好的接口和协议与这些插件进行交互。
最基本的方式是定义一个接口,提供一系列插件(动态的或者是静态)需要暴露出的函数。这种实现从技术上说是可行的,但实际并不那么简单地操作。原因在于,一个插件需要支持两类接口,但是却只能暴露出一个接口的函数集。这意味着,两类接口必须混合在一起。
第一个接口(协议)是通用插件接口。该接口允许中心系统初始化插件,能够将插件提供的用于创建、销毁对象的函数注册给中心系统。这个通用插件接口不是特定领域相关的,因此能够作为一个可复用库。第二个接口则是插件对象提供的功能接口。这个接口是与特定领域相关的,必须被仔细地设计,并且由插件实际实现。中心系统应当利用这个接口与插件对象进行交互。
下面我们给出一个通用插件接口的头文件。这里,我们不会深究细节,仅仅为了有个相对直观地认识。
1 #ifndef PF_PLUGIN_H 2 #define PF_PLUGIN_H 3 4 #include <apr-1/apr_general.h> 5 6 #ifdef __cplusplus 7 extern "C" { 8 #endif 9 10 typedef enum PF_ProgrammingLanguage 11 { 12 PF_ProgrammingLanguage_C, 13 PF_ProgrammingLanguage_CPP, 14 } PF_ProgrammingLanguage; 15 16 struct PF_PlatformServices_; 17 18 typedef struct PF_ObjectParams 19 { 20 const apr_byte_t * objectType; 21 const struct PF_PlatformServices_ * platformServices; 22 } PF_ObjectParams; 23 24 typedef struct PF_PluginAPI_Version 25 { 26 apr_int32_t major; 27 apr_int32_t minor; 28 } PF_PluginAPI_Version; 29 30 typedef void * (*PF_CreateFunc)(PF_ObjectParams *); 31 32 typedef apr_int32_t (*PF_DestroyFunc)(void *); 33 34 typedef struct PF_RegisterParams 35 { 36 PF_PluginAPI_Version version; 37 PF_CreateFunc createFunc; 38 PF_DestroyFunc destroyFunc; 39 PF_ProgrammingLanguage programmingLanguage; 40 } PF_RegisterParams; 41 42 typedef apr_int32_t (*PF_RegisterFunc)(const apr_byte_t * nodeType, 43 const PF_RegisterParams * params); 44 45 typedef apr_int32_t (*PF_InvokeServiceFunc)(const apr_byte_t * serviceName, 46 void * serviceParams); 47 48 typedef struct PF_PlatformServices 49 { 50 PF_PluginAPI_Version version; 51 PF_RegisterFunc registerObject; 52 PF_InvokeServiceFunc invokeService; 53 } PF_PlatformServices; 54 55 typedef apr_int32_t (*PF_ExitFunc)(); 56 57 typedef PF_ExitFunc (*PF_InitFunc)(const PF_PlatformServices *); 58 59 #ifndef PLUGIN_API 60 #ifdef WIN32 61 #define PLUGIN_API __declspec(dllimport) 62 #else 63 #define PLUGIN_API 64 #endif 65 #endif 66 67 extern 68 #ifdef __cplusplus 69 "C" 70 #endif 71 PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params); 72 73 #ifdef __cplusplus 74 } 75 #endif 76 77 #endif /* PF_PLUGIN_H */
你需要认识到的第一件事是,这是一个 C 头文件。这就允许我们的插件框架能够被纯 C 系统编译和使用,并且能够编写纯 C 插件。但是,这么做并不会限定必须使用 C,实际上,它已经被设计为更常用 C++ 来实现。
PF_ProgrammingLanguage
枚举允许插件告诉插件管理器,它本身是由 C 还是 C++ 实现的。
PF_ObjectParams
是一个抽象结构,在创建插件对象时被传入。
PF_PluginAPI_Version
用于指明版本信息。这有助于插件管理器只加载兼容版本的插件。
函数指针PF_CreateFunc
和PF_DestroyFunc
必须由插件实现,用于插件管理器创建和销毁插件对象。
PF_RegisterParams
结构包含了插件必须提供给插件管理器的所有信息,以便插件管理器初始化插件(版本,创建、销毁函数以及开发语言)。
PF_RegisterFunc
函数指针(由插件管理器实现)允许每个插件将其支持的对象类型以PF_RegisterParams
结构的形式注册给插件管理器。注意,这种实现允许插件注册不同版本的对象,以及注册多个对象类型。
PF_InvokeService
函数指针是一个通用函数,允许插件调用主系统提供的各种服务,例如日志、事件处理或者错误报告等。该函数要求有一个服务名称以及一个指向参数结构的不透明的指针。插件应当知道可用的服务以及如何调用它们(或者实现一种服务发现机制)。
PF_PlatformServices
结构用于表示平台提供的所有服务(版本、已注册对象和调用函数)。该结构会在插件初始化的时候传给每一个插件。
PF_ExitFunc
是插件退出函数的指针,由插件实现。
PF_InitFunc
是插件初始化的函数指针。
PF_initPlugin
是动态插件(也就是通过动态链接库或者共享库部署的插件)初始化函数的实际声明。它由动态插件暴露出,所以插件管理器可以在加载插件时进行调用。它有一个指向PF_PlatformServices
结构的指针,所以在插件初始化时,这些服务都是可以调用的(这正是注册对象的理想时机),函数返回退出函数的指针。
而对于静态插件(由静态链接库实现,并且直接与主应用程序链接的插件)应该实现init
函数,但是不能命名为PF_initPlugin
。原因是,如果有多个静态插件,它们不能有相同的名字的函数。
静态插件的初始化过程有所不同。它们必须由主程序显式地进行初始化,也就是通过PF_InitFunc
调用其初始化函数。这实际是不好的设计,因为如果要新增或者删除静态插件,主应用的代码都必须修改,并且那些不同名字的init
函数都必须能够找到。
有一个叫做“自动注册”的技术试图解决这个问题。自动注册由一个静态库的全局对象实现。该对象会在main()
函数执行之前构造完成。这个全局对象能够请求插件管理器初始化静态插件(通过传递插件init()
函数的指针来完成)。不幸的是,在某些版本的 Visual C++ 中,这种技术并不支持。
编写插件
如何编写插件?我们的插件框架提供了最通用的功能,在目前的条件下很难添加能够与主应用交互的插件。所以,你必须再次插件框架的基础之上构建自己的应用程序对象。这意味着,你的应用程序(加载插件的)连同插件本身,都必须遵守同一个交互模型。通常这代表,应用程序需要插件提供特定类型的对象,用于暴露某些特定的 API。插件框架提供所有必须的公共基础代码,用于插件的注册、枚举以及加载。
下面的例子是 C++ 接口定义的IActor
。这个接口有两个操作:getInitialInfo()
和play()
。注意,这个接口并不足以应付所有情况,因为getInitialInfo()
函数需要一个指向ActorInfo
结构的指针,而play()
则需要另外一个接口ITurn
的指针。这是经常遇见的情况,你必须这么设计,并且指定一个特定的对象模型。
1 struct IActor 2 { 3 virtual ~IActor() {} 4 virtual void getInitialInfo(ActorInfo * info) = 0; 5 virtual void play( ITurn * turnInfo) = 0; 6 };
每个插件都可以注册IActor
接口的多个实现。当应用程序决定实例化一个由插件注册的对象时,它就调用由插件实现的PF_CreateFunc
函数。插件就会做出响应,创建对象并返回给应用程序。函数返回值是void *
,因为对象的创建操作是通用插件框架的一部分,因此并不知道任何关于特定的IActor
接口的信息。应用程序负责将void *
转换成IActor *
,然后像其他对象一样通过接口调用其函数。当应用程序使用完IActor
对象时,会调用注册的PF_DestroyFunc
函数,插件就销毁该对象。至于为什么需要有虚析构函数,我们会在以后的讨论中介绍。