在上周完成的一项工作中,发现了一个当时有点困惑的问题:
当编写供VB调用的C++ DLL时,必须使用def文件来定义导出符号,否则VB程序将链接失败。我们都知道使用def文件是为了避免C++编译器对函数进行重命名,另一个达到相同效果的替代方式是使用extern "C"标识符。若DLL由C++、C#、Java等调用,后者可以工作得很好;然而VB不可以,即便使用了extern "C"来标识导出函数,依然需要定义def文件。
就在刚刚,我找到了答案,《Windows核心编程》——19.3.2 创建用于非Visual C++工具的DLL
根据其中说明,可知:当使用__stdcall调用方式时,就算使用了extern "C",编译器也依然会更改函数名。因此会导致链接失败。
文中也提到了不依赖def文件解决此问题的一种方法。
原文如下:
如果使用Microsoft Visual C++来创建D L L和将要链接到该D L L的可执行模块,可以跳过本节内容的学习。但是,如果使用Visual C++创建D L L,而这个D L L要链接到使用任何供应商的工具创建的可执行模块,那么必须做一些额外的工作。
前面讲过当进行C和C + +混合编程时使用e x t e r n“C”修改符的问题。也讲过C + +类的问题以及为什么因为名字改变的缘故你必须使用同一个编译器供应商的工具的问题。当你直接将C语言编程用于多个工具供应商时将会出现另一个问题。这个问题是,即使你根本不使用C + +,M i c r o s o f t的C编译器也会损害C函数。当你的函数使用_ _ s t d c a l l ( W I N A P I )调用规则时会出现这种问题。这种调用规则是最流行的一种类型。当使用_ _ s t d c a l l将C函数输出时,M i c r o s o f t的编译器就会改变函数的名字,设置一个前导下划线,再加上一个@符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。例如,下面的函数是作为D L L的输出节中的_ M y F u n c @ 8输出的:
__declspec(dllexport) LONG __stdcall MyFunc(int a, int b);
如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫M y F u n c的函数,该函数在M i c r o s o f t编译器已有的D L L中并不存在,因此链接将失败。
若要使用与其他编译器供应商的工具链接的M i c r o s o f t的工具创建一个可执行模块,必须告诉M i c r o s o f t的编译器输出没有经过改变的函数名。可以用两种方法来进行这项操作。第一种方法是为编程项目建立一个. d e f文件,并在该. d e f文件中加上类似下面的E X P O RT S节:
EXPORTS
MyFunc
当M i c r o s o f t的链接程序分析这个. d e f文件时,它发现_ M y F u n c @ 8和M y F u n c均被输出。由于这两个函数名是互相匹配的(除了截断的尾部外),因此链接程序使用M y F u n c的. d e f文件名来输出该函数,而根本不使用_ M y F u n c @ 8的名字来输出函数。
现在你可能认为,如果使用M i c r o s o f t的工具创建一个可执行模块,并且设法将它链接到包含未截断名字的D L L,那么链接程序的运行将会失败,因为它将试图链接到称为_ M y F u n c @ 8的函数。当然,你会高兴地了解到M i c r o s o f t的链接程序进行了正确的操作,将可执行模块链接到名字为M y F u n c的函数。
如果想避免使用. d e f文件,可以使用第二种方法输出未截断的函数版本。在D L L的源代码模块中,可以添加下面这行代码:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫M y F u n c的函数将被输出,其进入点与称为_ M y F u n c @ 8的函数的进入点相同。第二种方法没有第一种方法容易,因为你必须自己截断函数名,以便创建该代码行。另外,当使用第二种方法时, D L L实际上输出用于标识单个函数的两个符号,即M y F u n c和_ M y F u n c @ 8,而第一种方法只输出符号M y F u n c。第二种方法并没有给你带来更多的好处,它只是使你可以避免使用. d e f的文件而已。