首頁 > 軟體

C++外掛化 NDD原始碼的外掛機制實現解析

2023-03-21 06:01:52

外掛機制是一種框架,允許開發人員簡單地在應用程式中新增或擴充套件功能。它使廣泛使用,因為它可以作為模組被重複使用,並使它們更易於維護和擴充套件,因此它們在應用程式中非常有用。外掛機制允許管理員在需要時輕鬆安裝和解除安裝外掛,而無需對基礎應用程式做出更改。

NDD介紹

這裡再介紹推薦下優秀的國產軟體開源專案 NDD(notepad--)。一個支援windows/linux/mac的文字編輯器,目標是要國產替換同類軟體。對比其它競品Notepad類軟體而言,優勢是可以跨平臺,支援linux mac作業系統。期待國人蔘與開源,貢獻更多有意思的外掛。

gitee倉庫地址:https://gitee.com/cxasm/notepad--

 外掛的優勢

基於外掛的擴充套件性,進而實現業務模組兒的獨立和解耦,增加可維護性和可延伸性。外掛使得第三方開發人員可以為系統做增值和拓展工作,也可以使其他開發人員協同開發相互配合,增加新的功能而不破壞現有的核心功能。外掛化還能夠促進將關注點分開,保證隱藏實現細節,且可以將測試獨立開來,並最具有實踐意義。

比如強大的Eclipse的平臺實際上就是一個所有功能都由外掛提供的骨架。Eclipse IDE自身(包括UI和Java開發環境)僅僅是一系列掛在核心框架上的外掛。

NDD的外掛化實現,是一種很好的範例,讓我們看到外掛化機制的好處,可以靈活的對軟體進行功能拓展,以下對NDD的外掛化實現原理做下分析。

NDD外掛機制分析

用C++實現外掛機制的基本思路是:

一、應用程式(框架)提供出外掛介面。

二、由使用者或第三方實現這些介面,並編譯出相應的動態庫(即外掛);

三、將所有外掛放到某個特定目錄,應用程式(框架)執行時會自動搜尋該目錄,並動態載入目錄中的外掛。

按照以上思路,分析下NDD原始碼中的外掛機制實現。

外掛介面

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的方式,那麼動態庫resolve這種查詢入口方式,會找不到控制程式碼handle入口。

以上兩個介面,一個是外掛的相關說明資訊,一個是外掛的核心功能實現。

外掛實現

NDD_PROC_IDENTIFY介面最簡單,就是用來讓外掛開發者填充外掛資訊用的。傳進來的引數有以下資訊:

struct ndd_proc_data
{
	QString m_strPlugName; //外掛名稱 必選
	QString m_strFilePath; //lib 外掛的全域性路徑。必選。外掛內部不用管,主程式傳遞下來
	QString m_strComment; //外掛說明
	QString m_version; //版本號碼。可選
	QString m_auther;//作者名稱。可選
	int m_menuType;//選單型別。0:不使用二級選單 1:建立二級選單
	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這個是外掛功能的具體實現介面,外掛開發者可在此介面中實現外掛的主要功能。

//外掛的入口點介面實現
//則點選選單欄按鈕時,會自動呼叫到該外掛的入口點函數介面。
//pNotepad:就是CCNotepad的主介面指標
//strFileName:當前外掛DLL的全路徑,如果不關心,則可以不使用
//getCurEdit:從NDD主程式傳遞過來的仿函數,通過該函數獲取當前編輯框操作物件QsciScintilla
int NDD_PROC_MAIN(QWidget* pNotepad, const QString &strFileName, std::function<QsciScintilla*()>getCurEdit, NDD_PROC_DATA* pProcData)
{
    //對於不需要建立二級選單的例子,pProcData總是nullptr。
    //該函數每次點選外掛選單時,都會被執行。
    QsciScintilla* pEdit = getCurEdit();
    if (pEdit == nullptr)
    {
	    return -1;
    }
 
	//務必拷貝一份pProcData,在外面會釋放。
	if (pProcData != nullptr)
	{
		s_procData = *pProcData;
	}
 
	s_pMainNotepad = pNotepad;
	s_getCurEdit = getCurEdit;
 
	//做一個簡單的轉大寫的操作
	QtTestClass* p = new QtTestClass(pNotepad,pEdit);
	//主視窗關閉時,子視窗也關閉。避免空指標操作
	p->setWindowFlag(Qt::Window);
	p->show();
 
	return 0;
}

完成了以上這兩個介面,編譯成動態dll庫,其實外掛開發就完成啦。如果編譯器和使用的QT庫同NDD發行版一致,則直接把dll庫放入plugin目錄即可。接下來看下NDD應用程式是如何載入和使用外掛的。

NDD外掛載入過程

從ndd應用程式啟動到外掛載入。過程大致如下:

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();
 
}
//
//先快速讓視窗展示處理,後續再去做複雜的初始化
void CCNotePad::quickshow()
{
    //......
    init_toolsMenu();
}
//
void CCNotePad::init_toolsMenu()
{
	slot_dynamicLoadToolMenu();
	//connect(ui.menuTools,&QMenu::aboutToShow,this,&CCNotePad::slot_dynamicLoadToolMenu);
}
//動態載入工具選單項
void CCNotePad::slot_dynamicLoadToolMenu()
{
 //......
#ifdef NO_PLUGIN
	//動態載入外掛
	m_pluginList.clear();
	loadPluginLib();
#endif
}

外掛的載入過程在loadPluginLib()函數中,進入到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回撥函數介面,找到外掛資訊後 在onPlugFound函數中處理,完成與介面選單的繫結。

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));
	}
}

在點選選單後觸發執行onPlugWork,如果設定的有啟用二級選單,則初始化設定二級選單。

void CCNotePad::onPlugFound(NDD_PROC_DATA& procData, QMenu* pUserData)
{
	QMenu* pMenu = pUserData;
 
	if (pMenu == NULL)
	{
		return;
	}
 
	//建立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)
	{
		//建立二級選單
		QMenu* pluginMenu = new QMenu(procData.m_strPlugName, pMenu);
		pMenu->addMenu(pluginMenu);
 
		//選單控制程式碼通過procData傳遞到外掛中
		procData.m_rootMenu = pluginMenu;
		sendParaToPlugin(procData);
	}
	else
	{
		return;
	}
    // 暫存載入到的外掛資訊
	m_pluginList.append(procData);
}
//把外掛需要的引數,傳遞到外掛中去
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);
		}
}
//真正執行外掛的工作
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);
		}
	
	}
}

雖然以上過程看似複雜一點兒,其實關鍵呼叫就是拿到函數指標,然後根據需要做些處理。外掛資訊儲存在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機制的關鍵,既定義函數指標,拿到函數指標,使用函數指標。 

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;
}

到此這篇關於C++外掛化 NDD原始碼的外掛機制實現解析的文章就介紹到這了,更多相關c++ NDD原始碼外掛機制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com