"使用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的插件是前提,但不是重點。站在項目的角度會有一覽衆山小的感覺,不要拘泥於某個具體的東西。我這裏有實現源碼,如果感興趣的可以在下面留下郵箱,我會發送到你的郵箱中,或者發站內信也可以。
千里之行始於足下,不要好高騖遠!!加油。