大家好,又见面了,我是你们的朋友全栈君。
“使用ctkPluginFramework插件系统构建项目实战”,这篇文章是写博客以来最纠结的一篇文章。倒不是因为技术都多么困难,而是想去描述一个项目架构采用ctkPluginFramework来构建总是未尽其意,描述的太少未免词不达意,描述的太多又显得太啰嗦。有些看过之前写的【大话QT之四】ctkPlugin插件系统实现项目插件式开发这篇文章的朋友也想了解一下到底如果从零开始架构一个项目。在写这篇文章的时候又回头总结了下我之前认为已经懂了的东西,发现还是好多东西没有真正明白其原理是什么,在文章下面的描述中,对构建项目中的关键流程给出实现过程,关于整体细节之处大家可以看例子中的代码。
一、准备阶段
因为本次想实现的项目架构就是基于ctkPluginFramework的,因此开始之前需要:
1) Windows下安装VS2010和QT环境。
2) CTK编译好的CTKCore.dll、CTKPluginFramework.dll以及相关的头文件。
3) 了解QT中基本插件的制作与加载方式。
二、实战阶段
2.1 项目架构分析
每一个中型或大型的项目,在实际开发之前,其代码组织架构一定会经过仔细的规划,比如:目录架构(头文件放在哪个目录;lib库放在哪个目录;开发源代码放在哪个目录;生成的项目插件放在哪个目录;最终发布程序放在哪个目录)。现在,假设我们要为图书馆开发一个图书管理程序,称之为:LibraryProject,然后在该文件夹下有:includes,libs,plugins,bin,application等文件夹,其基本结构如下:
其基本用途如下:applications用于放置项目加载程序的源码;bin下包含conf、plugins目录,bin下存放项目入口exe程序以及独立运行时依赖的dll文件,bin/conf下存放项目配置文件,bin/plugins下放置项目中所有使用到自己开发的插件;includes目录用于放置头文件,包括第三方头文件以及项目中自己定义的接口文件等;libs用于放置项目中使用的第三方开源库的lib文件;plugins用于放置项目中所有的插件开发源码;而上图中的cuc_base.pri是用来定义或加载项目中通用的内容,比如:INCLUDEPATH在该文件中定义则在其它插件子项目中只要include一下这个文件,就可以使用includes中包含的所有头文件。
接下来说一下项目整体运作流程。首先,项目是基于插件开发,因而项目中所有的功能模块都是以dll插件的形式提供。插件大致分为两种类型,即界面插件和功能插件,界面插件主要用来完成界面显示,以及界面上的操作流程,功能插件主要用来提供某一方面的功能。以Ftp客户端工具为例,其操作界面就可以采用一个界面插件来实现,而其本地文件系统数据的提供、文件上传下载等功能的实现就可以使用功能插件来提供。其次,虽然项目所有界面显示以及功能实现都采用dll插件来提供,难道整个项目就是一堆dll吗?并非如此,一个项目总归需要一个启动入口,即传统意义上的exe文件,它既可以复杂到实现所有的功能,也可以简单到只负责加载一个逻辑插件运行就可以了,就好像上程序设计课时老师提及的一句话,main()函数是一定存在的。在本实现中即需要一个这样的最简化的如果程序exe来通过某种方式调用到主逻辑插件,然后开始运行。
在实际动手前我们再总结一下前面提到的几个关键模块:项目启动程序、完成插件注册的插件、功能插件、主逻辑插件以及它们之间的相互关系:
从简单来说,整个项目的运行就如上如所示,读配置,注册插件,加载插件;从复杂来说,在实现上远没有那么简答,总之在使用过程中会碰上各种各样的问题,只有在反复的使用过程中才能不断了解,对出现问题时才能迅速地排查。
2.2 构建项目启动程序
下面,我们就来逐步构建这个项目。首先,项目启动程序最终生成的是exe文件,它是项目的主入口,其主要功能是:1) 通过QPluginLoader的方式来加载 “用于注册插件的插件”。2) 利用 “用于注册插件的插件”来运行主逻辑插件。关键代码如下:
QSettings settings(strConfFile, QSettings::IniFormat);
settings.setIniCodec(QTextCodec::codecForName("UTF-8"));
QString strPluginLoader = settings.value(CUC_PLUGIN_LOADER).toString();
QString strPluginPortal = settings.value(CUC_PLUGIN_PORTAL).toString();
//!加载controller插件(用于加载其它插件)
QString strPluginLoaderPath = QString(CUC_PLUGIN_PATH).arg(qApp->applicationDirPath()) + strPluginLoader + QString(".dll");
QString strPluginPortalPath = QString(CUC_PLUGIN_PATH).arg(qApp->applicationDirPath()) + strPluginPortal + QString(".dll");
if (!QFile(strPluginLoaderPath).exists())
{
qDebug() << "[Error] Controller Plugin does not exists ...";
return CUC_FAILED;
}
QPluginLoader PluginLoader(strPluginLoaderPath);
CUControllerInterface *Controller = qobject_cast<CUControllerInterface *>(PluginLoader.instance());
if (!Controller)
{
qCritical() << QObject::tr("The required module (%1) is invalid, the application will quit.(%2)").arg(strPluginLoader).arg(PluginLoader.errorString());
return CUC_FAILED;
}
else
{
CUCParameters Parameters;
Parameters[CUC_KEY_CONF_FILE] = strConfFile;
Parameters[CUC_KEY_PLUGIN_PATH] = strPluginPath;
Parameters[CUC_KEY_MAIN_HANDLE] = qVariantFromValue((void *)&CUC);
if (Controller->Init(Parameters) != CUC_SUCCESS)
{
return CUC_FAILED;
}
Controller->LoadAllPlugin(strPluginPath, settings.value(CUC_PLUGIN_EXCLUDE).toString());
if (Controller->ExecutePlugin(strPluginPortalPath, Parameters) != CUC_SUCCESS)
{
return CUC_FAILED;
}
}
代码中,Init()、LoadAllPlugin()以及ExecutePlugin()函数均是在“用于注册插件”的插件中提供的,具体实现请看2.3。
此外,在以上代码调试过程中碰到一个问题:使用QPluginLoader加载插件时,load()一直返回false,而errorstring()提示的为“找不到指定模块”,关于这个问题的解决方法请看第三部分。
2.3 编写第一个用于注册插件的插件
本部分开始介绍,第一个用于“注册插件的”插件。请注意,这个插件是传统意义上的插件,它还不算真正的ctkPlugin的插件,因为它不具有声明周期。2.4会介绍一个真正的ctkPlugin的插件如何定义。有经验的开发人员都会这样想,插件既然作为基于ctkPluginFramework构建的项目中的模块单元,应该会一直用到,那么所有的这些插件应该存在一些共性的东西,因此,我们应该定义一个统一的接口来约束这种规范。这里,我们实现的定义接口的规则为:首先定义一个所有插件必须实现的接口,当然可以在实现里不去做事情,这里假设我们的通用插件接口文件为:cuc_base_interface。其次,在每个插件的实现中,我们定义插件的接口,插件中的接口类继承自通用插件接口,并在此基础上定义自己插件对外服务的功能。接下来会通过关键代码来展示应如何定义这样一个插件:
1> 基础插件类,用于定义所有插件必须实现的接口。
/*
* 定义基础插件的接口
*/
class CUCBaseInterface
{
public:
virtual int Init(const CUCParameters &Parameters) = 0;
virtual int Uninit() = 0;
virtual int CreateInstance(const CUCParameters &Parameters) = 0;
virtual int DestoryInstance() = 0;
virtual int Launch(const CUCParameters &Parameters) = 0;
virtual int Close() = 0;
//!插件更新接口
virtual void Upgrade() = 0;
};
Q_DECLARE_INTERFACE(CUCBaseInterface, "com.cuc.base")
2> 插件接口类,继承自基础插件类,并定义自己对外的服务接口。
class CUControllerInterface: public CUCBaseInterface
{
public:
/*
* 接口说明:完成所有插件的加载工作
* 参数说明:strPluginPath: 插件文件所在路径
* strFilter: 插件过滤条件
*/
virtual void LoadAllPlugin(const QString &strPluginPath, const QString &strFilter) = 0;
/*
* 接口说明:安装插件
* 参数说明:strPlugin:插件文件绝对路径
*/
virtual int InstallPlugin(const QString &strPlugin) = 0;
/*
* 接口说明:运行插件
* 参数说明:strPlugin:插件文件绝对路径
* Parameters: 插件加载相关参数说明
*/
virtual int ExecutePlugin(const QString &strPlugin, const CUCParameters &Parameters) = 0;
};
Q_DECLARE_INTERFACE(CUControllerInterface, "com.cuc.controller")
3> 插件类,继承自插件接口类,实现所有的接口,这里只对关键实现进行说明。
1) Init()实现ctkPluginFramework的初始化。
int CUController::Init(const CUCParameters &Parameters)
{
if (m_bInit)
{
return CUC_SUCCESS;
}
m_Parameters = Parameters;
//!初始化CTKPluginFramework系统
m_PluginFramework = m_FrameworkFactory.getFramework();
try
{
m_PluginFramework->init();
m_PluginFramework->start();
qDebug() << "[Info] ctkPluginFramework start ...";
}
catch(const ctkPluginException &Exception)
{
qCritical()<<QObject::tr("Failed to initialize the plug-in framework: ")<<Exception.what();
return CUC_FAILED;
}
m_bInit = true;
return CUC_SUCCESS;
}
2) InstallPlugin()实现插件加载。
int CUController::InstallPlugin(const QString &strPlugin)
{
QString strPluginName = GetPluginNameWithVersion(strPlugin);
//!如果插件已经加载则直接返回
if (m_mapPlugins.contains(strPluginName))
{
return CUC_SUCCESS;
}
try
{
QSharedPointer<ctkPlugin> Plugin = m_PluginFramework->getPluginContext()->installPlugin(QUrl::fromLocalFile(strPlugin));
Plugin->start(ctkPlugin::START_TRANSIENT);
m_strPluginLog += QObject::tr("%1 (%2) is loaded.\r\n").arg(Plugin->getSymbolicName()).arg(Plugin->getVersion().toString());
}
catch(const ctkPluginException &Exc)
{
m_strPluginLog += QObject::tr("Failed to load %1: ctkPluginException(%2).\r\n").arg(strPlugin).arg(Exc.what());
qDebug() << m_strPluginLog;
return CUC_FAILED;
}
catch(const std::exception &E)
{
m_strPluginLog += QObject::tr("Failed to load %1: std::exception(%2).\r\n").arg(strPlugin).arg(E.what());
qDebug() << m_strPluginLog;
return CUC_FAILED;
}
catch(...)
{
m_strPluginLog += QObject::tr("Failed to load %1: Unknown error.\r\n").arg(strPlugin);
qDebug() << m_strPluginLog;
return CUC_UNKNOW;
}
return CUC_SUCCESS;
}
3) ExecutePlugin()实现主插件运行。
int CUController::ExecutePlugin(const QString &strPlugin, const CUCParameters &Parameters)
{
if (!m_bInit)
{
return CUC_NOT_INIT;
}
QString strPluginName = GetPluginNameWithVersion(strPlugin);
CUCBaseInterface *Base = 0 ;
if (!m_mapPlugins.contains(strPluginName))
{
QPluginLoader PluginLoader(strPlugin);
PluginLoader.load();
Base = qobject_cast<CUCBaseInterface *>(PluginLoader.instance());
}
else
{
Base = qobject_cast<CUCBaseInterface *>(m_mapPlugins.value(strPluginName));
}
if (Base)
{
m_Parameters[CUC_KEY_CONTROLLER_HANDLE] = qVariantFromValue((void *)this);
m_Parameters[CUC_KEY_PLGUIN_CONTEXT] = qVariantFromValue((void *)m_PluginFramework->getPluginContext());
Base->Init(m_Parameters);
Base->CreateInstance(m_Parameters);
if (Base->Launch(m_Parameters) != CUC_SUCCESS)
{
qDebug() << QObject::tr("Failed to launch (%1).").arg(strPlugin);
return CUC_FAILED;
}
}
else
{
qDebug() << QObject::tr("The portal (%1) is invalid.").arg(strPlugin);
return CUC_FAILED;
}
}
2.4 编写第一个功能插件
将要实现的这个插件是一个真正的ctkPluginFramework的插件,结合【大话QT之四】ctkPlugin插件系统实现项目插件式开发 以及如下的图形象地展示了一个插件的生命周期,由start开始,向系统注册可以提供服务;由stop结束,终止对外提供服务;由uninstall终止,在ctkPluginFramework系统中移除。因此,在2.3介绍的插件实现的基础上(即也遵循每个插件有自己的插件接口类,它继承自通用插件接口,并由插件类继承实现),每个ctk的插件都应该通过定义Plugin插件类来提供start和stop接口,实现自身插件的声明周期,下面来看一下插件的Plugin类是如何定义实现的:
1> 插件类头文件,如下所示,该类继承自ctkPluginActivator,用于实现自己的声明周期。
#include <ctkPluginActivator.h>
class CUHelloWorld;
class CUHelloWorldPlugin : public QObject, public ctkPluginActivator
{
Q_OBJECT
Q_INTERFACES(ctkPluginActivator)
public:
void start(ctkPluginContext *Context);
void stop(ctkPluginContext *Context);
private:
CUHelloWorld *m_HelloWorld;
};
2> 插件类的实现,如下所示,在start()阶段通过:registerService()向ctkPluginFramework系统进行注册;在stop阶段删除指针。
#include "cuc_helloworld_plugin.h"
#include <QtPlugin>
#include "cuc_helloworld.h"
void CUHelloWorldPlugin::start(ctkPluginContext *Context)
{
m_HelloWorld = new CUHelloWorld;
Context->registerService(QStringList("CUHelloWorldInterface"), m_HelloWorld);
}
void CUHelloWorldPlugin::stop(ctkPluginContext *Context)
{
if (m_HelloWorld)
{
delete m_HelloWorld;
m_HelloWorld = 0;
}
}
Q_EXPORT_PLUGIN2(com_cuc_helloworld, CUHelloWorldPlugin)
这个插件介绍完之后,相信关于ctk的插件如何定义就能有一个大概的了解,只有真正去写一个插件才能了解它是如何实现的。其它的插件都大同小异,只是自己实现的接口不通。鉴于上面插件注册的实现,这里关于插件如何引用不过多说了,关键代码如下:
3> 使用ctkPluginFramework中的插件
int CULibraryManager::LoadUsedPlugins()
{
try
{
//!初始化HelloWorld插件
ctkServiceReference refHelloWorld = m_PluginContext->getServiceReference("CUHelloWorldInterface");
m_HelloWorld = (qobject_cast<CUHelloWorldInterface *>(m_PluginContext->getService(refHelloWorld)));
if (!m_HelloWorld || m_HelloWorld->Init(m_Parameters) != CUC_SUCCESS)
{
qDebug()<<QObject::tr("Module %1 is invalid").arg("com.cuc.helloworld");
return CUC_FAILED;
}
}
catch(...)
{
return CUC_FAILED;
}
return CUC_SUCCESS;
}
通过getService获取到插件指针,就能够使用它提供的服务了。
三、解决的问题
1. QPluginLoader在load插件com.cuc.controller插件的时候一直无法成功,返回false。
可能出现的问题,见相关链接:http://stackoverflow.com/questions/17920303/strange-error-error-loading-plugin-the-specified-module-could-not-be-found 它提出了几点可能会出现的问题,逐个排查即可。
如果你确定你写的插件格式没有问题,那么很有可能是插件存在其它依赖,依赖的dll没有放进来,我就是这个问题。利用小工具:Dependency Walker可以查看一个dll依赖的其它dll文件。
2. 为插件文件添加插件详细信息。
一般,dll插件文件都有自己的详细信息,即在右键属性详细信息里面,里面会对插件的一些详细信息进行说明。要实现这样必须定义一个资源文件,相关参考连接如下,这里就不详细描述了:http://msdn.microsoft.com/en-us/library/aa381058%28v=VS.85%29.aspx
四、总结
这篇文章拖了好久,一是因为写论文占了好大部分时间,二是在自己写例子的过程中又遇到了很多问题,花费了很多的时间。这里我想突出的重点是如何从一个项目的角度来设计它的实现,诚然会写ctk的插件是前提,但不是重点。站在项目的角度会有一览众山小的感觉,不要拘泥于某个具体的东西。我这里有实现源码,如果感兴趣的可以在下面留下邮箱,我会发送到你的邮箱中,或者发站内信也可以。
千里之行始于足下,不要好高骛远!!加油。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/134903.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...