不只是应用程序才有Main函数,DLL也有它的DLLMain。在DLLMain中你可以分配、初始化相应的数据。一下代码是自动生成的,并没有修改过:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "stdafx.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
3、2__declspec(export)
导出一个函数所需做的工作量很少。只需要在函数定义最前面加上__declspec(dllexport)。我在此使用__stdcall属性来改变调用约定。(默认的调用约定是__cdecl)
__declspec(dllexport) int __stdcall Add(int a, int b)
{
return a+ b;
}
当你编译完后,就会生成DLL和.lib文件。如果你想运行时让DLL自动加载到你的应用程序中,当你链接你的程序时需要使用.lib文件。
在应用程序中,你需要添加.lib文件的引用,可以通过工程设置或者程序中添加pragma命令。我个人喜好pragma命令,因为工程设置和配置相关,你必须记得改动所有的更新配置。
#include "../DLLExports/DLLExports.h"
#ifdef _DEBUG
#pragma comment(lib, "../Debug/DLLExports.lib")
#else
#pragma comment(lib, "../Release/DLLExports.lib")
#endif
int main()
{
int res = Add(88, 23);
}
以上代码是通过DLL导入导出函数最简单的形式,这并不是一个好的DLL,因为它和其他编译器、语言的互操作性很差。
如果你想在C、Pascal、VB、C#或者其他语言中使用这个DLL,这会让你失望,因为目前为止这个DLL只适用于C++。
我们用dumpbin.exe(注:原文为bindump.exe,在vs的工具目录下)来看看为什么其他语言不识别此DLL。
dumpbin.exe /EXPORTS D:\programExample\others\DLLExport.dll
红色框起来的名字就是我们的Add函数,类型信息被C++连接器编码。但是只要调用这个DLL的应用程序也是C++编译和链接的话这并没关系。然而C语言便并没有像C++这样管理命名。
加上 extern "C"来让让C和C++调用DLL能互操作
extern "C"
{
__declspec(dllexport) int __stdcall Add(int a, int b);
}
然后重新生成一次,这时候使用bindemp.exe查看DLL文件:
现在的名字变成了_Add@8。为什么是_Add@8而不是Add?现在是C编译器处理的结果了。
C编译器处理后开头使用一个下划线,接着是函数名,最终以参数占用的字节数多少来结束。我们传递了2个整形数据(共8字节),因此最后是8。你现在可以在C/C++环境中使用这个库。这已经进步了一些,但还是不够强大。
调用约定十分严格,但是修饰后的名字却不严整。有很多语言根本不修饰名字。通过使用dumpbin.exe查看DLL中被修饰了的名字,我们也能通过修饰了的函数名字来调用它,但是当你用其他编译器、更改参数等等,被修饰的名字就会改变。
从图中可以看出_Add@8重复了1次。 _Add@8 = @ILT+155(_Add@8) 左边是导出函数名,右边是内部使用的名字。在这种情况下它们是相同的。在下一节中,我们将学会如何改变导出函数名。
使用.def文件
.def文件能让你对导出的函数有更多的控制权。不知道为什么很多写DLL技术的人并没有提到这个文件的使用。
添加一个以.def结尾的文件到工程,名字无所谓。我通常使用exports.def
export文件应该是下面的格式
LIBRARY DLLExports
EXPORTS
Add@1
然后转到工程的属性页,在“Linker-》input”下添加此文件名
添加了这个文件之后编译生成,用dumpbin.exe打开DLL
可以看出,添加了.def文件后输出函数名就只有Add,没有其他的修饰了。
.def做了什么?
从DLL中导出的函数被从新分配了一个标号和一个可选的名字。对于导出给外部使用函数来说,添加一个函数名是有必要的。而对于只是DLL内部、而不公开的函数来说,给一个标号就行了。我们现在来看看USER32.dll的导出函数。
由上可以看出,总共导出函数为1062个,但是只有822个是有名字的。意味着你只能按顺序访问它们。通常的顺序是从1开始的,但也可能是从其他数开始,比如上面的1502.
现在我们自己再来创建一个DLL。这个DLL包括两个函数:公开的Foo函数,有名字和下标;另一个私有函数Bar,只能通过下标访问。为了能让你更明白,我们将下标从1502开始,而且在下标数上故意让Bar从1505开始。
LIBRARY DLLExports
EXPORTS
Foo@1502
Bar@1505NONAME
此时,如果你要访问Bar函数,就需要动态加载,使用下标1505访问:
HMODULE hLoadedLibrary = LoadLibrary("DLLExports.dll");
// Notice __stdcall on function pointer typedef
typedef int (__stdcall* BarFunc)(int, int);
BarFunc Bar = (BarFunc) GetProcAddress(hLoadedLibrary, (LPCSTR)1505);
int result = Bar(6,3);
printf("#1505(6,3) = %i\n", result);
FreeLibrary(hLoadedLibrary);
这样写看起来很别扭,但是谁叫你使用下标,不给它个名字呢?