动态链接库(DLL)导出:需要注意的问题

动态链接库(DLL)导出:需要注意的问题

一、简介

从DLL中导出函数有很多种方式,然而当你自己做DLL导出时会遇到很多问题。如果你用一种语言编写DLL而用于其他语言时,就有很多需要注意的地方。我们将分析其中的一些。

解决问题的关键是对程序究竟发生了什么了如指掌。我将以C++创建的DLL开始,然后引出很常见的一些问题。最后我们会对DLL做出改变以修复这些问题。

二、背景

早晚有一天,你会需要使用DLL导出的函数。或者你会使用一个第三方库,再或者,给第三方提供你的公有API接口。

让我们想想当你VB很流行的时候吧。VB是一个能快速编写一定框架程序的语言。两三天你就能学会如何与GUI代码交互。
VB执行太慢,而且缺少像无符号整形和位转换这样基本的功能,我认为如果不能使用其他语言编写的DLL,VB绝不会有那么成功。

当执行速度是很重要的考虑因素时,人们便用c/c++或其他语言编写DLL供调用。相比于用GDI或MFC,VB创建GUI界面更简单。你可以用各自的语言试试。

三、创建一个简单DLL

本文主要是关于管理DLL导出函数名和顺序的,因此,简化了DLL创建过程。
创建Win32控制台应用程序:


选择“DLL”:



3、1DLLMain

不只是应用程序才有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,因为它和其他编译器、语言的互操作性很差。

四、提高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		@1505	NONAME



此时,如果你要访问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);
这样写看起来很别扭,但是谁叫你使用下标,不给它个名字呢?

五、让.NET和其他语言使用此库

。。。看原文后面的翻译。


翻译原文: 传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值