创建与使用DLL项目常见错误和解决办法

来源:互联网 发布:访客网络的区别 编辑:程序博客网 时间:2024/06/14 22:18

前面讲原理有点啰嗦,如果直接看创建和使用DLL,直接跳转到【DLL项目创建】。

DLL

在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可使用多个DLL文件,一个DLL文件也可能被不同的应用程序使用,这样的DLL文件被称为共享DLL文件。(百度百科给的解释)

DLL中存放的内容

DLL中可以存放任何正常可以定义的代码类型,其实DLL中可以使用模板类和模板函数,但是在接口上需要指定模板类和模板函数的类型,而对于接口而言无法做到真正的模板(对于很大的很庞杂的DLL而言,使用模板还是很方便的)

为什么使用DLL

在编程语言中,问目的的事情基本上就是代码复用。DLL文件也是一样,但是DLL相比较而言会有隐藏实现过程,代码不开源等特点。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用。

比较常见的例子是各种应用程序框架,如ATL、MFC等,它们都以源代码的形式发布。由于这种复用是“源码级别”的,源代码完全暴露给了程序员,因而称之为“白盒复用”。“白盒复用”的缺点比较多,总结起来有4点。

  • 暴露了源代码;
  • 容易与程序员的“普通”代码发生命名冲突;
  • 多份拷贝,造成存储浪费;
  • 更新功能模块比较困难。

实际上,以上4点概括起来就是“暴露的源代码”造成“代码严重耦合”。为了弥补这些不足,就提出了“二进制级别”的代码复用。使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为“黑盒复用”。

在Windows操作系统中有两种可执行文件,其后缀名分别为.exe和.dll。它们的区别在于,.exe文件可被独立的装载于内存中运行;.dll文件却不能,它只能被其它进程调用。然而无论什么格式,它们都是二进制文件。上面说到的“二进制级别”的代码复用,可以使用.dll来实现。

与白盒复用相比,.dll很大程度上弥补了上述4大缺陷。.dll是二进制文件,因此隐藏了源代码;如果采用“显式调用”(后边将会提到),一般不会发生命名冲突;由于.dll是动态链接到应用程序中去的,它并不会在链接生成程序时被原原本本拷贝进去;.dll文件相对独立的存在,因此更新功能模块是可行的。

说明:实现“黑盒复用”的途径不只dll一种,静态链接库甚至更高级的COM组件都是。本文只对dll进行讨论。

DLL项目创建

在VS2015中,File–New–Project,然后创建一个空的项目dynimicLinkLib,解决方案名也为dynamicLinkLib,然后新建一个头文件MydyLinkLib.h,一个MydyLinkLib.cpp文件,在头文件中添加如下信息:

#pragma once#ifndef _MyDYNAMICLINKLIB#define _MyDYNAMICLINKLIB _declspec(dllexport)#else#define _MyDYNAMICLINKLIB _declspec(dllimport)#endif_MyDYNAMICLINKLIB int add2(int a, int b);

dll_head_file
MydyLinkLib.cpp中添加:

#include "MydyLinkLib.h"#include <iostream>using namespace std;_MyDYNAMICLINKLIB int add2(int a, int b){    cout << (a + b) << endl;    return a + b;}

dll_cpp_file

将项目进行Build,就可以得到dll文件,文件中含有一个add2这样一个函数可以在外部调用。
dll所在文件目录一般是项目的输出目录:
dll_path

注:这里进行的是手动添加DLL声明,在VS中可以直接利用VS的特性,自动生成DLL项目,但是添加的样式是一样的,很简单就不讲了,如果不知道可以搜索查询一下如何使用VS进行自动生成DLL项目。

DLL使用

和上边的项目一样,创建一个空项目,名称为test4dll,也是建在当前的解决方案下面:
test4dllsolution
这样可以避免进行刚刚创建的.dll文件移动或在使用项目中添加目录。
在test4dll.cpp中添加如下代码:

