使用QT+MinGW编写动态库dll供VC或VB调用

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/flfihpv259/article/details/70145080

前言

在MSVC下编写一个Windows dll库供调用是非常简单的,VS会给你创建好一个模板工程。但使用MinGW创建一个window dll则有点小麻烦。下面就简单说下如何使用MinGW创建window dll的。

版本

QT5.8.0-mingw53_32版本
环境 win7 64

dll的编写和调用及查看工具

DLL的基本认识

library库文件分两种,一种是static link library,一种是dynamic link library(dll)。两者都是代码共享的方式。

链接库类型 区分 特点 生成文件
静态库 在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中 可执行文件中包含了库代码的一份完整拷贝;因此当被多次使用时就会有多份冗余拷贝。即静态库中的指令都全部被直接包含在最终生成的 EXE 文件中了 在vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件,qt+MinGW中只生成.a文件,
动态库 是一个包含可由多个程序同时使用的代码和数据的库,在程序运行时动态调用 dll提供了一种方法,使程序可以在执行时利用函数地址去dll中调用对应的函数 在vs中新建生成动态库的工程,编译成功后,产生一个.lib文件和一个.dll文件,qt+MinGW中只生成dll

那么上述静态库和动态库中的lib有什么区别呢?

静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中

动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供

总之,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。

引用参考自lib 和 dll 的区别、生成以及使用详解,这个比较清晰。

要强调一点的是,使用静态库编译的程序可以独立运行,使用导入库编译出来的程序必须要dll才能正常运行。

这里重点说dll,dll 是一种在Microsoft Windows 和 OS/2使用的共享库,包含能被任意应用程序重复使用的函数(当然不只有函数)。假如你有个库tst.dll包涵有一个doWork()函数,为了让应用程序tst.ext使用,你必须export它,而tst.ext使用它时又必须要import它(当然你也可以像插件那样在应用程序运行时通过某些函数加载它,然后从中调用它的方法,当不需要时在卸载)。

查看工具

用来查看dll相关信息(如使依赖的其它库文件,导出函数名等),VS里提供了depends.exe,Dumpbin.exe,Qt可使用自带的小工具windeployqt(这个是命令行工具,可以自行帮你拷贝依赖到当前文件夹下)

  • 注意,不同版本下的windeployqt会拷贝不同的库,比如你用msvc2015_32下的windeployqt去查验一个msvc2015_64位编译出来的库,那它会拷贝32位的库过来,即使你的依赖需要的是64位的库。

编辑

以下参考自Building Windows DLLs with MinGW

  • 关于.h头文件
    dll中的函数实现与普通函数一样,只不过在制作库时需要将被外部调用的函数在定义时用宏__declspec(dllexport)修饰以暴露出来让外部可见,导出类同样使用它修饰就行。而在使用时又要用__declspec(dllimport) 修饰以引入,当然这与使用的使用方法有关(显式调用就不需要),同时为了适应不同的语言最好再定义个宏来指定函数的调用协议,方便更改(比如下方的DLLCALL ),因此头文件为了方便复用还是要处理下如下,DLL_EXPORTS的制作库时定义下就行。:

    
    #ifdef _WIN32
    
      /* You should define ADD_EXPORTS *only* when building the DLL. */
      #ifdef DLL_EXPORTS
        #define DLLAPI __declspec(dllexport)
      #else
        #define DLLAPI __declspec(dllimport)
      #endif
    
      /* Define calling convention in one place, for convenience. */
      #define DLLCALL __cdecl  //vc c++ call
    // #define DLLCALL __stdcall //vb call
    
    
    #else /* _WIN32 not defined. */
    
      /* Define with no value on non-Windows OSes. */
      #define DLLAPI
      #define DLLCALL
    
    #endif
    
    
    
    #ifdef __cplusplus
    
    extern "C"{
    
    #endif
    
    
    DLLAPI int DLLCALL openDevice(const char *camera,const char *serialport);
    DLLAPI void DLLCALL saveImage(const char *file);
    
    #ifdef __cplusplus
    
    }
    class DLLAPI classname{
        ...
    }
    
    #endif
    
    

    同时因为c++ 和c函数名称修饰规则是不一致的。所以需要注意是否以c的形式编译,如果以c形式编译需要加修改符extern "C",当然如果是类,使用它修饰也不起作用。同时如果是C++,即使是同一编译器,也可能因为不能的版本而使导出名称有所不同,所以最好用c的形式。

  • 关于.cpp实现文件
    主要说下dllmain()函数,它是dll库的入口函数,在静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数

    BOOL WINAPI DllMain( HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpvReserved*/ )
    {
        if ( dwReason == DLL_PROCESS_ATTACH )
        {
            qDebug()<<"attach done.";
        }
        if ( dwReason == DLL_PROCESS_DETACH )
        {
            qDebug()<<"detach done.";
        }
        if ( dwReason == DLL_THREAD_ATTACH)
        {
            qDebug()<<"detach done.";
        }
        if ( dwReason == DLL_THREAD_DETACH )
        {
            qDebug()<<"detach done.";
        }
        return TRUE;
    }
    

