能否用某DLL全局变量初始化另一DLL全局变量?如何实现?
问题
能否使用一个DLL模块中的全局变量来初始化另一个DLL模块中的全局变量?如果可以,该如何实现?
使用Microsoft Visual Studio 17.3.6,结合C++/CLI包装类与部分C文件开发。在大型项目中遇到此问题,编写了简化示例复现该行为:
四个文件中,file1.c、file1.h属于一个项目,file2.c、file2.h属于另一个项目,分别编译为file1.dll和file2.dll。第一个项目定义预处理器符号file1_EXPORTS,第二个定义file2_EXPORTS,省略包含保护。
示例代码
file1.c
#include <file1.h> #include <file2.h> structure1_t struct1 = { &struct2 };
file1.h
typedef struct { structure2_t* ptr; } structure1_t; #ifdef file1_EXPORTS #define EXPORT_SYMBOL __declspec(dllexport) #else #define EXPORT_SYMBOL __declspec(dllimport) #endif EXPORT_SYMBOL structure1_t struct1;
file2.h
typedef struct { int* i; } structure2_t; #if defined (file2_EXPORTS) #define EXPORT_SYMBOL __declspec(dllexport) #else #define EXPORT_SYMBOL __declspec(dllimport) #endif EXPORT_SYMBOL structure2_t struct2;
file2.c
#include <file2.h> #include <file1.h> int i; structure2_t struct2 = { &i };
编译与运行问题
- 按上述代码编译会报错
C2099: initializer is not a constant。 - 将
file2.h中的#define EXPORT_SYMBOL __declspec(dllimport)改为#define EXPORT_SYMBOL extern时,出现链接错误LNK2001: unresolved external symbol struct2和LNK1120: 1 unresolved externals。 - 唯一可编译的组合是将
EXPORT_SYMBOL定义为空,同时保留file1.h中的定义,但生成DLL后用file3.cpp测试:
file3.cpp
#include <file1.h> #include <file2.h> #include <iostream> int main(void) { std::cout << struct1.ptr << std::endl; std::cout << struct2.i << std::endl; return 0; }
运行输出:
0000000000000000 0000000000000000
核心疑问
- 是否存在方式让指针不为NULL(
struct1指向struct2,struct2指向整数i)? - 程序为何如此表现?是否因
file3.cpp仅能看到头文件声明,变量未正确初始化? - 已有两种备选方案:编写初始化函数填充结构(但结构数量多,不愿手动完成);在
DllMain中初始化。但想知道是否有更直接的方式。
解答
为什么直接跨DLL全局初始化不可行?
在Windows平台下,DLL的全局变量初始化是在DLL加载阶段完成的,而不同DLL的加载顺序是不确定的。更关键的是,C语言要求全局变量的初始化器必须是编译期常量——跨DLL的全局变量地址属于运行时才能确定的值,无法在编译阶段被解析为常量,这就是你遇到C2099错误的根本原因。
当你把__declspec(dllimport)改成extern时,链接器无法在当前模块(file1.dll)中找到struct2的定义,因为它属于另一个DLL,所以会抛出LNK2001未解析符号错误。而把EXPORT_SYMBOL设为空时,相当于每个DLL都创建了自己的struct1和struct2副本,这些副本的初始化值自然是NULL,因为没有真正关联到另一个DLL的变量。
可行的解决方案
虽然无法直接通过编译期初始化实现,但可以通过以下方式达到目标:
1. 使用DLL初始化函数(推荐)
在每个DLL中添加一个初始化函数,在DLL加载完成后手动完成跨模块的指针赋值。如果结构数量多,可以通过标记或自动遍历的方式批量处理:
- 在头文件中声明初始化函数,比如
void InitDLLGlobals(); - 在
file1.c中实现:
#include <file1.h> #include <file2.h> void InitDLLGlobals() { struct1.ptr = &struct2; }
- 在
file2.c中实现:
#include <file2.h> void InitDLLGlobals() { struct2.i = &i; }
- 在主程序
file3.cpp中,加载DLL后依次调用两个初始化函数,或者在每个DLL的DllMain的DLL_PROCESS_ATTACH阶段调用自身的初始化函数(注意DllMain中避免复杂操作,简单赋值是安全的)。
2. 使用延迟加载(Delay Load)结合初始化函数
通过Visual Studio的延迟加载特性,让DLL在第一次使用时才加载,然后在第一次访问全局变量前触发初始化。不过这种方式需要额外配置项目属性,复杂度略高。
3. 合并DLL(如果允许)
如果业务上可以将两个模块合并为一个DLL,那么全局变量的跨模块初始化就变成了模块内初始化,此时编译期可以直接解析变量地址,不会有任何问题。
关于运行输出全为NULL的解释
当你把EXPORT_SYMBOL设为空时,file1.dll和file2.dll各自定义了一份struct1、struct2和i的副本。主程序file3.cpp中访问的是哪个副本取决于链接时的依赖顺序,但无论如何,这些副本的初始化器都是NULL(因为编译期无法获取另一个DLL中变量的地址),所以输出全为0。
内容的提问来源于stack exchange,提问作者Buse

