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

简介: c++插件化 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;
}


相关文章
|
2月前
|
C++
基本二叉树与排序二叉树(C++源码)
本程序实现二叉树基本操作与二叉排序树应用。支持前序建树、四种遍历、求深度、叶子数、第K层节点数及查找功能;并实现二叉排序树的构建、中序输出与查找比较次数统计,分析不同插入顺序对树形态和查找效率的影响。
|
2月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
7月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
174 2
|
9月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
267 2
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
176 4
|
11月前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
12586 46
|
10月前
|
存储 监控 算法
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
在数字化办公时代,公司监控上网软件成为企业管理网络资源和保障信息安全的关键工具。本文深入剖析C++中的链表数据结构及其在该软件中的应用。链表通过节点存储网络访问记录,具备高效插入、删除操作及节省内存的优势,助力企业实时追踪员工上网行为,提升运营效率并降低安全风险。示例代码展示了如何用C++实现链表记录上网行为,并模拟发送至服务器。链表为公司监控上网软件提供了灵活高效的数据管理方式,但实际开发还需考虑安全性、隐私保护等多方面因素。
210 0
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
|
12月前
|
存储 算法 安全
基于红黑树的局域网上网行为控制C++ 算法解析
在当今网络环境中,局域网上网行为控制对企业和学校至关重要。本文探讨了一种基于红黑树数据结构的高效算法,用于管理用户的上网行为,如IP地址、上网时长、访问网站类别和流量使用情况。通过红黑树的自平衡特性,确保了高效的查找、插入和删除操作。文中提供了C++代码示例,展示了如何实现该算法,并强调其在网络管理中的应用价值。
|
10月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。

推荐镜像

更多
  • DNS