C++插件化 NDD源码的插件机制实现解析

目录
  • NDD介绍
  • 插件的优势
  • NDD插件机制分析
    • 插件接口
    • 插件实现
    • NDD插件加载过程

插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能。它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用。插件机制允许管理员在需要时轻松安装和卸载插件,而无需对基础应用程序做出更改。

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源码插件机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++实现简单插件机制原理解析

    在我做的第一个页游项目中,服务器使用了插件的机制,但是当时的插件都是用C#写,而且如何实现的也不是很清楚.之后的几个页游项目都是自己一个人包揽服务器部分,所以一直没有写插件的需求.下一个页游项目服务器这边需要多人合作,因此我想把其他模块都独立的做成插件的模式,目前也是在探索阶段.通过网上资料查找以及自己的整理,实现了一个简单版本的插件机制.实现代码如下: 文件Object.hpp中实现了所有插件类的基类,所有插件都要继承该类. #ifndef __OBJECT_HPP__ #define __O

  • 使用C++实现插件模式时的避坑要点(推荐)

    本文不打算严格地.用标准术语来讲前因后果.本文主要分析实践中常见的.因为对原理不清楚而搞出来的产品里的坑. 什么是插件模式和为什么要用插件模式 插件,Plug-In,或者(IE/Edge称之为)加载项/Add-On,(Office称之为)外接程序/Add-In,(GIMP称之为)扩展/Extension,等等,总之看字面意思都是“额外增加功能”的这种东西,是一类开发模式.基本思路就是,研发软件本体的时候,外部需求不明确.直到使用期仍然经常会增加功能细节.为了把变动部分切割开,在设计的时候,通过对

  • Ubuntu中使用VS Code与安装C/C++插件的教程详解

    优麒麟Ubuntu20.04中使用VS Code. VS Code的版本是1.48.0. 以下内容仅限于上述环境,对于Windows环境下的使用虽然类似,只能参考使用. 1.准备 a.软件安装 可以通过软件商店进行安装,也可以下载安装. 下载安装的官网地址:https://code.visualstudio.com/. 可以下载deb和rpm两种格式的安装软件. b.目录计划 编写程序过程中,需要事先决定编写的程序放在哪里,这就需要做一个目录计划.任何一个软件项目也都需要做目录规划,以便所写的程

  • vscode使用官方C/C++插件无法进行代码格式化问题

    官方的C/C++插件是支持使用.clang-format配置文件进行自定义风格代码格式化的,无需另外安装clang-format插件. 但是使用clang-format -style=llvm -dump-config > .clang-format导出的默认配置文件进行格式化的时候会有一些小问题.如图 根据输出窗口的提示,是第94行出现了无法识别的key: 将.clang-format中第93~96行的代码加上注释就解决了: 至于为啥,笔者估计是官方插件还不支持RawStringFormats

  • C++插件化 NDD源码的插件机制实现解析

    目录 NDD介绍 插件的优势 NDD插件机制分析 插件接口 插件实现 NDD插件加载过程 插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能.它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用.插件机制允许管理员在需要时轻松安装和卸载插件,而无需对基础应用程序做出更改. NDD介绍 这里再介绍推荐下优秀的国产软件开源项目 NDD(notepad--).一个支持windows/linux/mac的文本编辑器,目标是要国产替换同类软件.对

  • jQuery自动完成插件completer附源码下载

    我们在表单输入的时候,如要输入邮箱之类,只需输入邮箱名前段,那么@后面的内容将自动补全,将常用的几个邮箱列出来,用户只需选择一下就可以完成表单的输入.诸如此类的还有时间的输入.域名的输入都能自动完成,这些都由插件completer来帮您实现. 效果展示图如下所示: 效果展示       源码下载 HTML 首先载入jQuery库和插件completer.js,当然还有相关CSS样式文件,这些都在源码下载里已打包好了. <script src="jquery.js"><

  • webpack源码之loader机制详解

    loader概念 loader是用来加载处理各种形式的资源,本质上是一个函数, 接受文件作为参数,返回转化后的结构. loader 用于对模块的源代码进行转换.loader 可以使你在 import 或"加载"模块时预处理文件.因此,loader 类似于其他构建工具中"任务(task)",并提供了处理前端构建步骤的强大方法.loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL.loader

  • 深入理解Vue.js源码之事件机制

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出

  • vue源码nextTick使用及原理解析

    1 nextTick的使用 vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码. <template> <div class="box">{{msg}}</div> </template> export default { name: 'index', data () { return { msg: 'hello' } }, mount

  • Python Request类源码实现方法及原理解析

    通过APIView进入找到Request的源码 可以看见一堆属性和方法,其中request.data其实是一个方法,被包装成一个属性 继续看__getattr__和query_params方法: 代码总结: Request其实就是原生request对象被包装后的Request,即重写了__getattr__,return getattr(self._request, attr) 比如:print(request.GET)就当于print(request.query_params) 以上就是本文的

  • JDK源码之Vector与HashSet解析

    Vector简介 ArrayList 和 Vector 其实大同小异,基本结构都差不多,但是一些细节上有区别:比如线程安全与否,扩容的大小等,Vector的线程安全通过在方法上直接加synchronized实现.扩容默认扩大为原来的2倍. 继承体系 从图中我们可以看出:Vector继承了AbstractList,实现了List,RandomAccess,Cloneable,Serializable接口,因此Vector支持快速随机访问,可以被克隆,支持序列化. Vector的成员变量(属性) /

  • 移动端手指放大缩小插件与js源码

    本文实例为大家分享了移动端手指放大缩小的具体代码,供大家参考,具体内容如下 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, use

  • 6款经典实用的jQuery小插件及源码(对话框/提示工具等等)

    jQuery是一款当之无愧的JS开源框架,凭借其小巧实用占领了很大的开发市场,jQuery另外一个优势就是拥有丰富多彩的插件,这些插件可以帮助你简化很多的开发过程.下面介绍的6款实用jQuery小插件及源码,希望大家喜欢. 1.jQuery智能单词提示工具 这是一款可以根据当前输入智能提示单词的插件,效果很棒. 在线演示 /源码下载 2.CSS3实现的Photoshop按钮效果 这是用jQuery和CSS3实现的Photoshop按钮效果. 在线演示 /源码下载 3.jQuery自定义确认对话框

  • 亲自动手实现Android App插件化

    Android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好. 下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案. 按照需要插件化主要解决下面的几种问题: 1. 代码的加载 (1) 要解决纯Java代码的加载 (2) Android组件加载,如Activity.Service.Broadcast Receiver.ContentProvider,因为它们是有生命周期的,所以要特殊处理 (3) Android Native代码的加载 (4) Andro

随机推荐