使用minGW创建DLLs
不灵活优雅的方式
首先,我们将创建一个DLL,只导出一个非常基本的函数,该函数将两个整数相加并返回结果。注意到下方代码中的“__declspec(dllexport)”,这是从DLL导出函数的关键。你想从DLL中导出的每个函数都应该用这个标记(任何不构成API一部分的内部函数都应该保持原样)。另外,注意到函数名前面的“__cdecl” 它声明了函数的调用约定(如果你不熟悉,请参阅Wikipedia关于x86调用约定的文章)。MinGW中C函数的默认调用约定是cdecl,但最好总是显式地声明调用约定,以防其他编译器有不同的默认值—如果发生这种情况,您的应用程序很可能会因为调用这些函数之一而出现错误行为或崩溃。请注意,为了准确地显示幕后发生了什么,第一个示例故意显得不灵活和不优雅。在下一节中,我们将使用预处理器定义隐藏这些细节。
1. /* add_basic.c
2. Demonstrates creating a DLL with an exported function, the inflexible way.
3. */
4.
5. __declspec(dllexport) int __cdecl Add(int a, int b)
6. {
7. return (a + b);
8. }
要编译它,只需要添加命令" -shared ":
>gcc -c -o add_basic.o add_basic.c
>gcc -o add_basic.dll -s -shared add_basic.o -Wl,--subsystem,windows
第一条命令生成文件add_basic.o,第二条命令生成add_basic.dll文件
应该不会报告任何错误,你现在应该有一个可用的DLL。在本例中,我将编译器和链接步骤分开。虽然对于这样一个小项目可以将其写在一起
gcc -o add_basic.dll -s -shared add_basic.c -Wl,--subsystem,windows
其中 -Wl,--subsystem,windows 不是必要的参数,因为不是编译窗口程序。注意 -s 选项,它清理导出的 DLL 符号,你可能只会想在发布发行版时需要。
现在可以构建使用此DLL的应用程序。为了使用Add(a, b)函数,需要在使用函数之前用" __declspec(dllimport) "属性对函数进行声明。 (注意它是用于客户端应用程序的dllimport,而不是在我们的DLL中使用的dllexport)。
1. /* addtest_basic.c
2.
3. Demon-strates using the function imported from the DLL, the inelegant way.
4. */
5.
6. #include <stdlib.h>
7. #include <stdio.h>
8.
9. /* Declare imported function so that we can actually use it. */
10. __declspec(dllimport) int __cdecl Add(int a, int b);
11.
12. int main(int argc, char** argv)
13. {
14. printf("%d\n", Add(6, 23));
15.
16. return EXIT_SUCCESS;
17. }
现在,只需在命令行上引用DLL,就可以编译和链接此应用程序。
>gcc -c -o addtest_basic.o addtest_basic.c
>gcc -o addtest_basic.exe -s addtest_basic.o -L. -ladd_basic
>addtest_basic.exe
" -L "选项令gcc搜索DLL的附加文件夹。在这种情况下,我们希望gcc搜索当前目录,所以我们可以只使用句点,但对于一个真正的DLL,我们已经部署到一个系统,我们将使用其目录的完整路径。" -l "选项指定了我们想要从其中导入函数的DLL -这应该是不带文件扩展名的DLL文件名。同样,对于这样一个小项目,可以通过执行“gcc -o addtest_basic.exe -s addtest_basic.c –L. -ladd_basic””来编译和链接应用程序。
灵活优雅的方式
上面的内容很好地演示了所发生的事情,但它还不够理想—在实际的应用程序中,您不会希望在使用它的每个源代码文件中声明每个导入函数。相反,您可以将声明放在头文件中,并在需要的地方使用#include头文件。唯一的问题是客户端应用程序要求函数声明为" __declspec(dllimport) ",而当构建DLL时,你必须声明它们" __declspec(dllexport) "。尽管您可以使用两个单独的头文件,但这可能会带来一些维护方面的麻烦,因此我们改用一些预处理器定义。
1. /* add.h
2.
3. Declares the functions to be imported by our application, and exported by our
4. DLL, in a flexible and elegant way.
5. */
6.
7. /* You should define ADD_EXPORTS *only* when building the DLL. */
8. #ifdef ADD_EXPORTS
9. #define ADDAPI __declspec(dllexport)
10. #else
11. #define ADDAPI __declspec(dllimport)
12. #endif
13.
14. /* Define calling convention in one place, for convenience. */
15. #define ADDCALL __cdecl
16.
17. /* Make sure functions are exported with C linkage under C++ compilers. */
18.
19. #ifdef __cplusplus
20. extern "C"
21. {
22. #endif
23.
24. /* Declare our Add function using the above definitions. */
25. ADDAPI int ADDCALL Add(int a, int b);
26.
27. #ifdef __cplusplus
28. } // __cplusplus defined.
29. #endif
注意,如果定义了" ADD_EXPORTS ",则定义了" ADDAPI "为" __declspec(dllexport) ",否则定义为" __declspec(dllimport) "。这使我们能够在应用程序和DLL中使用相同的头文件。还要注意,我们将" ADDCALL "定义为" __cdecl ",它允许简单地重新定义我们正在使用的API的调用约定。现在,在代码中任何需要从DLL中导出函数的地方,我们都会指定" ADDAPI "而不是" __declspec(dllexport) "属性,并指定" ADDCALL "而不是指定,即" __cdecl "。通常将这些预处理器定义命名为“[库名]_EXPORTS”、“[库名]API”和“[库名]CALL”,所以坚持这样做是一个好主意,这样你的代码自己和别人都能读懂。
最后,请注意导入/导出的函数都应该包装在一个包含" extern "C"语句的" #ifdef __cplusplus "块中。这使得C和c++应用程序都可以使用头文件——如果没有头文件,c++编译器将使用c++函数名mangling,这将导致链接器步骤失败。
请记住,这段代码是不可移植的,因为它在代码中使用了Microsoft特定的属性。这没问题,因为这是一个关于构建Windows dll的教程,但如果你需要跨平台兼容性,那么可以有条件地定义ADDAPI和ADDCALL。通常会这样做:
1. #ifdef _WIN32
2.
3. /* You should define ADD_EXPORTS *only* when building the DLL. */
4. #ifdef ADD_EXPORTS
5. #define ADDAPI __declspec(dllexport)
6. #else
7. #define ADDAPI __declspec(dllimport)
8. #endif
9.
10. /* Define calling convention in one place, for convenience. */
11. #define ADDCALL __cdecl
12.
13. #else /* _WIN32 not defined. */
14.
15. /* Define with no value on non-Windows OSes. */
16. #define ADDAPI
17. #define ADDCALL
18.
19. #endif
DLL的代码现在可以include我们刚刚创建的头文件,我们唯一需要做的更改是在函数名之前增添“ADDCALL”(有必要在这里指定调用约定,以防止函数的声明和定义之间可能的冲突)。
1. /* add.c
2.
3. Demonstrates creating a DLL with an exported function in a flexible and
4. elegant way.
5. */
6.
7. #include "add.h"
8.
9. int ADDCALL Add(int a, int b)
10. {
11. return (a + b);
12. }
要编译这个DLL,在编译目标代码时需要定义“ADD_EXPORTS”,以确保“ADDAPI”在头文件中被正确定义。最简单的方法是在命令行中输入" -D ADD_EXPORTS ":
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o add.o add.c -D ADD_EXPORTS
z:\Users\mpayne\Documents\MinGWDLL>gcc -o add.dll add.o -s -shared -Wl,--subsystem,windows
客户机应用程序代码与以前的代码基本相同,只是现在我们#include我们创建的头文件,而不是在源文件中声明函数
1. /* addtest.c
2.
3. Demonstrates using the function imported from the DLL, in a flexible and
4. elegant way.
5. */
6.
7. #include <stdlib.h>
8. #include <stdio.h>
9. #include <add.h>
10.
11. int main(int argc, char** argv)
12. {
13. printf("%d\n", Add(6, 23));
14.
15. return EXIT_SUCCESS;
16. }
编译dll所需的命令不需要变动
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o addtest.o addtest.c
z:\Users\mpayne\Documents\MinGWDLL>gcc -o addtest.exe -s addtest.o -L. -ladd
z:\Users\mpayne\Documents\MinGWDLL>addtest.exe
这是许多实际库使用的构建过程。如果不清楚预处理器定义的情况,可以在编译阶段在gcc命令行中传递" -save-temps ",然后查看带有"的文件。I " extension—您会注意到,最终在优雅和不优雅的示例中生成的代码几乎是相同的。
变量的导入导出
除了函数之外,还可以导出和导入变量。这些变量必须声明为" extern __declspec(dllexport) "或" extern __declspec(dllimport) ",这取决于我们是在构建DLL还是使用这些变量构建客户端应用程序。与函数类似,我们可以使用预处理器定义,并简单地声明变量“extern ADDAPI”。不应为变量指定调用约定。
在这里,我们向头文件添加了两个导出变量,foo和bar
1. /* add_var.h
2.
3. Declares a function and variables to be imported by our application, and
4. exported by our DLL.
5. */
6.
7. /* You should define ADD_EXPORTS *only* when building the DLL. */
8. #ifdef ADD_EXPORTS
9. #define ADDAPI __declspec(dllexport)
10. #else
11. #define ADDAPI __declspec(dllimport)
12. #endif
13.
14. /* Define calling convention in one place, for convenience. */
15. #define ADDCALL __cdecl
16.
17. /* Make sure functions are exported with C linkage under C++ compilers. */
18. #ifdef __cplusplus
19. extern "C"
20. {
21. #endif
22.
23. /* Declare our Add function using the above definitions. */
24. ADDAPI int ADDCALL Add(int a, int b);
25.
26. /* Exported variables. */
27. extern ADDAPI int foo;
28. extern ADDAPI int bar;
29.
30. #ifdef __cplusplus
31. } // __cplusplus defined.
32. #endif
DLL的代码现在包含了对导出变量的赋值:
1. /* add_vartest.c
2.
3. Demonstrates using the function and variables exported by our DLL.
4. */
5.
6. #include <stdlib.h>
7. #include <stdio.h>
8.
9. /* Don't forget to change this to #include <add.h> for real applications where
10. the header has been deployed to a standard include folder!
11. */
12. #include "add_var.h"
13.
14. int main(int argc, char** argv)
15. {
16. /* foo + bar = Add(foo, bar) */
17. printf("%d + %d = %d\n", foo, bar, Add(foo, bar));
18.
19. return EXIT_SUCCESS;
20. }
编译过程和之前相同
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o add_var.o add_var.c -D ADD_EXPORTS
z:\Users\mpayne\Documents\MinGWDLL>gcc -o add_var.dll add_var.o -s -shared -Wl,--subsystem,windows
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o add_vartest.o add_vartest.c
z:\Users\mpayne\Documents\MinGWDLL>gcc -o add_vartest.exe -s add_vartest.o -L. -ladd_var
z:\Users\mpayne\Documents\MinGWDLL>add_vartest.exe
7 + 41 = 48
如果更改foo和bar的值,重新编译DLL,并再次运行应用程序,您可以看到变量确实是从DLL导入的。
库的导入
虽然通常可以通过在系统上显示DLL并添加一些命令行选项来链接DLL,但这并不总是理想的。在这种情况下,应该使用导入库。导入库不包含任何代码,但包含应用程序查找DLL导出的函数所需的所有必要信息。当创建供第三方使用的库时,我建议总是创建一个导入库,并将其与DLL和头文件一起分发,因为您的用户可能需要构建他们的应用程序。
如果修改了DLL导出的函数名(请参阅下面的“关于导出stdcall函数的警告”),则导入库是唯一的选择。这是Windows API的情况,你必须使用导入库链接你的应用程序,即在编译Windows GUI程序时,在链接器命令行上传递" -lgdi32 -luser32 "等信息。
导入库的创建于使用
如果你没有对由DLL导出的任何函数名做过修改,那么创建导出库只要传递一个额外参数“-Wl,--out-implib,lib[library name].a”
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o add.o add.c -D ADD_EXPORTS
z:\Users\mpayne\Documents\MinGWDLL>gcc -o add.dll add.o -s -shared -Wl,--subsystem,windows,--out-implib,libadd.a
Creating library file: libadd.a
通常在导入库名称中使用库名称。文件名必须以“lib”开头,以“.a”结尾。 (实际上,你可以使用一些支持的命名约定,这些在手册中都有介绍,但其他的都不起作用)。
构建应用程序与以前一样,但不是将包含DLL的目录传递给链接器,而是传递包含导入库的目录(实际上我们仍然从当前目录执行所有操作,但实际应用程序可能不是这样)。若要链接到DLL,则传递不带lib前缀和文件扩展名的导入库名称.
z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o addtest.o addtest.c
z:\Users\mpayne\Documents\MinGWDLL>gcc -o addtest.exe -s addtest.o -L. -ladd
z:\Users\mpayne\Documents\MinGWDLL>addtest.exe
通常dll放在名为“bin”的文件夹中,而导入库放在名为“lib”的文件夹中,例如“C:\Program Files\Add\bin\”和“C:\Program Files\Add\lib\”。
关于导出stdcall函数的警告
在声明函数" __stdcall "时,你应该注意的一件事是,MinGW和MSVC导出的函数名称略有不同(函数装饰)。对于上面的添加示例,MSVC将导出名为“_Add@8”(下划线,函数名,@,参数字节大小)的“add (int a, int b)”函数,而MinGW将导出为“Add@8”(与MSVC相同,但没有下划线)。因此,如果你想在你的DLL的MinGW和MSVC构建之间有二进制兼容性,就有必要更改导出函数的名称。这比我打算在本文中介绍的要高级一些,但是我已经写了一篇高级MinGW DLL主题文章,解释了如何做到这一点,以及一些您应该注意的陷阱。
在DLL中添加版本信息和注释
如果您使用Windows资源管理器查看DLL的属性,然后转到“details”选项卡,您可能会看到有关该DLL的信息,如版本、作者、版权和库的描述。将其添加到DLL中是一种很好的方法(当您想知道您的PC上安装了哪个版本的DLL时,这尤其有用),并且将其添加到DLL中也相当简单。你只需要创建一个版本资源,如下所示:
1. #include <windows.h>
2.
3. // DLL version information.
4. VS_VERSION_INFO VERSIONINFO
5. FILEVERSION 1,0,0,0
6. PRODUCTVERSION 1,0,0,0
7. FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
8. #ifdef _DEBUG
9. FILEFLAGS VS_FF_DEBUG | VS_FF_PRERELEASE
10. #else
11. FILEFLAGS 0
12. #endif
13. FILEOS VOS_NT_WINDOWS32
14. FILETYPE VFT_DLL
15. FILESUBTYPE VFT2_UNKNOWN
16. BEGIN
17. BLOCK "StringFileInfo"
18. BEGIN
19. BLOCK "080904b0"
20. BEGIN
21. VALUE "CompanyName", "Transmission Zero"
22. VALUE "FileDescription", "A library to perform addition."
23. VALUE "FileVersion", "1.0.0.0"
24. VALUE "InternalName", "AddLib"
25. VALUE "LegalCopyright", "©2013 Transmission Zero"
26. VALUE "OriginalFilename", "AddLib.dll"
27. VALUE "ProductName", "Addition Library"
28. VALUE "ProductVersion", "1.0.0.0"
29. END
30. END
31. BLOCK "VarFileInfo"
32. BEGIN
33. VALUE "Translation", 0x809, 1200
34. END
35. END
我不会详细说明每一个的含义,因为在MSDN中已经很好地介绍了,但大多数都是不言自明的(我建议阅读MSDN的文章,特别是如果语言是美式英语或英式英语以外的其他语言)。
其思想是使用windres.exe编译资源脚本,然后在链接DLL时将其传递给链接器。例如,如果你的资源脚本名为“resource.rc”:
z:\Users\mpayne\Documents\MinGWDLL>windres -i resource.rc -o resource.o
z:\Users\mpayne\Documents\MinGWDLL>gcc -o add.dll add.o resource.o -s -shared -Wl,--subsystem,windows,--out-implib,libadd.a
将所有东西放在一起
使用本文提供的信息,您现在应该能够将它们组合在一起,并使用MinGW创建dll,以及使用从dll导出的函数的可执行文件。为了帮助你,我创建了一个小项目,它构建了一个DLL,具有以下功能:
- 使用cdecl调用约定,导出“int Add(int a, int b);”函数。
- 导出变量foo和bar。
- 嵌入到DLL中的版本信息资源。
此外,还将构建一个可执行文件,使用导出的函数和导出的变量打印添加的结果。可以使用mingw32-make实用程序构建该项目
需要注意的事情
当从DLL公开C API时,有几件事需要注意。首先,您的代码不能在DLL中抛出任何c++异常,并期望客户机应用程序捕获这些异常。如果您使用纯C代码,这应该不是问题,因为C不支持c++异常,但如果您使用c++编写代码,并为导出的函数指定C链接,这是您需要小心的事情。最好在可能抛出异常的地方捕获所有异常,并向您所暴露的API的调用者返回一些错误代码。
如果你确实穿过DLL boundary抛出了一个异常,你可能很幸运,它会为你工作。但在其他人使用不同语言、不同编译器或者一个不同版本编译器运行你的库的时候,它很可能会失败。 (因为你用c写的库,,并不意味着其他人也用c,例如,在用Visual Basic、汇编、D或c#编写的应用程序中)。
另一件需要注意的事情是,如果你将struct传递给一个由DLL导出的函数,或者从该函数返回一个struct,你以后不能改变该struct的定义。进行这样的更改可能会导致旧应用程序崩溃或以意想不到的方式运行,因为自编译应用程序以来,结构体的内存布局已经发生了更改。在不破坏向后兼容性的情况下改变结构的定义并非不可能,但这也不是很优雅。例如,在Windows API中,结构通常有一个(看起来毫无意义的)表示结构大小的成员,在结构定义的末尾添加了新成员,并通过地址而不是值传递给函数(例如OPENFILENAME结构)。这些东西允许Windows API在不同版本的Windows之间改变,而不会破坏向后兼容性(我知道人们可能会说每个版本的Windows都会导致许多向后兼容性问题,但事实是,一个为Windows 95创建的编写良好的应用程序很有可能运行在最新版本的Windows上,在我看来这是相当令人印象深刻的)。
虽然可以从DLL中导出c++变量和c++函数,但应该避免这样做,除非您真正了解自己在做什么。c++不具有C所具有的定义良好的ABI,您可能会发现,使用一个供应商的编译器创建的编译后的c++函数与另一个供应商的编译器不兼容。
事实上,您可能会发现来自同一厂商的不同版本的c++编译器之间的兼容性被破坏了。由于这个原因,每个编译器都有自己的方式来处理c++函数名,这就防止了由不同的c++编译器创建的函数被链接在一起。令人失望的是,它防止了因ABI差异而可能发生的潜在混乱。我在我的高级MinGW DLL主题文章中介绍了c++ DLL。
原文地址:https://www.transmissionzero.co.uk/computing/building-dlls-with-mingw/
高级MinGW DLL:https://www.transmissionzero.co.uk/computing/advanced-mingw-dll-topics/