OSP插件开发

UCS 中, 应用骨架程序启动时, 调起OSP插件框架, 作为OSP插件的加载器和容器, 在运行时根据需要动态管理插件的加载、卸载、启动和停止.

一个OSP插件, 可具有如下基本功能:

  • 对外提供可调用的服务(函数接口);

  • 对外提供可订阅的事件(消息接口), 及发布消息;

  • 查找或监听其他插件提供的服务的实时状态, 和调用其服务接口;

  • 订阅及接收其他插件提供和发布的消息;

  • 可使用独立或统一的配置文件;

  • 统一的日志系统;

1. 创建一个OSP插件项目

创建一个OSP插件项目 , 可根据需要在 appFramework 或 appTest 目录下创建一个子目录作为插件项目目录. 在目录下创建一个cmake项目工程。根据需要编写 CMakeLists.txt 文件, 并添加相应的源文件. 另外为了方便编译和打包插件, 还需编写相关的编译和打包脚本. 打包插件需要编写 xxx.bndlspec 文件, 用于打包时生成插件的manifest信息以供插件框架解析处理.

最快的创建方式是复制一个已有的OSP插件示例项目目录, 并修改其各项内容满足自己的需求.

一个OSP插件示例项目目录结构如下:

eventadmin                                      # 插件项目目录
├── bundle.sh                                   # 打包脚本
├── CMakeLists.txt                              # cmake项目工程文件
├── eventadmin.bndlspec                         # 插件打包配置文件
├── include                                     # 头文件目录
│   └── eventadmin                              # 插件需对外发布服务接口定义等的头文件目录, 需安装到 ./deps/workspace/install 目录下以便其他插件引用
├── make.sh                                     # 编译脚本
└── src                                         # 源文件目录
    ├── detail
    ├── EventAdminServiceImpl.cpp
    ├── EventAdminServiceImpl.h
    └── MainActivator.cpp

2. 编写插件代码

一个OSP插件之所以可以被OSP插件框架加载和管理, 是通过继承 Cagy::OSP::BundleActivator 基类以及使用 POCO_BEGIN_MANIFEST/POCO_EXPORT_CLASS/POCO_END_MANIFEST 宏声明和定义插件的manifest信息及导出类来实现的. 如 EAPubActivator.cpp 中代码:

#include "Cagy/OSP/BundleActivator.h"
#include "Cagy/OSP/BundleContext.h"
#include "Cagy/OSP/Bundle.h"
#include "Cagy/OSP/ServiceRegistry.h"
#include "Poco/ClassLibrary.h"

#include "EAPubServiceImpl.h"


using namespace Cagy::OSP;


namespace EAPub {


// 定义插件激活器类, 继承自Cagy::OSP::BundleActivator
class EAPubActivator: public BundleActivator
{
public:
    EAPubActivator()
    {
    }

    ~EAPubActivator()
    {
    }

    // 实现插件启动方法
    void start(BundleContext::Ptr pContext)
    {
        pContext->logger().information("---------------------- EAPubActivator start -- begin");

        // 实例化一个插件内部业务逻辑实现类
        _pService = new EAPubServiceImpl(pContext);

        pContext->logger().information("---------------------- EAPubActivator start -- end");
    }

    void stop(BundleContext::Ptr pContext)
    {
        pContext->logger().information("---------------------- EAPubActivator stop -- begin");

        // 清理内部业务逻辑
        delete _pService;
        _pService = 0;

        pContext->logger().information("---------------------- EAPubActivator stop -- end");
    }

private:
    EAPubServiceImpl* _pService;
};


}   // namespace EAPub

// 使用宏导出插件激活器类, 以便插件框架加载和初始化
POCO_BEGIN_MANIFEST(Cagy::OSP::BundleActivator)
    POCO_EXPORT_CLASS(EAPub::EAPubActivator)
POCO_END_MANIFEST

在该代码中 EAPubActivator 类继承和实现了 Cagy::OSP::BundleActivator 类的 start() 和 stop() 方法, 在 start() 方法中创建 EAPubServiceImpl 业务逻辑类的实例, 在 stop() 方法中, 销毁 EAPubServiceImpl 类的实例.


3. 编写CMakeLists.txt

编写CMakeLists.txt用于插件库的编译和安装, 可根据需要将插件需要导出的头文件及生成的库文件安装到指定目录:

  • 头文件, 需安装到 ./deps/workspace/install 目录;

  • 库文件, 需安装到 ./deploy/lib 目录.

install(FILES ${HDRS_INCLUDE_G}       COMPONENT Devel DESTINATION include/eapub)

