公司动态
学习记录:使用Qt在动态库之间实现信息传递
这是一篇学习记录关于如何在动态库之间进行数据传递。动态库在程序开发中的优势很明显可以极大提高开发效率降低模块之间耦合度但动态库之间实现数据交互也是常见需求。在Qt中实现动态库之间的数据交换有多种方式基于信号和槽机制是主要方式之一。信号和槽机制通常在类之间使用但在Qt的动态库之间仍然可以使用基于此机制的具体实现方案也同样有多种方式比如可以直接在主程序中包含动态库头文件在编译时连接动态库就可以直接进行库之间的信号传递这是Qt最常用的、也是最简单的一种动态库调用方法。也可以使用QLibrary加载动态库但任需要包含动态库头文件并在动态库中以C风格导出接口函数供主程序调用创建动态库对象。还有一种是利用Qt插件实现具体不再赘述。以上实现方式其实都没有离开Qt的信号和槽机制。不可否认信号和槽机制为程序开发提供了极大方便简化了程序开发但一个不可避免的限制就是主程序和动态库的开发不能离开Qt操作。如果对其它动态库有这样的需求实现这种方式仍然有其局限。因此要想这种需求实现能有更大范围的适应性还是得考虑C和C最初的实现机制。C和C没有信号和槽机制它的动态库之间的数据传递是通过回调函数实现的以下便是对回调函数实现的具体学习记录。在C中能够实现回调函数的基础是函数名同样是一个指针它也同样可以作为参数在函数之间进行传递。或许再解释一下对回调这个词的理解。回调字面意思像是回头调用实际上是在对象A中定义一个对象B对象A可以直接访问B中公有成员这是很自然的但是在B中并不能直接访问A这是不允许的逻辑上也不可实现因为A在B中是不可见的。但是我们不可避免会存在当B中的工作执行完成之后需要告知A这样的需求而实现这样需求的操作就是回调即在A创建了B在B中实现对A的调用。从上面对回调的解释可以发现在动态库之间实现信息传递与回调是一样的。我们在主程序中加载了动态库但要在动态库中向主程序传递信息好比主程序就是对象A动态库是对象B。如果没有回调一般情况下动态库是不可能访问主程序中的函数的那么动态库也就无法向主程序传递信息。我们前面已经说了上述需求可以通过回调来实现那么回调具体在动态库之间是如何操作的呢这里需要注意一点类和类之间同样存在回调只是具体实现操作相对简单一点而在动态库之间稍微麻烦一点。我们以一个简单的操作链路来描述这一操作过程它的核心思想就是传递函数指针。首先我们在动态库B中导出一个可供主程序A访问的函数函数名称就叫zhuce()其功能就是把A中的一个函数指针传递给动态库B然后在A中调用zhuce()函数将A中的一个函数比如caozuo()。它的函数名caozuo作为zhuce()的参数。zhuce()函数的具体实现代码是在动态库B中编写的其功能就是在A调用该函数时将传进来的参数caozuo函数的指针存下来所以在B中还需要定义一个函数类型指针变量fun_address用于存放caozuo的指针。在B获得函数caozuo的指针后也就代表A成功在B中注册了函数caozuo。那么在B中则可以通过fun_address这个存放函数指针的变量来执行函数caozuo的具体操作了。这也就实现了在B中调用A的函数的要求通过向fun_address代表函数caozuo传递参数也就实现了向A传递信息。以上详细阐述了回调实现的核心思想下面给出具体一个实现案例做进一步说明。首先建立一个共用的头文件用于声明一些主程序和动态库共用的定义这样做只是为了简化代码编写这些定义也可而在主程序和动态库编写时分别设置但需要保持形式一致。typedef void (*NotifyCallBackClose) (void *,int code, const QString msg);typedef QWidget* (*OpendllFunction)(QWidget *,QStringList);typedef void (*RegisterClosefunction)(NotifyCallBackClose,void *);extern NotifyCallBackClose outfunctionClose;//存放回调函数指针extern void * outObject;//在这个头文件中给出了5个定义NotifyCallBackClose表示回调函数的类型它的形式就是上文提到的caozuo()函数。NotifyCallBackClose类型是一个没有返回值但自带3个参数的函数3个参数分别是无类型指针、整型、字符串。NotifyCallBackClose类型的形式必须与caozuo()函数保持一致这个需要根据实际情况进行设计。OpendllFunction表示打开动态的函数类型这个函数类型与回调无关此处只是为了导出一个函数供主程序调用后创建动态库实例。它返回一个QWidget指针带QWidget 指针和QStringList两个类型参数。RegisterClosefunction表示注册函数类型它的形式就是上文中的zhuce()函数这是一个没有返回值带有2个参数的函数2个参数分别是NotifyCallBackClose类型和无类型指针。从这个函数类型就可以看出主程序注册函数时将NotifyCallBackClose类型的函数指针传递了出去同时传递了一个无类型指针。这里先说一下为什么要传递一个无类型指针它不是必须的但传递它的目的是为了便于对主程序中的类对象进行访问。也就是说这个无类型指针其实就是主程序的类对象地址。在上述头文件中还声明了两个全局变量这个含义就很简单outfunctionClose用于存放回调函数的地址outObject用于存放主程序的类对象地址。在此处不能对全局变量进行初始化否则在多个.cpp文件包含该头文件时会产生多个全局变量副本全局变量不唯一造成访问混乱。使用时只需在一个.cpp文件中进行初始化即可。动态库的头文件具体实现如下核心是声明两个导出函数供主程序调用。openOrbitSimulateWin函数与OpendllFunction类型保持一致主程序利用OpendllFunction来解析动态库中的openOrbitSimulateWin函数。registerCallBackClose与RegisterClosefunction类型一致主程序利用RegisterClosefunction来解析动态库中registerCallBackClose函数。#include “上述公共头文件”class ORBITSIMULATE_EXPORT OrbitSimulate: public QMainWindow{}extern C{ORBITSIMULATE_EXPORT OrbitSimulate * openOrbitSimulateWin(QWidget *p,QStringList args);ORBITSIMULATE_EXPORT void registerCallBackClose(NotifyCallBackClose cb,void * co);}动态库的.cpp文件实现如下。文件中首先对两个全局变量进行初始化然后是对两个导出函数的具体实现。openOrbitSimulateWin函数用于创建动态库实例并将实例对象返回给主程序。registerCallBackClose函数用于接收回调函数的地址和主程序对象的地址分别存放在两个全局变量中。最后在动态库类的析构函数中我们通过outfunctionClose指针调用来回调函数并将主程序对象指针和其它定义的参数传回去。outfunctionClose可以在类的任何成员函数中调用此处只是在动态库对象析构时告诉主程序一个信息。NotifyCallBackClose outfunctionClose nullptr;//存放回调函数指针void * outObject nullptr;//OrbitSimulate *openOrbitSimulateWin(QWidget *p, QStringList args){if(args.count() 0)//调用此库时必须传入至少一个数据库路径参数return new OrbitSimulate(p,args.at(0));//新建一个实例elsereturn nullptr;}void registerCallBackClose(NotifyCallBackClose cb,void * co){outfunctionClose cb;outObject co;}OrbitSimulate::~OrbitSimulate(){delete ui;if(outfunctionClose ! nullptr)//outfunctionClose只是回调函数的地址具体是在外部实现的outfunctionClose(outObject,0,OrbitSimulate);//}主程序相关实现代码如下。下面代码是动态库的解析和回调函数注册函数解析的几个必要条件一是知道动态库的路径用QLibrary 加载动态库二是知道需要解析的函数的名称和类型公用头文件中对动态库导出的函数类型进行了定义此处直接使用函数名称则必须要提前知道三是回调函数dealLibClose的形式必须与NotifyCallBackClose保持一致具体实现在主程序中。QLibrary * loadlib new QLibrary;loadlib-setFileName(“动态库路径”);OpendllFunction openlib (OpendllFunction)loadlib-resolve(“openOrbitSimulateWin”);QWidget * newWidget openlib(this,args);RegisterClosefunction registerCloseFun (RegisterClosefunction)loadlib-resolve(“registerCallBackClose”);registerCloseFun(dealLibClose,this);//注册函数供动态库调用可能存在的几个问题一、回调函数必须是静态的。在上述实现中被传递的回调函数必须是静态的可以是全局静态函数也可以是类成员静态函数。二、上述传递主程序对象指针的目的。我们知道静态函数不能访问主程序类中的普通成员。当需要在回调函数中执行一些复杂操作时将不可避免需要操作主程序对象。传递主程序对象指针的目的就是为了能够在回调函数中实现对主程序成员对象的访问。在回调函数中只需要将传递的无类型指针强制转换为主程序类的类型即可通过该指针访问成员对象。三、可能存在内存泄漏问题。在上述代码中如果主程序对象已经释放动态库再调用回调函数时这时传递的指针是非空的因为动态库传回的指针值是存放在动态库内部的全局变量中的动态库未释放时任然有值。回调函数将会得到一个非空指针而实际上这个指针指向的对象已经释放继续操作将导致非法访问程序直接崩溃。这就是上述代码存在的风险指针使程序更加灵活但也带来了风险。在这种情况下回调函数中是无法通过该指针来判断指针是否可用的。那么该如何来避免这种风险呢我们知道静态成员是在程序的main函数退出后最后释放的因此我们可以使用一个静态变量来标记一个对象是否被释放。在对象的析构函数中对该静态变量进行赋值在静态函数中通过该变量值来判断对象是否被释放从而避免非法访问风险。