首页 插件 自己动手写插件框架(5)

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

0 1.6K

注册对象

现在,球已经在插件手上了(在PF_initPlugin中)。如果版本正确,插件管理器应当已经注册了所有支持的对象类型。注册的目的是提供类似PF_CreateFuncPF_DestroyFunc这样的函数,以便以后用于创建和销毁插件对象。这种安排允许插件自己控制所管理对象的实际创建和销毁操作,包括资源管理等(如内存),而由应用程序控制所有的对象及其生命周期。当然,插件也可能使用单例模式,始终返回相同的对象实例。

完成注册,需要为每一个对象类型准备注册记录(PF_RegisterParams),调用在 PF_PlatformServices结构体(该结构体通过PF_initPlugin传入)中定义的registerObject()函数指针。registerObject()函数接受一个字符串,作为对象类型的唯一标识符,或者是星号(*),同时还需要提供PF_RegisterParams结构。我们会在后面讨论为什么需要这个字符串。这个类型字符串之所以需要,是因为插件可能支持多种类型的对象。

在上一节的代码中,我们注册了两种怪物类型:"KillerBunny" 和 "StationarySatan"。

现在,鞋已经在脚上了,准备起跑吧!一旦插件调用过registerObject()函数,控制权就回到PluginManager手上。PF_RegisterParams还包含了一个版本号和编程语言的类型。版本号允许PluginManager确定它是否能够处理该对象类型。如果版本号不匹配,则该对象就不会注册。这并不会成为一个致命错误。这种设计允许程序拥有充分的灵活性,插件可以尝试注册同一对象类型的多种版本,以便在兼容旧版本的情况下使用新接口的优点。我们将在后面的章节中解释编程语言类型的作用。如果插件管理器发现了合适的PF_RegisterParams结构,它只需简单地将其保存为一个内部数据,使用一个对象类型和PF_RegisterParams的散列表。

当插件注册过所有的对象类型,它会返回一个指向PF_ExitFunc的函数指针。这个函数将会在插件卸载之前被调用,目的是让插件能够清理其生命周期内用到的全局资源。

如果插件发现它不能正确工作(可能是因为某些资源没有获取,可能是因为注册对象类型失败或者版本不对等),应当及时清理并返回 NULL。这让PluginManager能够知道插件初始化失败。PluginManager就会移除该插件注册的所有内容。

由应用程序创建插件对象

当运行到这一步时,所有动态插件都已经加载完成,所有静态插件和动态插件都已经初始化完毕,并且将其支持的对象类型注册完成。应用程序现在可以使用PluginManagercreateObject()函数创建插件对象。这个函数需要一个对象类型字符串和一个IObjectAdapter接口。我们会在下一节讨论这个接口,现在先来看看这个对象类型字符串。

应用程序需要知道支持哪些对象类型。这种信息可以硬编码到应用程序中,也可以由程序自己去查询插件管理器维护的映射表,在运行时找出已经注册了哪些对象类型。

回忆一下,类型字符串可以是一个唯一的字符串,也可以是“*”。当应用程序使用类型字符串(“*”是一个无效对象类型)调用createBbject()函数时,PluginManager查询其注册表,看有没有这么一种对象类型。如果存在,则调用已经注册的PF_CreateFunc函数,将结果返回给应用程序(通常会以适配的形式)。如果没有找到,则遍历所有以“*”注册的插件,然后调用其PF_CreateFunc函数。如果插件返回非 NULL 指针,则将其返回给应用程序。

那么,提供“*”的目的是什么?简单来说,就是允许插件创建在注册时并不知道的对象。这是什么意思?在 Numenta(译注:原作者所在公司),我们使用这种技术支持 Python 插件:一个使用“*”类型字符串的单独的 C++ 插件。如果应用程序请求 Python 类(其类型是一个 Python 类的完全限定类名),那么,这个嵌入了 Python 解释器的 C++ 插件会创建一个特殊的对象,用于持有该 Python 类的实例,并且将对插件的请求操作转发给内部的 Python 对象(通过 Python C API)。这样从外部看来,就完全是一个标准的 C++ 对象了。这就带来了极大的灵活,因为我们可以在系统运行时加载一个 Python 类,这个 Python 类立即可用。

自动适配 C 对象

