ActorBaseTemplate
ActorBaseTemplate
是混合实现的核心。插件开发者需要继承这个类,实现 C++ 的IActor
接口,插件则通过 C 接口与插件管理器交互,这样才能保证二进制兼容。插件开发者则不应当看到这个 C 接口,C 接口对于插件开发者应当是完全透明的。
这个模板为子类提供了很多服务,所以,我们需要仔细分析下它的代码:
template <typename T, typename Interface=C_Actor> class ActorBaseTemplate : public C_Actor, public IActor { ... };
我们有两个模板参数:T
和Interface
。T
是子类的类型。当你从ActorBaseTemplate
继承时,你必须为基类指定子类的类型。这是递归模板模式(Curiously Recurring Template Pattern,CRTP)的一个例子。Interface
是插件对象与插件管理器进行交互的接口。它可以是 C++ 的 IActor
接口或者是 C 的C_Actor
接口。默认则是C_Actor
。或许你会问,为什么不会一直是C_Actor
?毕竟,如果插件对象希望使用 C++ 接口与插件管理器交互,它只需将其自身注册为 C++ 对象,然后直接实现 IActor
即可。这是一个好主意。AutoBaseTemplate
也支持IActor
的原因就在于,你能够方便地从 C 转换到 C++ 接口。在调试阶段,跳过 C 包装器代码当然会更加方便;并且,如果需要在受控环境下部署系统,你可能并不需要完全的兼容 C。在这种情况下,使用模板参数,你就可以切换底层交互通道。
ActorBaseTemplate
同时实现了C_Actor
和IActor
。它甚至提供了一个IActor
的默认实现,以便你在继承这个类时,只需要覆盖掉某些需要的函数。这可以节省你编码默认函数的时间。C_Actor
是核心接口,因为当Interface=C_Actor
时,我们需要使用这个接口与插件管理器交互。
下面是构造函数代码:
ActorBaseTemplate() : invokeService_(NULL) { // Initialize the function pointers of the C_Actor base class C_Actor::getInitialInfo = staticGetInitialInfo; C_Actor::play = staticPlay; C_Actor * handle = this; C_Actor::handle = (C_ActorHandle)handle; }
这个构造函数没有参数,将invokeService_
函数指针初始化为 NULL,然后继续初始化C_Actor
接口的成员,将其指向静态函数,然后把this
指针赋值给 handle。这与 C/C++ 双对象模型很类似,实际上,它就是一个双对象模型,除了真实的 C++ 实现在子类中是做了实际的工作的。
下面是PF_CreateFunc
和PF_DestroyFunc
的委托实现,需要注册到插件管理器,用于创建和销毁实例。
// PF_CreateFunc - plugin.h static void * create(PF_ObjectParams * params) { T * actor = new T(params); // Set the error reporting function pointer actor->invokeService_ = params->platformServices->invokeService; // return the actor with the correct inerface return static_cast<Interface *>(actor); } // PF_DestroyFunc - plugin.h static apr_int32_t destroy(void * actor) { if (!actor) return -1; delete ActorBaseTemplate<T, Interface>::getSelf (reinterpret_cast<Interface *>(actor)); return 0; }
虽然他们命名为create()
和destroy()
,但这些名字并不是必须的,因为它们实际是使用函数指针而不是名字来调用。事实上,ActorBaseTemplate
定义这些函数,为插件开发者减少了很多麻烦事。create()
简单地创建子类T
的新的实例,并且将invokeService
函数指针赋给invokeService_
数据成员来初始化。destroy()
函数将void
转换成模板参数指定的Interface
类型,然后使用getSelf()
函数(我们将在后面讨论这个函数)获得一个合适的指向子类T
的类型指针。然后,它会销毁该对象。这种处理很恰当。插件开发者使用标准构造函数(接受一个PF_ObjectParams
参数,但可以忽略)和析构函数创建对象,ActorBaseTemplate
则在底层做一些神奇的处理,保证所有的 static 函数都可以分发到子类。
下面是三个重载的getSelf()
static 函数:
// Helper method to convert the C_Actor * argument // in every method to an ActorBaseTemplate<T, Interface> instance pointer static ActorBaseTemplate<T, Interface> * getSelf(C_Actor * actor) { return static_cast<ActorBaseTemplate<T, Interface> *>(actor); } static ActorBaseTemplate<T, Interface> * getSelf(IActor * actor) { return static_cast<ActorBaseTemplate<T, Interface> *>(actor); } static ActorBaseTemplate<T, Interface> * getSelf(C_ActorHandle handle) { return static_cast<ActorBaseTemplate<T, Interface> *>((C_Actor *)handle); }
我们重载了三个版本:IActor
、C_Actor
和C_ActorHandle
。getSelf()
函数简单地使用了static_cast
运算符,将底层实现强制转换成不同的接口,正如前面见到的那样。在处理上,我们只是使用了 C 风格的转换,获得C_Actor
对象。正如你在构造函数以及后面的ActorBaseTemplate
中见到的那样,我们经常会在需要时获取一个接口或者句柄。
下面则是 staticreportError()
函数:
// Helper method to report errors from a static function static void reportError(C_ActorHandle handle, const apr_byte_t * filename, apr_uint32_t line, const apr_byte_t * message) { ActorBaseTemplate<T, Interface> * self = ActorBaseTemplate<T, Interface>::getSelf(handle); ReportErrorParams rep; rep.filename = filename; rep.line = line; rep.message = message; self->invokeService_((const apr_byte_t *)"reportError", &rep); }
这是一个提供方便的函数,将调用请求转发给invokeService
函数指针。它能够将调用者从将实际参数组织为ReportErrorParams
(在应用程序的 services.h 中定义)的繁琐工作中解放出来,并且自动选择正确的调用字符串“reportError”。这些错误报告转换是在应用程序的 service 层定义的,供插件开发者尽可能简单地开发插件。
下面则是C_Actor
接口的实现:
// C_Actor functions static void staticGetInitialInfo(C_ActorHandle handle, C_ActorInfo * info) { ActorBaseTemplate<T, Interface> * self = ActorBaseTemplate<T, Interface>::getSelf(handle); try { self->getInitialInfo(info); } catch (const StreamingException & e) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)e.filename_.c_str(), e.line_, (const apr_byte_t *)e.what()); } catch (const std::runtime_error & e) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)__FILE__, __LINE__, (const apr_byte_t *)e.what()); } catch (...) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)__FILE__, __LINE__, (const apr_byte_t *)("ActorBaseTemplate<T, Interface>" "::staticGetInitialInfo() failed")); } } static void staticPlay(C_ActorHandle handle, C_Turn * turn) { try { TurnWrapper tw(turn); getSelf((C_Actor *)handle)->play(&tw); } catch (const StreamingException & e) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)e.filename_.c_str(), e.line_, (const apr_byte_t *)e.what()); } catch (const std::runtime_error & e) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)__FILE__, __LINE__, (const apr_byte_t *)e.what()); } catch (...) { ActorBaseTemplate<T, Interface>::reportError(handle, (const apr_byte_t *)__FILE__, __LINE__, (const apr_byte_t *)("ActorBaseTemplate<T, Interface>::staticPlay()" " failed")); } }
这两个接口函数的实现几乎是一样的:getSelf()
,调用通过多态机制调用子类的 C++IActor
实现,同时进行错误处理。在我们讨论错误处理之前,注意下staticPlay()
函数。该函数接受一个C_Turn
接口,将其包装成TurnWrapper
,然后传递给IActor::play()
函数,而后者则需要一个 C++ 的ITurn
。这就是这个包装器的作用。
错误处理是ActorBaseTemplate
的另一个特性。它允许插件开发者忘记他们开发的插件必须遵守某些严格的限制(例如不能有违二进制兼容性的限制),可以正常地抛出异常。每一个子类函数的调用(除了构造函数和析构函数)都被包围在 try-catch 块之中。这里有一个从最特殊的异常到最普通的异常的处理链。插件开发者可以抛出插件框架定义的StreamingException
异常。这是一个独立异常类,在异常信息中包含了抛出位置信息(文件名和行号)。如果你需要了解StreamingException
,可以查看 Practical C++ Error Handling in Hybrid Environments。
下面给出使用StreamingException
的方便的宏:
#ifndef PF_BASE #define PF_BASE #include "StreamingException.h" #define THROW throw StreamingException(__FILE__, __LINE__) #define CHECK(condition) if (!(condition)) \ THROW << "CHECK FAILED: '" << #condition << "'" #ifdef _DEBUG #define ASSERT(condition) if (!(condition)) \ THROW << "ASSERT FAILED: '" << #condition << "'" #else #define ASSERT(condition) {} #endif // DEBUG //---------------------------------------------------------------------- namespace base { std::string getErrorMessage(); } #endif // BASE_H
这些宏很方便调试,因为最终你会通过应用程序的invokeService()
实现以及reportError()
函数获得所有信息。如果插件开发者选择抛出标准的std::runtime_error
,错误处理代码需要使用what()
函数获得错误嘻嘻,但是并没有有意义的文件名和行号的信息。__FILE__
和__LINE__
宏能够为ActorBaseTemplate
的错误处理代码提供文件名和行号,而不是错误的真实位置。最后,我们会使用通配符接受所有类型的异常。这里,我们并没有直接获取错误信息,而是提供了一个通用的信息获取函数。
ActorBaseTemplate
的最终目的是将插件开发者各种工作中解放出来,包括插件对象的实现列表、允许开发者使用标准 C++ 接口(这里是IActor
)进行开发、无需关心特殊的 static 函数的定义,例如创建、销毁、错误处理、以及与 C 的交互等。