如何使用单个的动态库 dll 文件

发布于 2022-01-16


背景

最近开发中会使用到一些三方动态库。有些动态库原本只是在 Linux 平台的,得通过 MinGW、Cygwin 等工具交叉编译出动态库 dll 文件放到 Windows 平台上才能使用。有时候发现最终的产物只是提供了 dll 文件,没有对应 lib 文件,在 Windows 平台上使用不是很方便。

写篇博客总结一下这个问题不同状况下的几种解决办法。

导出符号

首先了解一下导出符号。只有 dll 中导出了符号,外部才能加载 dll 使用这些符号的函数或者类。

最常见的是导出函数,也比较通用,C 和 C++ 中都能很好的二进制兼容。导出类则只能在 C++ 中使用,并且只有同一编译器中才有二进制兼容的保证。

我们可以通过 Windows 自带的 dumpbin.exe 来查看 dll 中的导出符号:

dumpbin.exe /EXPORTS C:\Windows\System32\kernel32.dll

输出:

    ordinal hint RVA      name

          1    0          AcquireSRWLockExclusive (forwarded to NTDLL.RtlAcquireSRWLockExclusive)
          2    1          AcquireSRWLockShared (forwarded to NTDLL.RtlAcquireSRWLockShared)
          3    2 0001E860 ActivateActCtx
          4    3 0001A570 ActivateActCtxWorker
          5    4 00021A70 AddAtomA
          6    5 0000FE30 AddAtomW
          7    6 00022DE0 AddConsoleAliasA
          8    7 00022DF0 AddConsoleAliasW
          9    8          AddDllDirectory (forwarded to api-ms-win-core-libraryloader-l1-1-0.AddDllDirectory)
         10    9 000375A0 AddIntegrityLabelToBoundaryDescriptor

通常有 def 文件和 __declspec(dllexport) 两种导出符号的方式,在下面再详细介绍。

符号修饰 name mangling

编译器对 dll 导出的符号是有过修饰的,根据调用约定方式、C 与 C++、参数有着不同的规则。

C 语言标准指定了统一的符号修饰规则,好像 C++ 却没有,不同的编译器有着不同的规则,这也造成二进制不兼容的问题。同时因为 C++ 有着函数重载、命名空间等特性,导致它的符号修饰规则更加复杂。

def 文件

def 文件是提供给链接器关于模块导出信息的文件。

  • def 文件的第一行必须是 LIBRARY 语句。用于指定 lib 库关联的 dll 名
  • EXPORTS 语句列出要导出的函数名,后面可通过 @ 跟上一个可选的序号值。

我们首先介绍一下 def 文件。下面是个 def 文件的示例:

LIBRARY   BTREE
EXPORTS
   Insert   @1
   Delete   @2
   Member   @3
   Min   @4

如果在 C 源文件中导出函数,在 C++ 中使用时,incude 头文件必须要 extern "C"

__declspec(dllexport)

__declspec(dllexport) 也可以导出函数,也可以导出 C++ 类。如下所示:

// def 中导出 def_cpp_add
int def_cpp_add(int a, int b);

// __declspec(dllexport) 方式导出
// 如果是在 C 工程中,导出 C 标准函数
// 如果是在 C++ 工程里,导出 C++ 标准函数
__declspec(dllexport) int def_cpp_sub(int a, int b);

// __declspec(dllexport) 方式导出 C 标准函数
extern "C" __declspec(dllexport) int def_cpp_mul(int a, int b);

输出结果如下:
?def_cpp_sub@@YAHHH@Z = ?def_cpp_sub@@YAHHH@Z (int __cdecl def_cpp_sub(int,int))
def_cpp_add = ?def_cpp_add@@YAHHH@Z (int __cdecl def_cpp_add(int,int))
def_cpp_mul = _def_cpp_mul

导出 C++ 类

可以使用 __declspec(dllexport) 导出 C++ 类。一个直接把整个类都导出

class __declspec(dllexport) Bar {
  Bar();
  virtual ~Bar();
  void Hello();
  static int Age();
  int a_ = 0;
  static int b_;
};

输出:
??0Bar@@AAE@XZ = ??0Bar@@AAE@XZ (private: __thiscall Bar::Bar(void))
??0Bar@@QAE@ABV0@@Z = ??0Bar@@QAE@ABV0@@Z (public: __thiscall Bar::Bar(class Bar const &))
??1Bar@@EAE@XZ = ??1Bar@@EAE@XZ (private: virtual __thiscall Bar::~Bar(void))
??4Bar@@QAEAAV0@ABV0@@Z = ??4Bar@@QAEAAV0@ABV0@@Z (public: class Bar & __thiscall Bar::operator=(class Bar const &))
??_7Bar@@6B@ = ??_7Bar@@6B@ (const Bar::`vftable')
?Age@Bar@@CAHXZ = ?Age@Bar@@CAHXZ (private: static int __cdecl Bar::Age(void))
?Hello@Bar@@AAEXXZ = ?Hello@Bar@@AAEXXZ (private: void __thiscall Bar::Hello(void))
?b_@Bar@@0HA = ?b_@Bar@@0HA (private: static int Bar::b_)

这样 Bar 类中所有函数和变量都导出了。

此外我们还可以选择性的导出 C++ 类中的一部分。

为了更好的二进制兼容性,比如把自己的 dll 给三方使用,可能是其他的编译器、运行时。这种情况下最好导出纯虚类接口,然后再提供函数接口来创建销毁类。

使用 dll

下面针对几种不同的情况,使用三方的 dll。

只有 dll、header、def 文件

这种情况比较理想,只是缺少了 lib 文件。

我们可以通过 Visual Studio 自带的 lib.exe 这个文件,从 def 文件文件中生成 lib文件,例如输入 demo.def 文件,生成对应的 demo.lib 文件:

lib.exe /def:demo.def /out:demo.lib

我们还可以通过 /MACHINE:{ARM|ARM64|EBC|X64|X86} 指定生成库的机器平台。

只有 dll、header 文件

这种情况下,我们先通过 dll 导出符号信息,构造出 def 文件,再通过 def 文件生成 lib 文件。

通过 dll 导出符号信息生成 def 文件

我们通过 dumpbin.exe 工具输出 dll 的导出符号信息,手动构造成 def 文件。

网上也有一些工具来帮忙做这个工作。

只有 dll、def 文件

这种情况下,我们只能通过 dll 导出符号信息,可以生成头文件。def 文件可以生成 lib 文件。

只有 dll 文件

这种情况,结合上面的几种情况,可以通过 dll 导出信息,生成 def 文件、头文件、lib 文件。

另外一种方式是直接通过 LoadLibraryGetProcAddress 来使用 dll,这比较适合调用 dll 中较少的函数。

参考