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

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

0 1

错误处理

错误处理在基于插件的 C++ 系统中与普通系统有所不同。你不能简单的在插件中抛出异常,然后由应用程序去处理它们。这是因为我们前面讨论过的二进制兼容的问题。如果插件使用与编译系统的同样的编译器编译,这种实现还可能正常。但是,你不能强制插件开发者使用与你一样的编译器。你可以使用 C 风格的返回错误码,但是这不符合我们的 C++ 插件框架的设计理念。插件框架的主要设计目标之一就是,允许插件开发者和系统开发者都使用 C++ 开发,即使在底层使用的是 C 风格的动态链接库。

所以,我们需要的是一种能够拦截插件抛出的异常的方法,然后将其安全地传递到动态库之外,并且要与编译器无关,最后将异常在应用程序内再次抛出。

这里我们使用的解决方案是,在插件内部将每一个函数调用放在一个 try-except 块中。当插件抛出异常时,我们导出异常信息,通过特定的invokeService()调用报告给主系统。在主系统方面,reportError()服务被调用,将错误信息存储下来;在当前插件对象函数返回之后,将存储的异常抛出。

这种延迟的序列化的异常抛出机制并不大方便,但是却足以解决在插件函数调用了reportError()函数之后不作任何处理的情况下,仍然能够使用标准 C++ 异常。

实现 C/C++ 双对象模型

本节或许是这篇 C++ 插件框架中最复杂、最创新的部分。双对象系统允许在同一应用程序中 C 和 C++ 插件同时存在,并且允许应用程序不必意识到这种底层的差别,将所有对象都当做 C++ 对象。不幸的是,通用插件框架不能满足做这种要求,也就是说,必须根据对象模型作相应的改变。这里,我们从设计模式的角度来重构我们的简单的游戏,不过,对于你自己的业务模型,你必须自己动手了。

双对象系统的基本想法是,系统中每一个对象都可以同时使用 C 接口或者 C++ 接口。这些接口应该能够识别出来。我们的游戏的对象模型包括:

  • ActorInfo
  • ActorInfoContainer
  • Turn
  • Actor

ActorInfo是其中最简单的一个,因为它是一个包含了关于 actor 信息的 struct。在 C 和 C++ 中,我们使用同样的 struct,定义在 c_object_model.h 文件中。但是,其它对象可就不那么简单了。

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;

ActorInfoContainer是完整的 C/C++ 双对象,其代码如下:

#ifndef ACTOR_INFO_CONTAINER_H
#define ACTOR_INFO_CONTAINER_H

#include "object_model.h"
#include <vector>

struct ActorInfoContainer :
    IActorInfoIterator,
    C_ActorInfoIterator
{
    static void reset_(C_ActorInfoIteratorHandle handle)
    {
        ActorInfoContainer * aic = reinterpret_cast<ActorInfoContainer *>(handle);
        aic->reset();
    }

    static C_ActorInfo * next_(C_ActorInfoIteratorHandle handle)
    {
        ActorInfoContainer * aic = reinterpret_cast<ActorInfoContainer *>(handle);
        return aic->next();
    }

    ActorInfoContainer()
        : index(0)
    {
        C_ActorInfoIterator::handle = (C_ActorInfoIteratorHandle)this;
        C_ActorInfoIterator::reset = reset_;
        C_ActorInfoIterator::next = next_;
    }

    void reset()
    {
        index = 0;
    }

    ActorInfo * next()
    {
        if (index >= vec.size())
            return NULL;
        return vec[index++];
    }

    apr_uint32_t index;
    std::vector<ActorInfo *> vec;
};
#endif

下面,我们将一行一行地说明这些代码。

这个类的作用很简单。它为保存不可变的ActorInfo对象的集合(std::vector)提供了一个单向的遍历器。同时也允许将内部指针重置到集合开始处,这就允许我们多次遍历。该接口有一个next()函数,返回指向当前对象(ActorInfo)的指针,并将内部指针指向下一个对象;如果现在已经是最后一个对象,则返回 NULL。该函数的第一次调用返回集合的第一个对象;如果集合是空的则返回 NULL。这种语义同 STL 遍历器不一样(STL 遍历器需要你自己取其指向的对象,并且 STL 遍历器也可以指向值对象,集合结尾的判断是遍历器与同一集合的end()遍历器相等)。我们使用不同形式的遍历器的理由有这么几个:STL 遍历器支持很多形式的遍历,而我们只需要向前的单向遍历。因此,我们选择了更简洁的代码来实现。更主要的原因是,插件对象的遍历接口需要支持 C 接口。最后,由于个人原因,我自己更喜欢返回 NULL 的遍历器,并且不需要再用额外的操作获取遍历器指向的值。基于以上几点,我们的代码就是这个样子。

回到ActorInfoContainer。这个类同时继承了 IActorInfoIteratorC_ActorInfoIterator。也正因为这个原因,才让它符合 C/C++ 双对象模型:

struct ActorInfoContainer :
    IActorInfoIterator,
    C_ActorInfoIterator
{
    ...
};

当然,这就要求我们同时实现两个接口。C++ 接口就是经典的 ABC(抽象基类,Abstract Base Class),其中所有的成员函数(next()reset())都是纯虚函数。

struct IActorInfoIterator
{
    virtual void reset() = 0;
    virtual ActorInfo * next() = 0;
};

C 接口则需要一个不透明的句柄,我们可以使用一个指向底层 struct 的指针。该 struct 包含了一个成员,这个成员有两个函数指针next()release(),它们都以此句柄作为第一参数。

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;

发表评论

关于我

devbean

devbean

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

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