CAGY_INSTALL_BUNDLE(${deploy} ${PROJECT_NAME})

4. 编写插件bndlspec文件

bndlspec文件用于插件打包时生成插件的manifest信息以供插件框架解析处理. 插件框架会根据插件中的manifest信息,动态加载插件, 并初始化插件.

<?xml version="1.0"?>
<bundlespec>
  <manifest>
    <name>EAPub</name>
    <symbolicName>eapub</symbolicName>
    <version>1.0.1</version>
    <vendor>CAGY</vendor>
    <copyright>(c) 2007-2023, CAGY</copyright>
    <activator>
      <class>EAPub::EAPubActivator</class>
      <library>eapub</library>
    </activator>
    <lazyStart>false</lazyStart>
    <runLevel>400</runLevel>
    <dependency>
			<symbolicName>eventadmin</symbolicName>
			<version>[1.0.1, 2.0.0)</version>
		</dependency>
  </manifest>
  <code>
    ../../deploy/lib/${osName}/${osArch}/*eapub.so
  </code>
</bundlespec>

如上例中的 .bndlspec 配置文件. 其中各项属性意义如下:

  • name: 插件的名称;

  • symbolicName: 插件的符号名称, 用于插件的识别管理, 一般建议与插件库文件同名;

  • version: 插件的版本号, 用于插件的版本管理;

  • copyright: 版权信息;

  • activator: 插件激活器信息, 需指定插件库文件名及其内使用 POCO_EXPORT_CLASS 宏导出的类名称;

  • lazyStart: 是否懒加载, 默认值为 false 表示应用启动时由插件框架自动加载并启动; 为 true 时表示应用启动时不启动该插件, 直到有其他需要依赖该插件的插件启动时才加载并启动;

  • runLevel: 该插件启动的优先级, 数字越小优先级越高, 越先启动;

  • dependency: 本插件需要依赖的其他插件, 被依赖的插件如果未启动, 则本插件启动时会等待被依赖的插件启动后再启动. 有多个依赖插件时需要编写多个 dependency 属性;

  • code: 罗列需要被打包的文件列表.


5. 编写make.sh 和 bundle.sh

  • make.sh, 用于根据系统信息设置环境变量和加载编译配置, 调用对应的编译工具链配置编译插件库, 并生成库文件;

  • bundle.sh, 用于调用BundleCreator工具, 根据指定的bndlspec文件配置, 将插件打包为bundle包并放到指定的插件目录.

具体内容可参考示例项目目录下的 make.sh 和 bundle.sh 文件. 执行编译和打包:

$ ./make.sh
$ ./bundle.sh

6. 在插件中发布服务

在插件中对外发布可调用的服务, 目前支持两种方式.

    1. ServiceRegistry方式

    在插件中, 可使用 Cagy::OSP::ServiceRegistry 类来注册服务. 此种方式需要向外发布 服务接口定义的头文件, 供其他插件引用. 此种方式的优点为可以通过插件框架管理监听服务的注册和注销事件, 以及通过对服务的引用进行服务接口调用,效率更高效, 缺点为需要向外发布服务接口定义的头文件.

    1. EventAdminService方式

    在插件中, 可使用 Cagy::OSP::EventAdminService 类来发布服务.此种方式优点为 不需要向外发布服务接口定义文件, 缺点为无法通过插件框架管理监听服务的注册和注销事件.

1). ServiceRegistry方式

使用ServiceRegistry方式发布服务需要以下几个步骤, 以network插件举例说明:

  1. 声明一个接口类如 NetworkService , 继承自 Cagy::OSP::Service 基类, 在该接口类中声明服务接口定义纯虚函数, 该类用于对外发布接口声明;

  2. 编写一个实现类如 NetworkServiceImpl , 继承自 NetworkService 类, 并实现该接口类中声明的接口以及 type() 和 isA 函数;

  3. 在Activator实现类的 start(BundleContext::Ptr pContext) 函数中, 实例化接口实现类, 并通过 pContext->registry().registerService(…) 函数将其注册到框架中;

  4. 在Activator实现类的 stop(BundleContext::Ptr pContext) 函数中, 通过 pContext->registry().unregisterService(…) 函数将服务实例从框架中注销;

接口声明类示例代码如下:

#ifndef NetworkService_INCLUDED
#define NetworkService_INCLUDED


#include "Poco/AutoPtr.h"
#include "Poco/BasicEvent.h"

#include "Cagy/OSP/Service.h"

#include "network/NetworkMessage.h"


namespace Network {


// 定义服务接口类, 继承自 Cagy::OSP::Service 服务类
class NetworkService: public Cagy::OSP::Service
{
public:
	using Ptr = Poco::AutoPtr<NetworkService>;

    virtual ~NetworkService() {}

    // 定义服务接口函数
    virtual void sendMessage(uint32_t index, int32_t value) = 0;
    virtual void sendData(uint32_t index, char* data, uint32_t length) = 0;


    // 定义两个事件, 用于向外发布从网络接口接收到的数据
    Poco::BasicEvent < const IndexValueMsg >        messageEvent;
    Poco::BasicEvent < const IndexDataMsg >         dataEvent;

};


}   // namespace Network


#endif // NetworkService_INCLUDED

接口实现类示例代码如下:

#ifndef NetworkServiceImpl_H
#define NetworkServiceImpl_H


#include "Cagy/OSP/BundleActivator.h"
#include "Cagy/OSP/BundleContext.h"
#include "Cagy/OSP/Bundle.h"
#include "Cagy/OSP/ServiceRegistry.h"

#include "Poco/ClassLibrary.h"

#include <memory>
#include <mutex>

#include "network/NetworkService.h"
#include "detail/comInterface.h"


namespace Network {


// 定义服务接口实现类, 继承自服务接口类
class NetworkServiceImpl : public NetworkService
{
public:
    explicit NetworkServiceImpl(Cagy::OSP::BundleContext::Ptr pContext);
    ~NetworkServiceImpl();

    // 实现Cagy::OSP::Service接口中的方法
    const std::type_info& type() const
	{
		return typeid(NetworkService);
	}
	
	bool isA(const std::type_info& otherType) const
	{
		std::string name(typeid(NetworkService).name());
		return name == otherType.name() || Service::isA(otherType);
	}
    //-----------------------------

    // 实现服务接口类中定义的服务接口方法
    virtual void sendMessage(uint32_t index, int32_t value);
    virtual void sendData(uint32_t index, char* data, uint32_t length);

protected:

    void init();
    void onMessageReceived(uint32_t index, int32_t value, double precision = 0, int32_t offset = 0);
    void onDataReceived(uint32_t index, char* data, uint32_t length);

private:
    Cagy::OSP::BundleContext::Ptr   _pContext;

    std::shared_ptr<comInterface>   _pComInterface;

    mutable std::mutex              _mutex;
};


}   // namespace Network


#endif // NetworkServiceImpl_H

注册及注销服务示例代码如下:

#include "Cagy/OSP/BundleActivator.h"
#include "Cagy/OSP/BundleContext.h"
#include "Cagy/OSP/Bundle.h"
#include "Cagy/OSP/ServiceRegistry.h"
#include "Poco/ClassLibrary.h"

#include "NetworkServiceImpl.h"


using namespace Cagy::OSP;


namespace Network {


// 定义插件激活器类, 继承自Cagy::OSP::BundleActivator
class NetworkActivator: public BundleActivator
{
public:
	NetworkActivator()
	{
	}
	
	~NetworkActivator()
	{
	}
	
	// 实现插件启动方法
	void start(BundleContext::Ptr pContext)
	{
		pContext->logger().information("---------------------- NetworkActivator start -- begin");
		
		// 实例化一个插件服务类实例
		NetworkService::Ptr pService = new NetworkServiceImpl(pContext);
		// 将服务注册到服务注册表, 以便其他插件可以检索到
		_pService = pContext->registry().registerService("cagy.NetworkService", pService, Properties());
		
		pContext->logger().information("---------------------- NetworkActivator start -- end");
	}
		
	void stop(BundleContext::Ptr pContext)
	{
		pContext->logger().information("---------------------- NetworkActivator stop -- begin");

		// 注销服务
		pContext->registry().unregisterService(_pService);
		_pService = 0;
		
		pContext->logger().information("---------------------- NetworkActivator stop -- end");
	}

private:
	ServiceRef::Ptr _pService;
};


}   // namespace Network


// 使用宏导出插件激活器类, 以便插件框架加载和初始化
POCO_BEGIN_MANIFEST(Cagy::OSP::BundleActivator)
	POCO_EXPORT_CLASS(Network::NetworkActivator)
POCO_END_MANIFEST

2). EventAdminService方式

使用EventAdminService方式发布服务需要以下几个步骤:

  1. 声明一个函数签名为 bool(QVariantHash&, QVariantHash&) 的服务接口函数, 该函数用于发布服务;

  2. 从框架中获取到 EventAdmin::EventAdminService 服务, 并调用其 publishService(…) 函数将声明的接口函数发布为服务, 并将返回的服务保持智能指针保存下来;

  3. 在需要停止服务时, 将服务保持智能指针释放, 或者调用 EventAdmin::EventAdminService 服务对象的 unpublishService(…) 函数将服务注销;

声明一个接口函数示例代码如下:

// 声明一个接口函数用于发布服务
bool testServiceFun(QVariantHash& request, QVariantHash& response)
{
    // 状态返回数据
    response.insert("c", "ccccc");
    response.insert("d", "ddddd");
    // 返回服务执行是否成功
    // true 表示成功, false 表示失败
    return true;
}

注册服务示例代码如下:

EventAdmin::EventAdminService::Ptr _pEAService = nullptr;
// 获取注册名为"cagy.EventAdminService"的服务引用
ServiceRef::Ptr pServiceRef = pContext->registry().findByName("cagy.EventAdminService");
if(pServiceRef)
{
    // 将服务引用转换为EventAdmin::EventAdminService接口类型
    _pEAService = pServiceRef->castedInstance<EventAdmin::EventAdminService>();
}
if(_pEAService)
{
    // 实例化一个Lambda函数包装服务接口函数
    std::function<bool(QVariantHash&, QVariantHash&)> fun = [this](QVariantHash& request, QVariantHash& response)
        {
            return testServiceFun(request, response);
        };
    // 将Lambda函数包装的服务接口函数发布为名为 "testcall" 的服务
    _funHolder = _pEAService->publishService("testcall", fun);
}

注销服务示例代码如下:

// 通过释放服务保持智能指针, 注销服务
// _funHolder.reset();
if(_pEAService)
{
    // 通过EventAdminService服务的 unpublishService(...) 函数注销服务
    _pEAService->unpublishService("testcall");
}

7. 在插件中调用服务

在插件中调用其他插件的服务, 目前支持两种方式.

    1. ServiceRegistry方式

    在插件中, 使用 Cagy::OSP::ServiceRegistry 类来查找服务, 并通过获取到的服务指针调用服务接口.

    1. EventAdminService方式

    在插件中, 使用 Cagy::OSP::EventAdminService 类来判断服务是否存在, 和调用服务接口.

1). ServiceRegistry方式

使用ServiceRegistry方式调用服务需要以下几个步骤:

  1. 使用如 pContext->registry().findByName(…) 或者 _pContext->registry().createListener(…) 的方式来获取到服务实例;

  2. 通过服务实例调用服务接口;

使用 pContext->registry().findByName(…) 查找和调用服务示例代码如下:

Network::NetworkService::Ptr _pNWService = nullptr;
// 获取注册名为"cagy.NetworkService"的服务引用
ServiceRef::Ptr pServiceRef = pContext->registry().findByName("cagy.NetworkService");
if(pServiceRef)
{
    // 将服务引用转换为Network::NetworkService接口类型
    _pNWService = pServiceRef->castedInstance<Network::NetworkService>();
}
if(_pNWService)
{
    // 调用NetworkService服务的提供的sendMessage(...)接口函数, 发送数据
    _pNWService->sendMessage(100, 1024);
}

使用 pContext->registry().findByName(…) 查找服务的缺点是: 如果调用 pContext->registry().findByName(…) 时, 目标服务还未注册到服务注册表, 将返回空指针不能获取到有效的服务引用.

为了避免这种情况, 可以通过手动指定不同服务所在插件的启动顺序来保证在调用 pContext->registry().findByName(…) 时服务已经注册. 但这种方式比较麻烦而且在插件依赖关系复杂时, 要计算出不同插件的启动顺序将变得比较困难. 另一种更好的办法是使用 _pContext->registry().createListener(…) 的方式创建一个服务注册状态的侦听器, 用于接收指定服务的注册和注销状态的事件. 在事件的处理函数中, 获取或注销服务实例. 示例代码如下:

// 创建一个针对"cagy.NetworkService"服务的侦听器
// 设置服务注册事件的处理函数为handleRegistered
// 设置服务注销事件的处理函数为handleUnregistered
Cagy::OSP::ServiceListener::Ptr _pListener = _pContext->registry().createListener("name == \"cagy.NetworkService\"",
        delegate(this, &NetworkServiceWrapper::handleRegistered),
        delegate(this, &NetworkServiceWrapper::handleUnregistered));


// 处理服务注册事件
void NetworkServiceWrapper::handleRegistered(const Cagy::OSP::ServiceRef::Ptr& pServiceRef)
{
    if (!pServiceRef.isNull())
    {
        // 判断名称是否匹配
        if(pServiceRef->name() == "cagy.NetworkService")
        {
            std::lock_guard<std::mutex> lock(_mutex);
            if(!_pNetworkService)
            {
                // 将服务引用转换为Network::NetworkService接口类型保存起来供后续服务接口调用
                _pNetworkService = pServiceRef->castedInstance<Network::NetworkService>();

                // 在服务提供的事件上注册事件处理函数, 此为插件之间发布-订阅事件的另一种方式
                _pNetworkService->messageEvent += Poco::Delegate<NetworkServiceWrapper, const Network::IndexValueMsg>(this, &NetworkServiceWrapper::handleNetworkMessage);
                _pNetworkService->dataEvent += Poco::Delegate<NetworkServiceWrapper, const Network::IndexDataMsg>(this, &NetworkServiceWrapper::handleNetworkData);
            }
        }
    }
}

// 处理服务注销事件
void NetworkServiceWrapper::handleUnregistered(const Cagy::OSP::ServiceRef::Ptr& pServiceRef)
{
    if (!pServiceRef.isNull())
    {
        // 判断名称是否匹配
        if(pServiceRef->name() == "cagy.NetworkService")
        {
            std::lock_guard<std::mutex> lock(_mutex);
            // 服务已经从服务注册表注销, 说明服务已经不可用, 因此清理对该服务的引用
            if(_pNetworkService)
            {
                _pNetworkService.reset();
            }
        }
    }
}

2). EventAdminService方式

使用EventAdminService方式发布服务需要以下几个步骤:

  1. 从框架中获取到 EventAdmin::EventAdminService 服务, 并调用其 callService(…) 函数来调用指定服务的接口;

示例代码如下:

EventAdmin::EventAdminService::Ptr _pEAService = nullptr;
// 获取注册名为"cagy.EventAdminService"的服务引用
ServiceRef::Ptr pServiceRef = pContext->registry().findByName("cagy.EventAdminService");
if(pServiceRef)
{
    // 将服务引用转换为EventAdmin::EventAdminService接口类型
    _pEAService = pServiceRef->castedInstance<EventAdmin::EventAdminService>();
}
if(_pEAService)
{
    // 使用QVariantHash 来包装消息数据
    QVariantHash request;
    request.insert("a", "aaaaa");
    request.insert("b", "bbbbb");
    // 使用QVariantHash 来接收返回数据
    QVariantHash response;
    // 调用EventAdminService的callService(...)函数来调用名为"testcall"服务接口
    bool ret = _pEAService->callService("testcall", request, response);
}

8. 使用EventAdminService发布和订阅事件(消息)

在插件中, 可使用 Cagy::OSP::EventAdminService 服务来发布和订阅事件(消息).

发布消息示例代码如下:

EventAdmin::EventAdminService::Ptr _pEAService = nullptr;
// 获取注册名为"cagy.EventAdminService"的服务引用
ServiceRef::Ptr pServiceRef = pContext->registry().findByName("cagy.EventAdminService");
if(pServiceRef)
{
    // 将服务引用转换为EventAdmin::EventAdminService接口类型
    _pEAService = pServiceRef->castedInstance<EventAdmin::EventAdminService>();
}
if(_pEAService)
{
    // 使用QVariantHash初始化包装消息数据
    QVariantHash props;
    props.insert("tttt", "test send msg now");
    // 实例化一个事件, 发布的topic为"aaa/bbb/ccc"
    // 内容为props, 并且设置为保持的(即在消息发布后才订阅的客户端也会收到该消息)
    EventAdmin::eaEvent eventImpl("aaa/bbb/ccc", props, true);
    // 调用EventAdminService的publishEvent(...)函数来发布消息
    _pEAService->publishEvent(eventImpl);
}

或者使用EventAdminService的publishSignal(…)将信号和topic关联起来, 然后通过 emit 信号来发布消息:

_pEAService->publishSignal(_pThread, SIGNAL(pubSignal(EventAdmin::eaEvent)), "aaa/bbb/ccc");

emit pubSignal(event);

订阅消息示例代码如下:

EventAdmin::EventAdminService::Ptr _pEAService = nullptr;
// 获取注册名为"cagy.EventAdminService"的服务引用
ServiceRef::Ptr pServiceRef = pContext->registry().findByName("cagy.EventAdminService");
if(pServiceRef)
{
    // 将服务引用转换为EventAdmin::EventAdminService接口类型
    _pEAService = pServiceRef->castedInstance<EventAdmin::EventAdminService>();
}
if(_pEAService)
{
    // 订阅名为 "aaa/bbb/ccc"  topic , 消息处理函数为this对象的 subSlot 函数
    _pEAService->subscribeSlot(this, SLOT(subSlot(QString, EventAdmin::eaEvent)), "aaa/bbb/ccc");
}

OSP插件的开发可参考 OSP插件示例 .