调用

  • VC下调用
    dll的调用分为隐式链接和显式链接

    • 显式链接:在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这里编译运行时都只需要一个dll文件就行,但为了知道暴露出来的函数提供下头文件给coder还是有必要的。
      使用方法:1.用LoadLibrary加载库,2.GetProcAddress获取函数指针,3.调用函数,4.FreeLibrary释放库。qt里调用也有对应的函数,这个可以查看帮助文档。

          typedef int  (*FUNA)(int,int);
          HMODULE hMod = LoadLibrary("testdll.dll");//dll路径
          if (hMod)
          {
              FUNA addfun = (FUNA)GetProcAddress(hMod, TEXT("addfun"));//直接使用原工程函数名,可以使用工具查看库里的导出名是否一致。
              if (addfun != NULL)
                  cout<<addfun(5, 4)<<endl;
              else
                  cout<<"ERROR on GetProcAddress"<<endl;
              FreeLibrary(hMod);
          }
          else
              cout<<"ERROR on LoadLibrary"<<endl;
    • 隐式链接: 据说需要.h、.lib、.dll三件套,我没有lib生成所以放弃。如果是同一编译器和使用环境就直接用一个库文件和头文件就OK,在用时加入库,头文件,然后直接如其它函数,类一样使用就行(处理过后的头文件的复用功能体现出来了,只是用来申明导入的函数)

      extern "C" _declspec(dllimport) int addfun(int a,int b);//extern是否需要看你的库是否用了,反正要使用编译出来的名称一致
      
  • VB下调用

    VB调用时使用的是__stdcal的约定,所以在制作库时的调用协议要改成__stdcal,在使用时将库放到工程下,然后申明下就可以了,不过库在使用__stdcal后编译出来的库导出名字会变成openDevice@8的样子,网上说如果想一致必须要写个def文件。我直接用工具查询下名字然后像我下面这样申明了。

    Private Declare Function openDevice Lib "bstdll-vb.dll" Alias "openDevice@8" (ByVal camera As String, ByVal uart As String) As Long
    Private Declare Sub saveImage Lib "bstdll-vb.dll" Alias "saveImage@4" (ByVal file As String)
    
    Private Sub Form_Load()
            Dim ret As Long
            ret = openDevice("Camera", "COM1")
            call saveImage("c:/tmp.jpg")
    End Sub
    

    minGW解决的办法参考下面的说法

    It’s fairly straightforward to use MinGW DLLs with Visual Basic 6 and
    VBA. The only requirement is that the calling convention for exported
    functions is stdcall (cdecl and other calling conventions are not
    supported), and the functions are exported without decorations by
    using the “–kill-at” command line option:

    gcc -o AddLib.dll add.o -shared -s -Wl,--subsystem,windows,--kill-at

    在qt的.pro中加入下面语句重新构建后在查看OK了,没有过度修饰,这样申明就简单了。

    QMAKE_LFLAGS += -Wl,--subsystem,windows,--kill-at

QT下dll动态库的制作

  1. 新建library c++库工程,在pro文件中加入,以排除拷贝一些动态库的麻烦。

    QMAKE_LFLAGS = -static-libgcc -static-libstdc++ 
    QMAKE_LFLAGS += -Wl,--subsystem,windows,--kill-at //如果是导出给VB调用还要加上这句,否则不加,JAVA似乎也要。看调用协议了。
  2. 如果用到了信号槽等qt特有的事件处理机制(比如你用到了QWidget,QCamera等),那你必须在库里跑一个qApp.exec以让qt库可以正常处理QT事件,当然为了不卡死在库里必须要做下处理,这个可以参考qtwinmigrate的例程,里面有示例,看qtdll就行,我理解的实质也就是在dllmain入口函数中,当加载库时实例化一个qApp,并让事件驱动起来并过滤掉window的事件,有qt的事件就处理之。卸载时删除qApp。
  3. 我在windows下用mingw编译的库是没有lib文件的,只有用mvsc编译才有这玩意。mingw下动态编译就只有dll,静态的话dll都没有,只出来个.a。至于说只有lib没dll这种情况没遇上过,据说加个CONFIG += dll就行。
  4. qt做的库,我尝试了只能以extern "C"形式编译才能让VC访问,所以类对象是没法导出了,只能一个一个方法导出来。就算我在内部创建实例化一个对象返回出来也出现各种问题,不过这里有篇参考文章不妨一试Exporting C++ Functions & Variables
    里面还有导出供JAVA,.NET调用的方法。
  5. 使用QT静态库编译出来的DLL依然需要别得依懒库,所以没什么必要非要用静态库来编译了。