#include "stdafx.h"#include <wtypes.h>#include <Windows.h>typedef int(*funp)(int, int);int main(){    const wchar_t * dllname = L"dynamicLinkLib.dll";    HMODULE dlllib = ::LoadLibrary(dllname);    if (!dlllib)    {        ::FreeLibrary(dlllib);        dlllib = 0;        return -1;    }    LPCSTR methodname;    methodname = "?add2@@YAHHH@Z";    funp mymethod = (int(*)(int, int))::GetProcAddress(dlllib, methodname);    if (mymethod == NULL)        printf("Can not Find method: %s", methodname);    else    {        int a = (*mymethod)(2, 4);        printf("Return value: %d", a);    }    return 0;}

图片:
dllusesourcemethodname

然后就可以进行运行,或得到dll文件的句柄,和方法句柄。但是会在获得方法的时候出现问题。因为无法从函数GetProcAddress获得相应的dll中函数地址。

直接导入DLL办法1:dumpbin或Dependency Walker检查

无法后的dll中函数地址,是因为在build形成dll文件中,方法名会被编写成乱码一样的形式,所以在获得方法地址的时候需要使用这个乱码,哈哈,这是第一步的解决办法,先让我们嫌弃的心将就一下吧。

那么怎么知道乱码的方法名呢?使用一个VS自带的工具,在C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin目录下,有一个dumpbin.exe的文件,这个文件可以查找出dll,exe等文件内部的方法,类,变量等。
将命令行工作目录转换到dumpbin目录下,运行命令:

dumpbin /exports dynamicLinkLib.dll 

注意:如果dynamicLinkLib.dll文件没有在dumpbin同级目录下,那么需要在命令中添加dll文件所在的路径,但是路径中不能有空格出现否则就无法解析dll文件。

dumpbin /exports D:/Projects/dynamicLinkLib/x64/debug/dynamicLinkLib.dll

* 具体如何使用dumpbin请参考另一篇文档如何使用dumpbin
而Dependency Walker是一个免费的实用工具,扫描所有32位或64位的Windows模块(如exe,dll,ocx,sys等文件)并能显示所有其依赖的模块树状图。具体的不讲了,想查看的可以查看Dependency Walker文档。

然后得到在dll中的函数名是: “?add2@@YAHHH@Z”
在使用项目中使用这个函数名:

dlluse

得到正确结果:
result

直接导入DLL办法2:导出函数时加上限定符:extern “C”

在定义的导出函数上加上限定符:extern “C”,实际上使用限定符 extern “C” 是告诉编译器,让它按C的方式编译。

如:

#pragma once#ifndef _MyDYNAMICLINKLIB#define _MyDYNAMICLINKLIB extern "C" _declspec(dllexport)#else#define _MyDYNAMICLINKLIB extern "C" _declspec(dllimport)#endif_MyDYNAMICLINKLIB int add2(int a, int b);

这样就可以在使用的文件中直接获得导出的函数名:add2,而不会发生乱码。
externC

后记

** 但是,extern “C”只解决了C和C++语方之间调用的问题,它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。
同时如果导出函数的调用约定发生改变,即使使用extern “C”,编译后的函数名还是会发生改变。

C++编译函数名修饰约定规则

前面看到的乱码现象C++编译时函数名修饰约定规则:
__stdcall调用约定:
1、以”?”标识函数名的开始,后跟函数名;

2、函数名后面以”@@YG”标识参数表的开始,后跟参数表;

3、参数表以代号表示:
X–void
D–char
E–unsigned char
F–short
H–int
I–unsigned int
J–long
K–unsigned long
M–float
N–double
_N–bool
….
PA–表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以”0”代替,一个”0”代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;

5、参数表后以”@Z”标识整个名字的结束,如果该函数无参数,则以”Z”标识结束。
其格式为”?functionname@@YG*****@Z”或”?functionname@@YG*XZ”,例如
int Test1(char *var1, unsigned long)—–”?Test1@@YGHPADK@Z” void Test2()—–”?Test2@@YGXXZ”

__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的”@@YG”变为”@@YA”。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的”@@YG”变为”@@YI”。
如果要用DEF文件输出一个”C++”类,则把要输出的数据和成员的修饰名都写入.def模块定义文件
所以… 通过def文件来导出C++类是很麻烦的,并且这个修饰名是不可避免的其实是C++编译时的一种编译规则形成的字符串,而非无序的乱码。