欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用MinGW写Windows DLLS

程序员文章站 2022-06-25 18:45:36
...

原文章链接:https://www.transmissionzero.co.uk/computing/building-dlls-with-mingw/

机翻给自己看

介绍

如何使用MinGW创建Windows DLLs.用MinGW创建DLLs并且将他们链接到你的应用。

基础

DLL是一种适用于微软Windows和OS/2的共享的库,包含了可以重用于各种应用的函数。如果有一个库foo.dll,包含了一个函数DoWork(),这个DLL必须导出函数以便被应用使用。比如,如果一个应用bar.exe想要使用函数DoWork(),他必须从foo.dll中导入函数(写一个当应用一开始启动就加载DLL的代码也是可以的,然后当不再使用的时候就卸载DLL——这是插件系统如何工作的)。导出函数和导入函数相当简单,就涉及到一些伴随着正确的链接器命令行代码。

不好的方法

首先,我们将创建一个DLL,导出一个非常基本的两个整数相加和返回结果功能。注意到在代码下面的“__declspec(dllexport)”属性,它是从DLL中导出函数的关键,每个你想从DLL中导出的函数应该有这个标记(任何不构成API的内部函数应该被留下)。同时注意到在函数名前的“__cdecl”,他声明该函数的调用协定(如果你不熟悉它们见*文章x86调用约定)。在MinGW中C的默认调用协定是cdecl,但这是一个好主意总是显式的指出调用协定,以防一些其他的编译器有不同的默认,如果发生这种情况,当调用这些函数之一时,您的应用程序可能会表现不好或崩溃。注意,第一个例子是故意的,不雅的,为了显示到底发生了什么。在下一节,我们将会通过使用预处理器定义隐藏这些细节。

/* add_basic.c

   Demonstrates creating a DLL with an exported function, the inflexible way.

*/


__declspec(dllexport) int __cdecl Add(int a, int b)

{

  return (a + b);

}

 

为了编译代码,只需要在通常的链接器命令行中选择“shared”

z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o add_basic.o add_basic.c

z:\Users\mpayne\Documents\MinGWDLL>gcc -o add_basic.dll -s -shared add_basic.o -Wl,--subsystem,windows

应该没有错误报告,你现在应该有一个可用的DLL。在这个例子中我分开了编译器和链接步骤,尽管对于这样一个小项目,本可以简单地用

gcc -o add_basic.dll -s -shared add_basic.c -Wl,--subsystem,windows

完成。

“-Wl,--subsystem,windows”

并不是必要的,但它是一个传统,在dll中指定Windows的GUI子系统PE头。还要注意“-s”选项,用于带符号的DLL,发布构建才做这个。

  

现在,您可以使用这个DLL构建一个应用程序。在使用函数Add(a,b)之前,所需要的是使用声明函数 __declspec(dllimport)(注意,这是客户机应用程序中的dllimport,而不是用于我们的DLL的dllexport)。)

/* addtest_basic.c

   Demonstrates using the function imported from the DLL, the inelegant way.

*/

#include <stdlib.h>

#include <stdio.h>

/* Declare imported function so that we can actually use it. */

__declspec(dllimport) int __cdecl Add(int a, int b);

int main(int argc, char** argv)

{

  printf("%d\n", Add(6, 23));

  return EXIT_SUCCESS;

}

现在编译链接这个程序,简单的使用命令行链接

z:\Users\mpayne\Documents\MinGWDLL>gcc -c -o addtest_basic.o addtest_basic.c

z:\Users\mpayne\Documents\MinGWDLL>gcc -o addtest_basic.exe -s addtest_basic.o -L. -ladd_basic

z:\Users\mpayne\Documents\MinGWDLL>addtest_basic.exe

29

 

“-L”选项指定链接器应该为DLL搜索的另一个文件夹。在这种情况下,我们希望链接器搜索当前目录,所以我们可以只使用一个句点,但是对于已经部署到系统的真实DLL,我们将使用到其目录的完整路径。“-l”选项指定我们想要从中导入函数的DLL——这应该是没有文件扩展名的DLL文件名。同样,对于这样一个小的项目,可以通过执行“gcc-o addtest_basic.exe-s addtest_basic.c-L.-ladd_basic”来编译和链接应用程序。

 

好用的方法

上面很好地演示了如何做,但是它并不理想——在真实的应用程序中,你不想在每个使用的源代码文件中声明每个导入的函数。相反,您可以将声明放置在头文件中,并在需要时包含它#include。唯一的问题是客户端应用程序需要声明函数 __declspec(dllimport),而在构建DLL时,必须声明它们 __declspec(dllimport)。虽然您可以使用两个独立的头部,但这可能会有点麻烦的维护工作,所以我们使用一些预处理器定义。

