自己动手写插件框架(2)

插件编程接口

所谓插件,其实就是基于接口的设计。基于插件的系统最基本的一点就是,要有一个中心系统,用于加载未知的插件,并且能够使用预先定义好的接口和协议与这些插件进行交互。

最基本的方式是定义一个接口,提供一系列插件(动态的或者是静态)需要暴露出的函数。这种实现从技术上说是可行的,但实际并不那么简单地操作。原因在于,一个插件需要支持两类接口,但是却只能暴露出一个接口的函数集。这意味着,两类接口必须混合在一起。

第一个接口(协议)是通用插件接口。该接口允许中心系统初始化插件,能够将插件提供的用于创建、销毁对象的函数注册给中心系统。这个通用插件接口不是特定领域相关的,因此能够作为一个可复用库。第二个接口则是插件对象提供的功能接口。这个接口是与特定领域相关的,必须被仔细地设计,并且由插件实际实现。中心系统应当利用这个接口与插件对象进行交互。

下面我们给出一个通用插件接口的头文件。这里,我们不会深究细节,仅仅为了有个相对直观地认识。

#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 */

你需要认识到的第一件事是,这是一个 C 头文件。这就允许我们的插件框架能够被纯 C 系统编译和使用,并且能够编写纯 C 插件。但是,这么做并不会限定必须使用 C,实际上,它已经被设计为更常用 C++ 来实现。

PF_ProgrammingLanguage枚举允许插件告诉插件管理器,它本身是由 C 还是 C++ 实现的。

PF_ObjectParams是一个抽象结构,在创建插件对象时被传入。

PF_PluginAPI_Version用于指明版本信息。这有助于插件管理器只加载兼容版本的插件。

函数指针PF_CreateFuncPF_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的指针。这是经常遇见的情况,你必须这么设计,并且指定一个特定的对象模型。

struct IActor
{
    virtual ~IActor() {}
    virtual void getInitialInfo(ActorInfo * info) = 0;
    virtual void play( ITurn * turnInfo) = 0;
};

每个插件都可以注册IActor接口的多个实现。当应用程序决定实例化一个由插件注册的对象时,它就调用由插件实现的PF_CreateFunc函数。插件就会做出响应,创建对象并返回给应用程序。函数返回值是void *,因为对象的创建操作是通用插件框架的一部分,因此并不知道任何关于特定的IActor接口的信息。应用程序负责将void *转换成IActor *,然后像其他对象一样通过接口调用其函数。当应用程序使用完IActor对象时,会调用注册的PF_DestroyFunc函数,插件就销毁该对象。至于为什么需要有虚析构函数,我们会在以后的讨论中介绍。

Leave a Reply