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

本文不打算严格地、用标准术语来讲前因后果。本文主要分析实践中常见的、因为对原理不清楚而搞出来的产品里的坑。

什么是插件模式和为什么要用插件模式

插件,Plug-In,或者(IE/Edge称之为)加载项/Add-On,(Office称之为)外接程序/Add-In,(GIMP称之为)扩展/Extension,等等,总之看字面意思都是“额外增加功能”的这种东西,是一类开发模式。基本思路就是,研发软件本体的时候,外部需求不明确、直到使用期仍然经常会增加功能细节。为了把变动部分切割开,在设计的时候,通过对可变部分的归纳分析,对可变部分抽象出一套接口;每套外部需求用动态库之类的形式实现接口;软件本体按某种约定,加载动态库,并从中获取插件实例,通过接口来调用满足当时需求的功能实现。

可以看到,插件的思想,其实就是灵活运用“动态库”的动态加载能力,把对“接口”的实现移到软件本体之外,并用工厂模式来约束动态库的实现方式。

只要是具有动态加载能力的运行环境上,都可以使用插件模式来设计软件系统。极端一些的软件系统,甚至只提供基础平台,所有功能都由插件的方式提供,例如 Visual Studio Code 、 Eclipse 等。

C++实现插件模式

用C++实现插件模式,一般是把下面这些功能组合起来:

  • 用一个C++的带虚函数的基类来表示功能
  • 约定动态库里的工厂模式接口
  • 在一些动态库里提供实现虚函数的派生类
  • 在动态库里实现工厂模式

不幸的是,由于各操作系统的动态库机制普遍是C风格的,用C++做动态库时候的坑,在用C++实现插件模式时,全都会遇到。比如:

  • C++的编译器差异导致的不互通
    会导致必须用同一种(或兼容的)编译器来生成插件和软件本体。

    • 名字改写(Name Mangling):参考:Wikipedia、C/C++中的名字空间与作用域示例详解 会导致加载插件时“找不到符号”
    • 虚函数(表)实现:
      会导致加载插件时可能不报错,但运行时候找不到正确的虚函数入口
  • 操作系统机制导致的不互通
    • Windows上使用MSVCRT时的内存分配和回收
      Windows每个模块的内存分配默认是在模块自己的堆里的,而Windows上的C运行时库(各种MSVCRT)为了封装出 malloc 、 free 等C函数的效果,建立了__crtheap(2010及之前版本行为)或直接使用进程默认堆(2015及之后版本行为)[1]。这导致,即使是同一个编译器,静态链接VC运行时会采用本模块内部的堆来实现 malloc 等,而动态链接VC运行时则会采用MSVCRT动态库DLL的模块堆。需要解决好“谁申请谁释放”的问题,否则内存管理的地方容易出异常。
    • 全局变量在模块间的共享问题

一些典型的不良实现

这里说的不良实现,使用时候未必会错或崩,但早晚要崩,或者会限制住插件的开发。以下用如下插件接口作为例子。

// IFilter.h

/// 滤波器接口.
class IFilter {
protected:
    IFilter();
public:
    virtual ~IFilter();
public:
    /// 一个将输入复数数组处理为输出复数数组的函数.
    virtual void Filter(const std::complex<double>* acdIn, std::complex<double>* acdOut, size_t uLen) = 0;
    /// 获取当前实现的一些描述字符串.
    virtual std::string GetDescription() const = 0;
};
// IFilter.cpp
IFilter::IFilter() { }
IFilter::~IFilter() { }

并约定插件实现中以如下形式提供工厂函数。

// FilterPluginDll.h
#include "IFilter.h"
/* 插件DLL应该提供如下函数
extern "C" int GetFilterPluginInDll(char* szFilterNamesBuf, size_t uBufLen);
extern "C" IFilter* BuildFilterPlugin(const char* szFilterName);
extern "C" void FreeFilterPlugin(IFilter* pFilter);
*/
typedef int (*PFNGetFilterPluginInDll)(char* szFilterNamesBuf, size_t uBufLen);
typedef IFilter* (*PFNBuildFilterPlugin)(const char* szFilterName);
typedef void (*PFNFreeFilterPlugin)(IFilter* pFilter);

接口类没有提供二进制实现

比如,对插件只发布两个头文件;认为 IFilter 的构造和析构反正是空函数无所谓,直接写在类定义里。

这样,插件开发者自己生成插件DLL时,会在自己的DLL里链接进一份 IFilter::IFilter() 和 IFilter::~IFilter() 的实现,而软件本体里也有一份自己的实现。虽然看上去,如果编译器一样,两份实现是等同的,但考虑到它们使用了不同的模块堆,以及其它各种原因,插件DLL中的 IFilter 和软件本体里的 IFilter 并不是完全等同的。

这里应该由软件本体导出 IFilter::IFitler() 和 IFilter::~IFilter() 等接口类的共性成分的实现给插件,以免出现一些奇怪的问题。

工厂函数里没有正确设计“谁分配谁释放”

比如,为了“简单”,只要求了 BuildFilterPlugin 工厂函数,认为可以由软件本体用 delete pFilter; 来释放插件实例。

一种建议的实现方法

用类似于Windows的COM风格的“放了一堆函数指针的结构体”来表示插件的接口定义;软件本体里为了使用方便,再用接口类包装一下。

