定义外部可用的全局符号

有时候,在插件或者其他的模块中,想要定义一个全局对象,供内部或者外部的模块访问,就像 UE 内置的 GEngine/GConfig 这些。

首先需要把 C++ 中 声明 定义 的概念区分开,我之前有篇文章介绍:C++ 中 declaration 与 define 的区别

因为我们想要访问的 全局对象,应该只有一个实例,而不是每个使用模块都包含了一个定义的实例,所以只应该在有一个定义,其他模块引用时只是一个声明,声明它在外部模块中定义,应该去其他模块中访问该实例。

以 Windows 为例,UE 的模块,会编译成 DLL,如果我们想要访问某个模块的符号,就是要访问它的 DLL 中的符号。在 UE 中,导出符号使用 XXXX_API 封装,同样在之前的笔记中有记录:

以 A、B 两个模块为例:A_API在 A 模块中被定义为 DLLEXPORT,在 B 模块中被定义为DLLIMPORT,所以,A 中使用A_API 定义一个符号,会控制它编译出的 A.dll 中该符号导出,在外部模块 B 中使用该符号时,就是导入。

理解起来或许有点绕,以代码来理解:

1
2
3
4
5
6
7
// A.h
extern A_API int32* GTestPtr; // declare a external pointer symbal
// extern DLLEXPORT int32* GTestPtr = nullptr;

// A.cpp
A_API int32* GTestPtr = nullptr; // define the global symbal
// DLLEXPORT int32* GTestPtr = nullptr;

在 B 中访问它:

1
2
3
4
// B.cpp
#include "A.h"

if(GTestPtr){}

在 B 中包含 A.h,在 B 中,A.h 中的 extern A_API int32* GTestPtr; 就变成了以下代码:

1
extern DLLIMPORT int32* GTestPtr;

在编译 B 模块时,DLLIMPORT会指导链接器 (linker) 去外部的模块查找该符号,从而实现在 B 中访问 A 中定义的全局对象。

错误用法 1:

1
2
3
4
// A.h
int32* GTestPtr;
// A.cpp
int* GTestPtr = NULL;

如果在 A 中这么声明,在 B 中访问 GTestPtr 则会有符号未定义的链接错误。

错误用法 2:

1
2
// A.h
static int32* GTestPtr;

则每个包含 A.h 的翻译单元都包含了一个 GTestPtr 的符号实例,并不是全局唯一的。