/* add.h

   Declares the functions to be imported by our application, and exported by our

   DLL, in a flexible and elegant way.

*/

/* You should define ADD_EXPORTS *only* when building the DLL. */

#ifdef ADD_EXPORTS

  #define ADDAPI __declspec(dllexport)

#else

  #define ADDAPI __declspec(dllimport)

#endif



/* Define calling convention in one place, for convenience. */

#define ADDCALL __cdecl



/* Make sure functions are exported with C linkage under C++ compilers. */

#ifdef __cplusplus

extern "C"

{

#endif

/* Declare our Add function using the above definitions. */

ADDAPI int ADDCALL Add(int a, int b);

#ifdef __cplusplus

} // __cplusplus defined.

#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++函数名进行篡改,这将导致链接器步骤失败。

 

请记住,这个代码不是可移植的,因为它在代码中使用微软特定的属性。这里没关系,因为这是关于构建WindowsDLL的教程,但是如果您需要跨平台的兼容性,则可以有条件地定义ADDAPI和ADCALL。通常情况下会这样做:

#ifdef _WIN32



  /* You should define ADD_EXPORTS *only* when building the DLL. */

  #ifdef ADD_EXPORTS

    #define ADDAPI __declspec(dllexport)

  #else

    #define ADDAPI __declspec(dllimport)

  #endif



  /* Define calling convention in one place, for convenience. */

  #define ADDCALL __cdecl



#else /* _WIN32 not defined. */



  /* Define with no value on non-Windows OSes. */

  #define ADDAPI

  #define ADDCALL



#endif

DLL的代码现在可以包括我们刚刚创建的头文件,并且我们需要做的唯一更改是在函数名之前包括“ADDCALL”(这里需要指定调用约定,以防止在函数的声明和定义之间可能发生冲突)。

/* add.c



   Demonstrates creating a DLL with an exported function in a flexible and

   elegant way.

*/



#include "add.h"



int ADDCALL Add(int a, int b)

{

  return (a + b);

}

为了编译这个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

客户端应用程序代码与以前基本相同,只是我们现在包括我们创建的头文件,而不是在源文件中声明函数。

/* addtest.c



   Demonstrates using the function imported from the DLL, in a flexible and

   elegant way.

*/



#include <stdlib.h>

#include <stdio.h>

#include <add.h>



int main(int argc, char** argv)

{

  printf("%d\n", Add(6, 23));



  return EXIT_SUCCESS;

}

 

编译时命令行不需要改变

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

29

这是许多现实世界库使用的构建过程。如果不清楚预处理器定义是怎么回事,那么在编译阶段在gcc命令行上传递“-save-temps”并查看带有“.i”扩展名的文件——您将注意到,最终优雅和不优雅示例中生成的代码几乎都是这样相同的。

 

导入和导出变量

除了函数之外,还可以导出和导入变量。这些变量必须声明为“extern __declspec(dllexport)”或“extern __declspec(dllimport)”,这取决于我们是使用这些变量构建DLL还是客户端应用程序。类似于函数,我们可以使用预处理器定义并简单地声明变量 “extern ADDAPI”。不应为变量指定调用约定。

在这里,我们将两个导出变量Foo和Bar添加到头文件中。

/* add_var.h



   Declares a function and variables to be imported by our application, and

   exported by our DLL.

*/



/* You should define ADD_EXPORTS *only* when building the DLL. */

#ifdef ADD_EXPORTS

  #define ADDAPI __declspec(dllexport)

#else

  #define ADDAPI __declspec(dllimport)

#endif



/* Define calling convention in one place, for convenience. */

#define ADDCALL __cdecl



/* Make sure functions are exported with C linkage under C++ compilers. */

#ifdef __cplusplus

extern "C"

{

#endif



/* Declare our Add function using the above definitions. */

ADDAPI int ADDCALL Add(int a, int b);



/* Exported variables. */

extern ADDAPI int foo;

extern ADDAPI int bar;



#ifdef __cplusplus

} // __cplusplus defined.

#endif

 

DLL的代码现在包括将值赋值给导出变量:

/* add_var.c



   Demonstrates creating a DLL with an exported function and imported variables.

*/



#include "add_var.h"



int ADDCALL Add(int a, int b)

{

  return (a + b);

}



/* Assign value to exported variables. */

int foo = 7;

int bar = 41;

已经修改了应用程序,以便现在将导入的foo和bar变量添加到一起,打印结果:

