C++插件化 NDD源碼的插件機制實現(xiàn)解析
插件機制是一種框架,允許開發(fā)人員簡單地在應(yīng)用程序中添加或擴展功能。它使廣泛使用,因為它可以作為模塊被重復(fù)使用,并使它們更易于維護和擴展,因此它們在應(yīng)用程序中非常有用。插件機制允許管理員在需要時輕松安裝和卸載插件,而無需對基礎(chǔ)應(yīng)用程序做出更改。
NDD介紹
這里再介紹推薦下優(yōu)秀的國產(chǎn)軟件開源項目 NDD(notepad--)。一個支持windows/linux/mac的文本編輯器,目標(biāo)是要國產(chǎn)替換同類軟件。對比其它競品Notepad類軟件而言,優(yōu)勢是可以跨平臺,支持linux mac操作系統(tǒng)。期待國人參與開源,貢獻更多有意思的插件。
gitee倉庫地址:https://gitee.com/cxasm/notepad--

插件的優(yōu)勢
基于插件的擴展性,進而實現(xiàn)業(yè)務(wù)模塊兒的獨立和解耦,增加可維護性和可擴展性。插件使得第三方開發(fā)人員可以為系統(tǒng)做增值和拓展工作,也可以使其他開發(fā)人員協(xié)同開發(fā)相互配合,增加新的功能而不破壞現(xiàn)有的核心功能。插件化還能夠促進將關(guān)注點分開,保證隱藏實現(xiàn)細節(jié),且可以將測試獨立開來,并最具有實踐意義。
比如強大的Eclipse的平臺實際上就是一個所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java開發(fā)環(huán)境)僅僅是一系列掛在核心框架上的插件。
NDD的插件化實現(xiàn),是一種很好的范例,讓我們看到插件化機制的好處,可以靈活的對軟件進行功能拓展,以下對NDD的插件化實現(xiàn)原理做下分析。
NDD插件機制分析
用C++實現(xiàn)插件機制的基本思路是:
一、應(yīng)用程序(框架)提供出插件接口。
二、由用戶或第三方實現(xiàn)這些接口,并編譯出相應(yīng)的動態(tài)庫(即插件);
三、將所有插件放到某個特定目錄,應(yīng)用程序(框架)運行時會自動搜索該目錄,并動態(tài)加載目錄中的插件。
按照以上思路,分析下NDD源碼中的插件機制實現(xiàn)。
插件接口
NDD源碼中提供出來的插件接口有兩個,接口聲明如下:
#define NDD_EXPORT __declspec(dllexport)
#ifdef __cplusplus
extern "C" {
#endif
NDD_EXPORT bool NDD_PROC_IDENTIFY(NDD_PROC_DATA* pProcData);
NDD_EXPORT int NDD_PROC_MAIN(QWidget* pNotepad, const QString& strFileName, std::function<QsciScintilla* ()>getCurEdit, NDD_PROC_DATA* procData);
#ifdef __cplusplus
}
#endif需要注意,插件接口必須要用extern "C"包含,因為C++的編譯器會對程序中符號進行修飾,這個過程在編譯器中叫符號修飾(Name Decoration)或者符號改編(Name Mangling)。如果不改為c的方式,那么動態(tài)庫resolve這種查找入口方式,會找不到句柄handle入口。
以上兩個接口,一個是插件的相關(guān)說明信息,一個是插件的核心功能實現(xiàn)。
插件實現(xiàn)
NDD_PROC_IDENTIFY接口最簡單,就是用來讓插件開發(fā)者填充插件信息用的。傳進來的參數(shù)有以下信息:
struct ndd_proc_data
{
QString m_strPlugName; //插件名稱 必選
QString m_strFilePath; //lib 插件的全局路徑。必選。插件內(nèi)部不用管,主程序傳遞下來
QString m_strComment; //插件說明
QString m_version; //版本號碼??蛇x
QString m_auther;//作者名稱??蛇x
int m_menuType;//菜單類型。0:不使用二級菜單 1:創(chuàng)建二級菜單
QMenu* m_rootMenu;//如果m_menuType = 1,給出二級根菜單的地址。其他值nullptr
ndd_proc_data(): m_rootMenu(nullptr), m_menuType(0)
{
}
};
typedef struct ndd_proc_data NDD_PROC_DATA;bool NDD_PROC_IDENTIFY(NDD_PROC_DATA* pProcData)
{
if(pProcData == NULL)
{
return false;
}
pProcData->m_strPlugName = QObject::tr("Hello World Plug");
pProcData->m_strComment = QObject::tr("char to Upper.");
pProcData->m_version = QString("v1.0");
pProcData->m_auther = QString("yangqq.xyz");
pProcData->m_menuType = 1;
return true;
}另外一個接口是NDD_PROC_MAIN這個是插件功能的具體實現(xiàn)接口,插件開發(fā)者可在此接口中實現(xiàn)插件的主要功能。
//插件的入口點接口實現(xiàn)
//則點擊菜單欄按鈕時,會自動調(diào)用到該插件的入口點函數(shù)接口。
//pNotepad:就是CCNotepad的主界面指針
//strFileName:當(dāng)前插件DLL的全路徑,如果不關(guān)心,則可以不使用
//getCurEdit:從NDD主程序傳遞過來的仿函數(shù),通過該函數(shù)獲取當(dāng)前編輯框操作對象QsciScintilla
int NDD_PROC_MAIN(QWidget* pNotepad, const QString &strFileName, std::function<QsciScintilla*()>getCurEdit, NDD_PROC_DATA* pProcData)
{
//對于不需要創(chuàng)建二級菜單的例子,pProcData總是nullptr。
//該函數(shù)每次點擊插件菜單時,都會被執(zhí)行。
QsciScintilla* pEdit = getCurEdit();
if (pEdit == nullptr)
{
return -1;
}
//務(wù)必拷貝一份pProcData,在外面會釋放。
if (pProcData != nullptr)
{
s_procData = *pProcData;
}
s_pMainNotepad = pNotepad;
s_getCurEdit = getCurEdit;
//做一個簡單的轉(zhuǎn)大寫的操作
QtTestClass* p = new QtTestClass(pNotepad,pEdit);
//主窗口關(guān)閉時,子窗口也關(guān)閉。避免空指針操作
p->setWindowFlag(Qt::Window);
p->show();
return 0;
}
完成了以上這兩個接口,編譯成動態(tài)dll庫,其實插件開發(fā)就完成啦。如果編譯器和使用的QT庫同NDD發(fā)行版一致,則直接把dll庫放入plugin目錄即可。接下來看下NDD應(yīng)用程序是如何加載和使用插件的。
NDD插件加載過程
從ndd應(yīng)用程序啟動到插件加載。過程大致如下:
int main(int argc, char *argv[])
{
//可以防止某些屏幕下的字體擁擠重疊問題
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#ifdef Q_OS_MAC
MyApplication a(argc, argv);
#else
QApplication a(argc, argv);
#endif
//......
CCNotePad *pMainNotepad = new CCNotePad(true);
pMainNotepad->setAttribute(Qt::WA_DeleteOnClose);
pMainNotepad->setShareMem(&shared);
pMainNotepad->quickshow();
a.exec();
}
//
//先快速讓窗口展示處理,后續(xù)再去做復(fù)雜的初始化
void CCNotePad::quickshow()
{
//......
init_toolsMenu();
}
//
void CCNotePad::init_toolsMenu()
{
slot_dynamicLoadToolMenu();
//connect(ui.menuTools,&QMenu::aboutToShow,this,&CCNotePad::slot_dynamicLoadToolMenu);
}
//動態(tài)加載工具菜單項
void CCNotePad::slot_dynamicLoadToolMenu()
{
//......
#ifdef NO_PLUGIN
//動態(tài)加載插件
m_pluginList.clear();
loadPluginLib();
#endif
}
插件的加載過程在loadPluginLib()函數(shù)中,進入到plugin目錄中加載插件。
#ifdef NO_PLUGIN
void CCNotePad::loadPluginLib()
{
QString strDir = qApp->applicationDirPath();
QDir dir(strDir);
if (dir.cd("./plugin"))
{
strDir = dir.absolutePath();
loadPluginProcs(strDir,ui.menuPlugin);
}
}foundCallback回調(diào)函數(shù)接口,找到插件信息后 在onPlugFound函數(shù)中處理,完成與界面菜單的綁定。
void CCNotePad::loadPluginProcs(QString strLibDir, QMenu* pMenu)
{
std::function<void(NDD_PROC_DATA&, QMenu*)> foundCallBack = std::bind(&CCNotePad::onPlugFound, this, std::placeholders::_1, std::placeholders::_2);
int nRet = loadProc(strLibDir, foundCallBack, pMenu);
if (nRet > 0)
{
ui.statusBar->showMessage(tr("load plugin in dir %1 success, plugin num %2").arg(strLibDir).arg(nRet));
}
}在點擊菜單后觸發(fā)執(zhí)行onPlugWork,如果設(shè)置的有啟用二級菜單,則初始化設(shè)置二級菜單。
void CCNotePad::onPlugFound(NDD_PROC_DATA& procData, QMenu* pUserData)
{
QMenu* pMenu = pUserData;
if (pMenu == NULL)
{
return;
}
//創(chuàng)建action
if (procData.m_menuType == 0)
{
QAction* pAction = new QAction(procData.m_strPlugName, pMenu);
pMenu->addAction(pAction);
pAction->setText(procData.m_strPlugName);
pAction->setData(procData.m_strFilePath);
connect(pAction, &QAction::triggered, this, &CCNotePad::onPlugWork);
}
else if (procData.m_menuType == 1)
{
//創(chuàng)建二級菜單
QMenu* pluginMenu = new QMenu(procData.m_strPlugName, pMenu);
pMenu->addMenu(pluginMenu);
//菜單句柄通過procData傳遞到插件中
procData.m_rootMenu = pluginMenu;
sendParaToPlugin(procData);
}
else
{
return;
}
// 暫存加載到的插件信息
m_pluginList.append(procData);
}
//把插件需要的參數(shù),傳遞到插件中去
void CCNotePad::sendParaToPlugin(NDD_PROC_DATA& procData)
{
QString plugPath = procData.m_strFilePath;
QLibrary* pLib = new QLibrary(plugPath);
NDD_PROC_MAIN_CALLBACK pMainCallBack;
pMainCallBack = (NDD_PROC_MAIN_CALLBACK)pLib->resolve("NDD_PROC_MAIN");
if (pMainCallBack != NULL)
{
std::function<QsciScintilla* ()> foundCallBack = std::bind(&CCNotePad::getCurEditView, this);
pMainCallBack(this, plugPath, foundCallBack, &procData);
}
else
{
ui.statusBar->showMessage(tr("plugin %1 load failed !").arg(plugPath), 10000);
}
}//真正執(zhí)行插件的工作
void CCNotePad::onPlugWork(bool check)
{
QAction* pAct = dynamic_cast<QAction*>(sender());
if (pAct != nullptr)
{
QString plugPath = pAct->data().toString();
QLibrary* pLib = new QLibrary(plugPath);
NDD_PROC_MAIN_CALLBACK pMainCallBack;
pMainCallBack = (NDD_PROC_MAIN_CALLBACK)pLib->resolve("NDD_PROC_MAIN");
if (pMainCallBack != NULL)
{
std::function<QsciScintilla* ()> foundCallBack = std::bind(&CCNotePad::getCurEditView, this);
pMainCallBack(this, plugPath, foundCallBack, nullptr);
}
else
{
ui.statusBar->showMessage(tr("plugin %1 load failed !").arg(plugPath), 10000);
}
}
}雖然以上過程看似復(fù)雜一點兒,其實關(guān)鍵調(diào)用就是拿到函數(shù)指針,然后根據(jù)需要做些處理。插件信息存儲在QList<NDD_PROC_DATA> m_pluginList。有個界面對這個信息進行展示。
void CCNotePad::slot_pluginMgr()
{
#ifdef NO_PLUGIN
PluginMgr* pWin = new PluginMgr(this, m_pluginList);
pWin->setAttribute(Qt::WA_DeleteOnClose);
pWin->show();
#else
QMessageBox::warning(this, "info", u8"便攜版本不支持插件,請下載插件版!");
#endif
}為防止中文亂碼,支持中文的方法是文件編碼保存為utf-8格式。 輸入漢字如上寫法,u8"中文字符"。編譯腳本指定如下:
# win下需要開啟UNICODE進行支持TCHAR
if(CMAKE_HOST_WIN32)
add_definitions(-D_UNICODE -DUNICODE)
endif()plugin機制的關(guān)鍵,既定義函數(shù)指針,拿到函數(shù)指針,使用函數(shù)指針。
typedef bool (*NDD_PROC_IDENTIFY_CALLBACK)(NDD_PROC_DATA* pProcData); typedef void (*NDD_PROC_FOUND_CALLBACK)(NDD_PROC_DATA* pProcData, void* pUserData);
#include "plugin.h"
#include <QLibrary>
#include <QDir>
#include <QMenu>
#include <QAction>
bool loadApplication(const QString& strFileName, NDD_PROC_DATA* pProcData)
{
QLibrary lib(strFileName);
NDD_PROC_IDENTIFY_CALLBACK procCallBack;
procCallBack = (NDD_PROC_IDENTIFY_CALLBACK)lib.resolve("NDD_PROC_IDENTIFY");
if (procCallBack == NULL)
{
return false;
}
if (!procCallBack(pProcData))
{
return false;
}
pProcData->m_strFilePath = strFileName;
return true;
}
int loadProc(const QString& strDirOut, std::function<void(NDD_PROC_DATA&, QMenu*)> funcallback, QMenu* pUserData)
{
int nReturn = 0;
QStringList list;
QDir dir;
dir.setPath(strDirOut);
QString strDir, strName;
QStringList strFilter;
strDir = dir.absolutePath();
strDir += QDir::separator();
#if defined(Q_OS_WIN)
strFilter << "*.dll";
#else
strFilter << "lib*.so";
#endif
list = dir.entryList(strFilter, QDir::Files | QDir::Readable, QDir::Name);
QStringList::Iterator it = list.begin();
for (; it != list.end(); ++it)
{
NDD_PROC_DATA procData;
strName = *it;
strName = strDir + strName;
if (!loadApplication(strName, &procData))
{
continue;
}
funcallback(procData, pUserData);
nReturn++;
}
return nReturn;
}
到此這篇關(guān)于C++插件化 NDD源碼的插件機制實現(xiàn)解析的文章就介紹到這了,更多相關(guān)c++ NDD源碼插件機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解VS2019 dumpbin查看DLL的導(dǎo)出函數(shù)
這篇文章主要介紹了詳解VS2019 dumpbin查看DLL的導(dǎo)出函數(shù),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
C++實現(xiàn)一個線程安全的單例工廠實現(xiàn)代碼
這篇文章主要介紹了 C++實現(xiàn)一個線程安全的單例工廠實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05
詳解數(shù)據(jù)結(jié)構(gòu)C語言實現(xiàn)之循環(huán)隊列
在我們生活中有很多隊列的影子,可以說與時間相關(guān)的問題,一般都會涉及到隊列問題;本文詳細介紹了如何使用C語言實現(xiàn)循環(huán)隊列,下面一起來看看。2016-07-07

