本文将有以下4个部分来讲如何使用g++编译调用dll的c++代码。
1.如何调用dll
2.动态链接和静态链接的区别
3.g++的编译参数以及如何编译调用dll的c++代码
4.总结
1.如何调用dll
动态链接库(Dynamic Link Library),简称DLL。DLL 是一个包含可由多个程序同时使用的代码和数据的库。它允许程序共享执行特殊任务所必需的代码和其他资源,一般来说,DLL是一种磁盘文件,以.dll、.DRV、.FON、.SYS和许多以.EXE为扩展名的系统文件都可以是DLL。它由全局数据、服务函数和资源组成,在运行时被系统加载到调用进程的虚拟空间中,成为调用进程的一部分。
DLL的调用可以分为两种:一种是隐式调用(需要.lib和.dll),一种是显示调用(需要.dll)。
1.1 隐式调用
隐式加载就是在程序编译的时候就将dll编译到可执行文件中。实现隐式链接只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面将通过一个例子来讲解隐式链接调用Dlltest.dll库中的Min函数。
首先新建一个项目为TestDll,在Dlltest.h、Dlltest.cpp文件中分别输入如下代码:
Dlltest.h的代码如下:
//隐式加载Dlltest #pragma comment(lib,"Dlltest.lib") //声明外部函数 extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b);
Dlltest.cpp的代码如下:
#include<stdio.h> #include"Dlltest.h" int main() { int a; a=Min(1,2); printf("Min(1,2)result is %d ",a); return 0; }
在生成Dlltest.exe文件之前,要先将Dlltest.dll和Dlltest.lib拷贝到debug同目录(工程根目录)下,也可以拷贝到windows的System目录下。如果DLL使用的是def文件,要删除Dlltest.h文件中关键字extern "C"。Dlltest.h文件中的关键字pragma commit是要Visual C++的编译器在link时,链接到Dlltest.lib文件。当然,开发人员也可以不使用#pragma comment(lib,"Dlltest.lib")语句,而直接在工程的Setting->Link页的Object/Moduls栏填入Dlltest.lib既可。
1.2 显式调用
显式加载是指在程序运行过程中,需要用到dll里的函数时,再动态加载dll到内存中,这种加载方式因为是在程序运行后再加载的,dll的维护更容易,使得程序如果需要更新,很多时候直接更新dll,而不用重新安装程序。使用这种方式使应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件。
下面讲一个通过显式链接调用DLL中的Max函数的例子。
#include <stdio.h> #include <windows.h> int main() { typedef int(*pMax)(int a,int b); typedef int(*pMin)(int a,int b); HINSTANCE hDLL; int A; PMax Max; //动态加载Dlltest.dll文件 HDLL=LoadLibrary("Dlltest.dll"); Max=(pMax)GetProcAddress(hDLL,"Max"); A=Max(1,2); Printf("result is %d ",A); //卸载Dlltest.dll文件; FreeLibrary(hDLL); }
在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用FreeLibrary()卸载DLL文件。
学习参考链接
DLLs in Visual C++:
https://docs.microsoft.com/en-us/previous-versions/1ez7dh12%28v%3dvs.140%29
Walkthrough: Creating and Using a Dynamic Link Library (C++):
https://docs.microsoft.com/en-us/previous-versions/ms235636%28v%3dvs.140%29
Using Run-Time Dynamic Linking:
https://docs.microsoft.com/zh-cn/windows/desktop/Dlls/using-run-time-dynamic-linking
2.动态链接和静态链接的区别
2.1 静态链接和动态链接
静态链接方法:静态链接库格式如:#pragma comment(lib, "Dlltest.lib") ,静态链接时,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来,将静态库中的函数和数据与应用程序的其他模块一起生成可执行程序。
动态链接方法:动态链接方法使用LoadLibrary()、GetProcessAddress()和FreeLibrary()等函数来链接dll,动态链接方式并不能一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序又需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以,这种方式使程序初始化时间较短,但运行期间的性能比不上静态链接的程序,但是生成的可执行程序比较小,不占内存。
2.2 静态链接和动态链接的优缺点
静态链接的优点 :
a. 代码装载速度快,执行速度比动态链接库略快;
b. 只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。
动态链接的优点:
a. 比较节省内存并能减少页面交换;
b. DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
c. 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
d. 适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
两者的不足之处:
(1) 使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
(2) 使用动态链接的应用程序要确保它依赖的DLL模块存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统不能正常运行。
2.3 静态库与动态库链接、执行时的搜索路径顺序
以下【】中加粗部分引用自https://blog.csdn.net/sunshixingh/article/details/52185307
【静态库链接时搜索路径顺序:
1. ld会去找g++命令中的参数-L
2. 再找g++的环境变量LIBRARY_PATH
3. 再找内定目录 /lib /usr/lib /usr/local/lib 这是当初compile g++时写在程序内的
动态链接时、执行时搜索路径顺序:
1. 编译目标代码时指定的动态库搜索路径
2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
4. 默认的动态库搜索路径/lib
5. 默认的动态库搜索路径/usr/lib】
有关环境变量:
LIBRARY_PATH环境变量:程序静态链接库文件搜索路径
LD_LIBRARY_PATH环境变量:程序动态链接库文件搜索路径
3.如何使用g++编译调用dll的c++程序
3.1 g++编译参数
G++手册:https://linux.die.net/man/1/g++
编译分为3步,首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;然后进行编译,这个过程主要是进行词法分析、语法分析和语义分析等,生成*.s的汇编文件;最后进行汇编,将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。
使用g++编译链接示例:
#预编译,生成main.i文件 g++ -E main.c -o main.i #编译,生成main.S文件 g++ -S main.i #汇编,生成main.o文件 g++ -c main.S #链接,生成可执行文件 g++ main.o -o main
g++ 命令的基本用法如下:
g++ [options] [filenames]
G++主要接受与GCC相同的选项。如果只想要编译的一些阶段,可以使用-x(或文件名后缀)告诉g++从哪里开始,可以使用选项-c、-S或-E告诉g++从哪里停止。注意,一些组合(例如,-x cpp-output-E)指示g++什么也不做。
下面【】中加粗部分的参数解释是通过翻译g++手册:https://linux.die.net/man/1/g++得到:
【1)-E参数
-E 选项指示编译器仅对输入文件进行预处理。当使用这个选项时,预处理的输出会被送到标准输出而不存储在文件中.-E会使g++在预处理阶段停止,不会运行编译器,输出的预处理源代码的形式,它会被发送到标准输出,预处理阶段g++会忽略掉不需要预处理的输入文件。
2)-S参数
-S 选项会使g++停止在编译后的阶段。默认情况下,源文件的汇编程序文件名是通过将后缀.c、.i等替换为.s来创建的,对于指定的每个非汇编输入文件,输出的汇编代码文件的格式。g++在使用-S选项时会忽略不需要编译的输入文件。
3)-c参数
-c 选项用来编译或汇编源文件,但并不会链接,输出的格式为每个源文件的对象文件的形式(.o)。默认情况下,源文件的目标文件名是通过将.c、.i、.s等后缀替换为.o来创建的。g++在使用-c选项会自动忽略未识别的输入文件,认为不需要编译或汇编。
4)-o参数
-o 选项用来为产生的可执行文件用指定的文件名,将输出置于文件文件中。如果没有指定-o,则缺省值生成可执行文件(.exe)。
5) -l参数(小写L)和-L参数
-l参数用来指定程序链接的库,-l参数紧接着就是库名,例如库名是test,他的库文件名是libtest.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了。
-L参数跟着的是库文件所在的目录名。举个栗子:我们把libtest.so放在" /ddd/study "目录下,那链接参数就是-L/ddd/study -ltest。
6)-I参数(大写i)
-I参数是用来指定头文件目录,-I参数可以使用相对路径。】
到这里就已经讲完我在编译c++程序常用的g++参数了。
3.2 生成一个dll
我们使用g++编译参数编译调用dll的c++程序,除了要了解常用的g++参数,如何调用dll,还需要学会如何生成一个dll,毕竟要有dll,才能去调用。接下来我简要介绍一下如何在Dev c++下生成一个dll。
步骤如下:
(进行如下步骤,首先要确保电脑已经安装了Dev c++)
打开Dev c++,点击 “file—》new—》project”
选择dll,我使用c语言,我将dll项目命名为test_dll。截图如下:
点击ok后Dev c++会自动生成一个dllmain.c和dll.h文件。里面有一个HelloWorld的示例。截图如下:
在dll.h文件添加如下代码:
DLLIMPORT int Add(int a,int b); DLLIMPORT int Multi(int a,int b);
dll.h的部分截图如下所示:
在dllmain.c添加如下代码:
DLLIMPORT int Add(int a,int b){ return a+b; } DLLIMPORT int Multi(int a,int b){ return a*b; }
dllmian.c部分截图如下所示:
然后编译生成对应的dll(注意生成是64位还是32位的,怎么注意生成的版本是多少位的,请注意下图红框部分。)
我生成了两个版本的dll文件,test_dll_32.dll为32位版本的,test_dll_64.dll为64位版本的,截图如下。一般情况下,编译生成的.dll文件在你新建项目的目录下,找dll文件到对应目录找就行。
3.3使用g++编译调用dll的c++程序
接下来简单写一个调用dll的程序。
新建一个文件名为demo.cpp,我调用的dll名称为test_dll_64.dll(64位),使用的是动态链接方式。调用dll的c++代码如下:
#include <Windows.h> #include<iostream> #include<stdlib.h> using namespace std; int main(){ typedef int(*FUNT)(int,int); HINSTANCE hDllInst = LoadLibrary("test_dll_64.dll"); cout<<"hDllInst:"<<hDllInst<<endl; int a=12,b=3; FUNT add=(FUNT) GetProcAddress(hDllInst, "Add"); printf("add(12,3) result is: %d ",add(12,3)); return 0; }
使用g++编译,按住win+R,打开cmd(你要确保已经配置g++的环境变量),输入如下指令:
#预编译,生成demo.i文件 g++ -E demo.cpp -o demo.i #编译,生成demo.S文件 g++ -S demo.i #汇编,生成demo.o文件 g++ -c demo.S #链接,生成demo.exe文件 g++ demo.o -o demo.exe #运行demo.exe demo.exe
在cmd下运行显示hDllInst:0 ,之后显示demo.exe已停止工作,运行截图如下所示:
之所以会出现demo.exe已停止工作,是由于loadlibrary返回值为0带来的结果。
我百思不得其解,为什么loadlibrary的返回值为0呢?我仔细检查代码明明没有错误,g++编译指令也没有错误。
后来我将代码改为调用test_dll_32.dll(32位的)。
HINSTANCE hDllInst = LoadLibrary("test_dll_32.dll"); cout<<"hDllInst:"<<hDllInst<<endl;
demo.cpp的 完整代码截图如下:
重新使用g++编译。按住win+r,输入cmd,输入如下命令:
#编译,生成demo.o文件 g++ -c demo.cpp #链接,生成demo.exe文件 g++ demo.o -o demo.exe #运行demo.exe demo.exe
Cmd下运行demo.exe输出为
hDllInst:0x6c040000(调用dll成功。)
add(12,3) result is: 15。
符合预期,能够成功调用dll。
cmd运行截图如下:
3.4 题外话
来一段小插曲(可以跳过)。
介绍一下visual studio和Dev c++的编译器。虽然我们经常用VS和Dev c++来编译C++程序,但是可能有些人并不知道它们的编译器是什么,就比如我。接下来简要介绍一下VS和Dev c++的编译器。
VS的编译器名称为MSVC。MSVC官方文档为:
msvc的编译器cl.exe
msvc的链接器link.exe
Dev-C++的编译器名称为Mingw(包含了g++和gcc)
4.总结
我在g++下编译了32位版本和64位版本的可执行程序,当编译的可执行程序为64位版本的,而调用的dll为64位版本时,运行不会出错。当编译的可执行程序为32位版本的,而调用的dll为64位时,运行可执行程序会出现loadlibrary的返回值为0,可执行程序停止工作。这样证明了当我们编译的可执行程序和应用调用的dll位数不匹配时,编译时没有出现错误,但会出现运行时错误。
因为我在某项目中遇到了类似的问题,编译的可执行代码版本与dll位数不匹配引起的loadlibrary返回值为0以及可执行程序已停止工作的现象,当时我却没有发现出现错误的原因,阻碍了我的项目进展。所以写此博客记录一下,希望能帮到和我有类似情况的人。
最后,在此感谢导师、彩虹、邦哥和小霞的帮助。