现在我们又回到那个问题:让插件框架同时支持 C 插件和 C++ 插件。C 和 C++ 插件对象实现了不同的接口。下面我们将讨论如何设计和实现同时支持 C/C++ 对象模型的系统。这种统一的T对象模型可以同时支持 C 和 C++ 的对象访问。但是,如果应用程序需要使用两套独立的代码去处理两种插件,显然是不方便的。应用程序代码应该避免 if 语句,也应该避免大量的强制类型转换。我们的插件框架可以使用两种技术来解决这些问题:

  1. 对象模型包括 C 和 C++ 两种对象;
  2. C 对象使用一个特殊的适配器包装,将其接口暴露为对应的 C++ 接口。这样做的结果是完全屏蔽 C 插件的差异性,当做 C++ 对象处理。

我们使用对象适配器来实现这种适配。这是一种由应用程序提供的,实现了IObjectAdapter接口的对象(其实就是一个由插件框架提供的ObjectAdapter模板)。下面的代码展示了IObjectAdapter接口和ObjectAdapter对象。

#ifndef OBJECT_ADAPTER_H
#define OBJECT_ADAPTER_H

#include "plugin_framework/plugin.h"

// 该接口用于将 C 插件对象适配成 C++ 插件对象。
// 必须传递给 PluginManager::createObject() 函数。
struct IObjectAdapter
{
    virtual ~IObjectAdapter() {}
    virtual void * adapt(void * object, PF_DestroyFunc df) = 0;
};

// 该模板需要在同时支持 C/C++ 对象设计的模式下使用。
// 否则,你需要提供自己的实现了 IObjectAdapter 接口的对象适配器。
template<typename T, typename U>
struct ObjectAdapter : public IObjectAdapter
{
    virtual void * adapt(void * object, PF_DestroyFunc df)
    {
        return new T((U *)object, df);
    }
};
#endif // OBJECT_ADAPTER_H

PluginManager使用这个对象将 C 对象适配成 C++ 对象。我们会在后面的章节中详细解释这个问题。

需要注意的是,插件框架提供了所有必须的基础接口,用于将 C 对象适配成 C++ 对象,但是仍然需要应用程序的帮助,因为它并不知道自己需要适配的对象类型。

应用程序同插件对象的交互

应用程序只需要调用它创建的插件对象的 C++ 接口(有可能是 C 接口适配而来)的 C++ 成员函数。为了忠实地从成员函数返回值,插件对象可能需要调用PlatformServices结构的PF_InvokeService函数。这些服务可以提供诸如日志、错误处理、长时间操作的进度通知以及事件处理。当然,这些回调函数是应用程序与插件的协议,属于完整的应用程序接口和对象模型设计的一部分。

由应用程序析构插件对象

管理对象生命周期的最佳实践是,谁创建谁销毁。这对于 C++ 这样的语言尤其重要,因为你必须自己管理内存分配和释放。我们有很多方法去分配和释放内存:malloc/free,new/delete,数组的 new/delete,操作系统提供的不同堆的分配、释放的特殊 API 等。同时,使用与分配函数相对应的释放函数也是很重要的。创建者就是获取资源如何分配的最好的位置。在插件框架中,每一个对象类型都会注册创建函数和销毁函数(PF_CreateFuncPF_DestroyFunc)。插件对象由PF_CreateFunc函数创建,就应该由PF_DestroyFunc函数销毁。每个插件都应该维护自己需要的资源的分配和释放。插件可以自由选择使用的内存模型。所有的插件对象可能都会静态分配内存,此时,PF_DestroyFunc无需做任何操作。或者,插件会维护一个预创建对象的对象池,PF_DestroyFunc函数只需要将对象放入该对象池。应用程序使用PF_CreateFunc创建对象,使用PF_DestroyFunc销毁对象。C++ 插件对象的析构函数会正确调用,所以应用程序不需要特意直接调用PF_DestroyFunc函数,只需调用标准的 delete 运算符即可。这种技术同样适用于适配的 C 对象,因为对象适配器保证在析构函数中调用PF_DestroyFunc

在应用程序关闭时清理插件系统

当应用程序退出时,需要销毁其创建的所有插件对象,并且通知所有插件(静态的和动态的)去清理自身。应用程序通过调用PluginManagershutdown()函数达到这一目的。PluginManager会调用每个插件的PF_ExitFunc函数(该函数指针会由PF_initPlugin函数返回),卸载所有动态插件。

当应用程序准备退出,插件持有的内存自动归还时,也需要调用这种退出函数。原因是可能有一些资源不会被自动回收,同时插件可能会保留着一些缓存状态,例如数据库提交、网络发送等。幸运的是,这些都是由PluginManager管理的,应用程序无需关心。

在某些情况下,应用程序可能仅仅需要卸载一个插件。此时,退出函数必须调用,插件本身卸载掉(如果是动态插件的话),并且要从PluginManager的内部数据中移除。

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2