错误记录

VB调用时的错误记录

  • 在VS2005里直接新建的VB程序遇下列问题:

    1. System.BadImageFormatException: 试图加载格式不正确的程序。

      VB pro in vs build error

      因为我的编译器是用的minw32的,根据网友所述VS默认生成的目标平台是x64,因此在项目属性中更改,如下所示。
      这里写图片描述
      因为dll中的调试信息不打印出来,顺便勾选了下debug选项如下这里写图片描述
      再调试运行就OK了,还可以看到我的dll里输出的调试信息

  • 使用VB新建工程调用dll

    1. 实时错误‘49’:DLL调用约定错误
    2. 实时错误’453’:找不到DLL入口点

      引用网友的表述:

      初次接触DLL的用户经常会遇到一个问题:在VC环境下创建的DLL,在VC里运行的好好的,可在VB应用程序中调用时却老是出现”调用约定错误”、”找不到入口点”之类的错误。这主要是由以下疏漏造成的。
        首先,要注意DLL中的函数和VB中的函数声明在名称、返回类型、参数类型、参数个数等方面必须完全相同,尤其要注意大小写的问题。
        其次,在DLL的.def文件中必须加上入口函数。
        最后,在函数定义前必须加上extern “c”, __stdcall关键字。

      • 第一个问题,更改成__stdcall就行。vb和vc函数调用协议不一样,几种协议的区分看下表,函数实现与定义如果使用了不同的函数调用协议,则无法实现函数调用,因此出错。
      • 第二个错误,按网上的说法就是要写def编译了,我比较懒了,也就几个导出函数,直接用Depends.Exe查出dll库里的导出函数名,然后在申明时依着写就OK了。查看mingw里有说明加个–kill-at编译项就OK了,亲测可行。
      • 以下整理自参考文章,感谢之.
      调用协议 常用场合 函数参数入栈方式 栈内数据清除方式
      __stdcall Windows API默认的函数调用协议 由右向左入栈 调用结束后由被调用函数清除
      __cdecl C/C++默认的函数调用协议 由右向左入栈 调用结束后由函数调用者清除
      __fastcall 对性能要求较高的场合 从左开始不大于4字节的参数放入CPU的ECX和EDX寄存器,其余参数从右向左入栈 调用结束后由被调用函数清除
      调用协议 c函数名称修饰规则 c++函数名称修饰规则
      __stdcall _funcname@number ?funcname@@YG******@Z
      __cdecl _funcname ?funcname@@YA******@Z
      __fastcall @funcname@nuber ?funcname@@YI******@Z
      备注 “funcname”为函数名,“number”为参数字节数 “******”为函数返回值类型和参数类型表

      问题一:__fastcall在寄存器中放入不大于4字节的参数,故性能较高,适用于需要高性能的场合。
      问题一:不同编译器设定的栈结构不尽相同,跨开发平台时由函数调用者清除栈内数据不可行。
      问题二:某些函数的参数是可变的,如printf函数,这样的函数只能由函数调用者清除栈内数据。
      问题三:由调用者清除栈内数据时,每次调用都包含清除栈内数据的代码,故可执行文件较大。

    3. 实时错误53: 文件未找到 xxx.dll
      这里写图片描述

      • VB本身的bug吧,先打开VB,后打开工程时有可能出这个问题,有时候这种情况使用绝对路径就好了,可生成exe执行时出问题,建议直接去工程中打开这样就不会出错。反正我有时这么干就OK,无语。
      • dll的依懒库缺少,导致误报成该dll找不到。用depends.exe查看下是否缺少相应库。注意qt的库有两种,一种是包含调试信息的库(通常以d结尾如xxxD.dll),一种是没有调试信息的库(这个比前者小很多)。
      • 在自己机子上调用是OK的,拿到别人机子上在VB中调试时出这问题,后来改成绝对路径就正常了,但生成exe执行就出错如下图,
        这里写图片描述
        然后我把dll做成release版发过去,exe能运行,这就奇怪了,我根本就没发release的库给他(当时忘了),但居然能用,当然在VB上调试就出问题,真是活见鬼。后来自己装vs2015中途终止后自己电脑上运行也出同样的问题。据了解那台机子也是这样的情况,怀疑是装vs时错误导致旧的VB某些兼容性引起的。重装一次VB,再运行时发现也一样,用depends检查也没用,有问号的是WINDOW下的东西,QT库都全了(我正常调用时也是这样),但我把相应的QT依懒库重新打包再覆盖工程下的库就OK了,depends显示是一样的。不要问为什么,就这样了,只能说发神经。

参考

参考1:Qt查找依赖库的简单方法
参考2:分享如何在VB中调用VC编写的DLL
参考3:C语言学习心得一:__stdcall、__cdcel和__fastcall三者的区别

展开阅读全文

没有更多推荐了,返回首页