—— 关于 DLL 在 VB 中调用的一些细节
(VS 2005 编译器)
黄 彪
2012-04-06
由于DLL通常是为 C 程序准备的,因此一个C/C++程序员在为C/C++程序编写DLL时可以不必关心导出函数的调用约定。在你不关心这个问题的时候,编译器通常会为导出函数指定默认的调用约定——那就是_cdecl调用约定. _cdecl 调用约定规定了参数的出栈工作由调用者来完成。如果查看编译器生成的汇编指令的话,对于一个有参数的函数调用,可以看到在 CALL 指令前,一定会有 PUSH 指令,而在CALL 之后一定会有 POP 指令。
·山穷水尽
一个完美的编译器可以使程序员们轻松很多,但再完美的编译器也是由不完美的人开发的,因此总是会有一些意料之外的事情发生。当你试着用 VC 为 VB 去开发一个DLL时, 这种事情就会发生。
首先,VB 使用 _stdcall 调用约定,因此 DLL 中的导出函数必须全部声明成 _stdcall,VB 才有可能正常调用,类似于下面这样:
extern "C" _declspec(dllexport) int _stdcall MyFunction (int aParam, int bParam){ return aParam + bParam; }
对于一个曾经无数次为 C/C++ 程序写过 DLL 的程序员来说,仅仅是添加了一个 _stdcall 修饰符而已,其余一切都是外甥打灯笼——照旧,很简单,不是吗?
然后我们分别建立一个 VC 和 VB 的工程去测试我们的 DLL 。
******************************************************************************************************
首先在 VC 中测试。
typedef int(*)(int,int) MYPROC; HMODULE hMod = LoadLibrary (_T("MyDll.dll")); MYPROC pFun = (MYPROC)GetProcAddress(hMod, "MyFunction"); int nRet = pFun ( 2, 3);
在第三行中 GetProcAddress 返回的总是 NULL, GetLastError 返回的错误码是 127. 看来这样在VC中是行不通的。
******************************************************************************************************
然后在 VB 中测试。
'在 form1 模块中添加声明。
Private Declare Function MyFunction Lib "MyDll.dll" (ByValaParam As Long,ByVal bParam As Long) As Long
'在事件中调用
Dim iRet As Long
iRet = MyFunction ( 2, 3)
结果会如我们所设计的那样 iRet = 5 吗?非也,程序在执行到 iRet = MyFunction( 2, 3) 这行时会产生异常,并且提示“找不到 MyFunction 的入口点 in MyDll.dll”。 看这样在VB中也是行不通的。
******************************************************************************************************
经过一翻努力,我们究竟做了些什么? 答案是:我们做了一个即不能在 VC 中使用,也不能在 VB 中使用的 DLL 怪胎。没有碰到过此类问题的读者会问:“有这么严重吗? 这些代码一点都不能用吗?”。“呃,在我看来,我想大概的确就是这么严重!”
·柳暗花明
我们可以使用 DEF 文件去规避上述的这些问题,对于那些对 DEF 文件不了解的程序员,我有一个好消息:DEF 文件的内容简单得连猴子都可以弄明白。因此,我认为使用 DEF 文件来定义 DLL 的导出函数是最经济有效的解决办法。当我们使用 DEF 文件导出我们的函数时,就没有必要在源文件中再加入那些导出修饰符了,但是源代码中的调用约定修饰符是不能省略的。
全部的做法分为三部:
一, 改变 MyFunction 函数声明,象下面这样:
int _stdcall MyFunction (intaParam, int bParam) {
return aParam + bParam;
}
二,编辑DEF 文件,象下面这样:
LIBRARY MyDll
EXPORTS
MyFunction @1
三,修改编译器开关,使得编译器使用 DEF 文件来定义导出函数。
经过以上三部修改,编译后的 DLL 在 VB 中是完全可以直接使用的。可是在VC中调用 LoadLibrary 加载使用的话,还是会有一个问题:因为 _stdcall 调用约定规定了参数是由函数自已进行出栈处理的。但调用者会认为它是一个使用 _cdecl 调用约定的函数。在函数返回后,调用者会对函数的参数执行出栈指令——这将会引发严重错误。
解决这个问题,我们可以自已使用汇编指令来调用 _stdcall 约定的函数,而不是由编译器去生成汇编指令。
类似下面这样:
int nRet = 0;
_asm{
PUSH 3
PUSH 2
CALL [pFun] // pFun 是通过 GetProcAddress 从 dll 中取得的导出函数地址。
MOV nRet, eax
}
nRet 便包含了 MyFunction 函数的返回值。