错误处理
错误处理在基于插件的 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
。这个类同时继承了 IActorInfoIterator
和C_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;