/* add_vartest.c



   Demonstrates using the function and variables exported by our DLL.

*/



#include <stdlib.h>

#include <stdio.h>



/* Don't forget to change this to #include <add.h> for real applications where

   the header has been deployed to a standard include folder!

*/

#include "add_var.h"



int main(int argc, char** argv)

{

  /* foo + bar = Add(foo, bar) */

  printf("%d + %d = %d\n", foo, bar, Add(foo, bar));



  return EXIT_SUCCESS;

}

编译与以前相同:

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

29

通常,DLL位于名为“bin”的文件夹中,导入库位于名为“lib”的文件夹中,例如“C:\Program Files\Add\bin\”和“C:\Program Files\Addlib”。

 

关于导入stdcall函数的警告

在声明函数 "__stdcall" 时, 应注意的一件事是, MinGW 和 MSVC 导出其函数的名称稍有不同 (函数修饰)。对于上面的 add 示例, MSVC 将导出 "add (int a, int b)" 函数, 其名称为 "_ [email protected]" (下划线、函数名称、符号、参数大小 (以字节为单位), 而 MinGW 将其导出为 "[email protected]" (与 MSVC 相同, 但没有下划线)。因此, 如果您打算在 DLL 的 MinGW 和 MSVC 生成之间建立二进制兼容性, 则必须更改导出函数的名称。这是一个比我打算在本文中介绍的稍微高级一点, 但我已经写了一个高级 MinGW DLL 主题文章, 解释如何做到这一点, 以及一些你应该注意的陷阱。

 

给你的DLL添加版本信息和注释

如果使用 Windows 资源管理器查看 dll 的属性并转到 "详细信息" 选项卡, 您可能会看到有关 dll 的信息, 如版本、作者、版权和库的说明。这是一个很好的触摸添加到 dll (特别有用, 当你想知道你的电脑上安装的 dll 的版本), 并且是相当简单的添加到您的 dll。您只需创建一个版本资源, 如下所示

#include <windows.h>



// DLL version information.

VS_VERSION_INFO    VERSIONINFO

FILEVERSION        1,0,0,0

PRODUCTVERSION     1,0,0,0

FILEFLAGSMASK      VS_FFI_FILEFLAGSMASK

#ifdef _DEBUG

  FILEFLAGS        VS_FF_DEBUG | VS_FF_PRERELEASE

#else

  FILEFLAGS        0

#endif

FILEOS             VOS_NT_WINDOWS32

FILETYPE           VFT_DLL

FILESUBTYPE        VFT2_UNKNOWN

BEGIN

  BLOCK "StringFileInfo"

  BEGIN

    BLOCK "080904b0"

    BEGIN

      VALUE "CompanyName", "Transmission Zero"

      VALUE "FileDescription", "A library to perform addition."

      VALUE "FileVersion", "1.0.0.0"

      VALUE "InternalName", "AddLib"

      VALUE "LegalCopyright", "©2013 Transmission Zero"

      VALUE "OriginalFilename", "AddLib.dll"

      VALUE "ProductName", "Addition Library"

      VALUE "ProductVersion", "1.0.0.0"

    END

  END

  BLOCK "VarFileInfo"

  BEGIN

    VALUE "Translation", 0x809, 1200

  END

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

这就是将版本信息添加到 DLL 的所有内容。这是值得花时间在您发布的任何 dll上, 因为它既有帮助, 又给你的应用程序一个专业的外观和感觉。

 

将他们合在一起

使用本文中的信息, 您现在应该能够将其全部放在一起, 并使用 MinGW 创建 dll, 以及使用从 dll 导出的函数的可执行文件。为了帮助您, 我创建了一个小项目, 它生成具有以下功能的 DLL:

 

  • 导出 "int 添加 (int a, int b);" 函数, 使用 cdecl 调用约定。
  • 导出的变量 "foo" 和 "bar"。
  • 嵌入到 DLL 中的版本信息资源。
  • 此外, 将生成一个可执行文件, 它使用导出的函数和导出的变量打印加法的结果。可以使用 mingw32-make 实用程序构建项目。

 

您可以从 GitHub 下载 MinGW dll 示例来构建本文的示例 DLL 和应用程序。https://github.com/TransmissionZero/MinGW-DLL-Example您可以要么 git 克隆存储库, 要么下载 MinGW DLL 示例版本。https://github.com/TransmissionZero/MinGW-DLL-Example/releases您可以*使用它为您认为适合的任何目的 (请参阅 "License.txt" 的完整使用条款)。使用项目文件, 您可以轻松地从本文中构建示例, 并将其用作您自己的 dll 和应用程序的基础。

相关标签: DLL