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

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

0 1

ActorBaseTemplate

ActorBaseTemplate是混合实现的核心。插件开发者需要继承这个类,实现 C++ 的IActor接口,插件则通过 C 接口与插件管理器交互,这样才能保证二进制兼容。插件开发者则不应当看到这个 C 接口,C 接口对于插件开发者应当是完全透明的。

这个模板为子类提供了很多服务,所以,我们需要仔细分析下它的代码:

template <typename T, typename Interface=C_Actor>
class ActorBaseTemplate :
    public C_Actor,
    public IActor
{
...
};

我们有两个模板参数:TInterfaceT是子类的类型。当你从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_ActorIActor。它甚至提供了一个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_CreateFuncPF_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);
}

我们重载了三个版本:IActorC_ActorC_ActorHandlegetSelf()函数简单地使用了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 的交互等。

发表评论

关于我

devbean

devbean

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

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