C ++模块和动态链接的预期关系是什么?

Ytt*_*ill 5 c++ c++-modules

C ++模块TS为消除预处理器,缩短编译时间,并为至少非模板代码至少支持C ++中更健壮,模块化的代码开发提供了绝佳的工具。

底层机制可控制普通程序中符号的导入和导出。

但是,开发用于两种动态加载的库存在一个主要问题:启动时加载和运行时加载。此问题涉及从库中导出符号,通常会在可见性方面进行讨论。

通常,并非所有用于构建动态链接库的翻译单元的外部符号都应对用户可见。此外,在运行时加载时,尤其是在使用插件概念加载时,必须从许多同时加载的库中导出同一符号。

在Windows上使用语言扩展

 __declspec(dllexport)
 __declspec(dllimport)
Run Code Online (Sandbox Code Playgroud)

附加在源代码中作为符号的属性,最近在unix平台上的gcc和clang系统上,

__attribute__((visibility("default")))
__attribute__((visibility("hidden"))) 
Run Code Online (Sandbox Code Playgroud)

旨在支持打算由图书馆公开的符号的提供和使用。使用它们是复杂且混乱的:在Windows上,在编译库时必须使用宏来导出符号,而在使用它们时则要导入它们。在unix平台上,必须将可见性设置为默认设置,以同时导出和导入符号,由编译器根据是否找到定义来决定其自身:必须使用以下命令调用编译器:

-fvisibility=hidden 
Run Code Online (Sandbox Code Playgroud)

开关。静态链接不需要export / import属性,可能应该将其宏化为空字符串。编写代码并摆弄构建系统,以使所有这些工作正常进行,尤其是考虑到#include必须在库翻译单元的编译过程中设置正确的符号可见性,这非常困难,存储库中所需的文件结构混乱,源代码杂乱无章与宏,总的来说,整个事情都是一场灾难。几乎所有开放源代码存储库都无法正确导出符号以进行动态链接,并且大多数程序员都不知道动态库代码结构(使用两级名称空间)与静态链接完全不同。

可以在此处看到如何执行此操作的示例(希望正确):

https://github.com/calccrypto/uint256_t

该存储库曾经有2个标头和2个实现文件,内置库的用户将看到2个标头。现在有7个头文件和2个实现文件,内置库的用户将看到5个头文件(其中3个扩展名include表示不直接包含这些文件)。

因此,在经过漫长的解释之后,问题是:最终的C ++模块规范是否有助于解决动态链接符号的导入和导出问题?我们可以期望能够开发共享库而不会用供应商特定的扩展名和宏来污染我们的代码吗?

jer*_*ong 11

模块无法帮助您跨 DLL 边界实现符号可见性。我们可以通过一个快速实验来检查这一点。

// A.ixx
export module A;

export int f() { return 1; }
Run Code Online (Sandbox Code Playgroud)

这里我们有一个简单的模块接口文件,导出f模块的模块接口中的一个符号A(恰好与文件基名共享相同的名称,但这不是必需的)。让我们像这样编译:

cl /c /std:c++20 /interface /TP A.ixx
Run Code Online (Sandbox Code Playgroud)

/c标志避免调用链接器(默认情况下自动发生),c++20或者稍后需要模块语法才能工作,并且该/interface标志让编译器知道我们正在编译模块接口单元。arg/TP表示“将源输入视为 C++ 输入”,并且在/interface指定时需要。最后,我们有了输入文件。

运行上面的代码会生成一个接口文件A.ifc和一个目标文件A.obj。请注意,如果您正在编译 DLL,则不会出现您期望的导入 lib 文件或 exp 文件。

接下来,让我们编写一个使用它的文件。

// main.cpp
import A;

int main() { return f(); }
Run Code Online (Sandbox Code Playgroud)

要将其编译为可执行文件,我们可以使用以下命令:

cl /std:c++20 main.cpp A.obj
Run Code Online (Sandbox Code Playgroud)

输入的存在A.obj不是可选的。如果没有它,我们就会遇到一个典型的链接器错误,即f成为未解析的符号。如果我们运行它,我们将得到一个main.exe静态链接A.obj.

如果我们尝试编译A.ixx成 DLL 会发生什么?也就是说,如果我们尝试使用来自的链接器生成 DLL 会怎样A.obj?答案是你得到一个 DLL,但没有导入库或 exp。如果您尝试运行,link /noentry /dll A.obj /out:A.dll您将得到一个A.dll包含预期/disasm部分的内容(通过转储箱可见),但没有导出表。

Dump of file A.dll

File Type: DLL

  0000000180001000: B8 01 00 00 00     mov         eax,1
  0000000180001005: C3                 ret

  Summary

        1000 .rdata
        1000 .text
Run Code Online (Sandbox Code Playgroud)

这是我们期望的反汇编A.dll,但检查导出部分却dumpbin /export A.dll没有发现任何结果。原因当然是我们没有导出符号!

如果我们将 的源代码更改A.ixx为以下内容:

// A.ixx
export module A;

export __declspec(export) int f() { return 1; }
Run Code Online (Sandbox Code Playgroud)

...我们可以重复这些步骤(compile A.obj、link A.dll),发现这一次,链接器按照我们的预期生成了一个导入lib和exp文件。调用生成的导入库dumpbin /exports A.lib应该显示?f@@YAHXZ存在的符号。

现在,我们可以main.cpp再次链接A.lib(而不是A.obj)viacl /std:c++20 main.cpp A.lib来生成有效的可执行文件,这次依赖于A.dll代码而不是f静态嵌入。

我们可以检查这实际上在 WinDbg 中是否按预期发生。

在此输入图像描述

请注意左下模块窗格中存在A.dll. 另请注意,在中心的反汇编视图中,我们将调用main!f. 呃哦,不好。虽然这确实正确解析了!A模块,但它是通过导入地址表中的额外间接来实现的,如下所示:

在此输入图像描述

这是当您忘记用指令修饰函数或符号时发生的经典问题__declspec(dllimport)。当编译器遇到不带dllimport它无法识别的指令的符号时,它会发出一个重定位条目,预计该条目将在链接时解析。除了该条目之外,它还会发出一个jmp未解析的地址。这是一个经典问题,我不会在这里讨论,但结果是我们有一个额外的不必要的间接,因为被识别为从模块导出的符号A应该是静态链接的。

事实证明,我们无法轻易解决这个问题。f如果我们尝试添加另一个to声明main.cpp或其他翻译单元,链接器会抱怨它看到f“不一致的 dll 链接”。解决这个问题的唯一方法是编译A带有装饰的模块接口的第二个版本dllimport(很像标头通常具有扩展为dllexportdllimport依赖于使用标头的 TU 的宏)。

这个故事的寓意是,DLL 链接和模块链接虽然并不完全矛盾,但也不是特别兼容。该模块export在导出表中不包括解析跨 DLL 边界符号所需的导出符号。此外,在通过导入地址表完成隐式动态链接之后,将这些符号放入导出表中仍然会给您带来额外间接的麻烦。