参考文献

到此这篇关于使用C++实现插件模式时的避坑要点的文章就介绍到这了,更多相关c++插件模式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

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

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

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

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

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

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

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

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

  • ant-design-vue 快速避坑指南(推荐)

    ant-design-vue是蚂蚁金服 Ant Design 官方唯一推荐的Vue版UI组件库,它其实是Ant Design的Vue实现,组件的风格与Ant Design保持同步,组件的html结构和css样式也保持一致. 用下来发现它的确称得上为数不多的完整的VUE组件库与开发方案集成项目. 本文主要目的是总结一些开发过程中比较耗时间去查找,文档中没有具体说明的常见问题,同时希望能给新上手此框架的同学提供一些参考作用. 1.Table对接后台返回数据 针对Table数据格式与后他接口返回数据格

  • .NET+PostgreSQL实践与避坑指南(推荐)

    简介 .NET+PostgreSQL(简称PG)这个组合我已经用了蛮长的一段时间,感觉还是挺不错的.不过大多数人说起.NET平台,还是会想起跟它"原汁原味"配套的Microsoft SQL Server(简称MSSQL),其实没有MSSQL也没有任何问题,甚至没有Windows Server都没问题,谁说用.NET就一定要上微软全家桶?这都什么年代了-- PG和MSSQL的具体比较我就不详细展开了,自行搜一下,这种比较分析文章很多.应该说两个RDBMS各有特色,MSSQL工具集庞大(大

  • Sublime Text3安装Go语言相关插件gosublime时搜不到gosublime的解决方法

    在windows下golang开发环境搭建时(具体步骤网上很多,百度搜索括号前的文字即可找到),遇到了很尴尬的情况(如下图),百度也没有找到想类似的情况, 解决办法 (共3步): (这也是手动安装插件的过程) 第一步: 从 工具栏--- Preferences----可以看到 Browse Package(第一个就是),打开后,即找到了插件安装路径,如: C:\Users.....AppData\Roaming\Sublime Text 3\Packages (如果有git,可用这个“简化方法”

  • Python爬虫程序中使用生产者与消费者模式时进程过早退出的问题

    之前写爬虫程序的时候,采用生产者和消费者的模式,利用Queue作为生产者进程和消费者进程之间的同步队列. 执行程序时,总是秒退,加了断点也无法中断,加打印也无法输出,我知道肯定是进程退出了,但还是百思不得解,为什么会这么快就退出. 一开始以为是我的进程代码写的有问题,在某个地方崩溃导致程序提前退出,排查了一遍又一遍,并没有发现什么明显的问题,后来走读代码,看到主模块中消费者和生产者进程的启动后,发现了问题,原因是我通过start()方法启动进程后,使用join()的方式有问题.消费者进程必须执行

  • gtoken替换jwt实现sso登录的排雷避坑

    目录 前言 gtoken gtoken替换jwt解决的问题 兼容JWT gtoken实现原理 踩坑之旅 1 gtoken版本 2 gtoken存储问题 3 不能跨环境使用token 4 测试账号不规范问题 总结 前言 前段时间整理的文章:gtoken替换jwt实现sso登录 | 带你读源码 收到了大家积极的反馈,gtoken替换jwt实现sso登录的开发过程是比较稳健的,但是在我们测试联调的过程中暴露出了很多问题. 如果大家也想使用gtoken替换jwt实现sso登录,那么这篇文章可以减少很多大

  • java开发使用BigDecimal避坑四则

    目录 引言 第一:浮点类型的坑 第二:浮点精度的坑 第三:设置精度的坑 第四:三种字符串输出的坑 小结 引言 在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨.掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的. 第一:浮点类型的坑 在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float.Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值. 比如下面的代码: @Test p

  • 小心!ASP.NET网站发布时的那些坑

    开发工具:VS2010,MVC4.0,SQLSERVER2008 服务器:Windows server 2012,IIS8,SQLSERVER2012 一.发布后,每个页面第一次打开都很卡,50秒或更长,第二次打开就很快了 估计原因:编译速度慢,但在有VS环境的机器上发布没有这么卡 解决办法: 使用高版本的VS,"在发布期间预编译": 使用IIS8的Application Initialization功能 1. 使用VS2017"在发布期间预编译" 发布时遇到一个报

  • MyBatis一级缓存避坑完全指南

    一级缓存概念 当我们使用Mybatis进行数据库的操作时候,会创建一个SqlSession来进行一次数据库的会话,会话结束则关闭SqlSession对象.那么一个SqlSession的生命周期即对应于Mybatis的一次会话.在Mybatis的一次会话中,我们很有可能多次查询完全相同的sql语句,如果不采取措施的话,每一次查询都查询一次数据库.而一次会话时间一般都是极短的,相同Sql的查询结果极有可能完全相同.由于查询数据库代价是比较大的,这会导致系统的资源浪费. 为了解决这个问题,Mybati

  • Linux下安装Python3.6及避坑指南

    Python3的安装 1.安装依赖环境 Python3在安装的过程中可能会用到各种依赖库,所以在正式安装Python3之前,需要将这些依赖库先行安装好. yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel 2. 下载Python3源代码 下载Python3的

随机推荐