将单例实例共享到共享库而不导出它

Share singleton instance to shared lib's without exporting it

我正在寻找一种共享单例实例的替代方法 在主要可执行文件和 dll 之间。

我的项目目前包含几个链接到可执行文件中的静态库,如下所示:

  common.lib (holds singletons)
      /             \
     /               \
    v                 \
tools.exe              \
                        V
                  database.lib (holds singletons)
                        |
                        V
                   shared.lib (holds singletons)
                     /
                    /
                   |
                   v
               game.lib (holds singletons)
                   |
                   v           I-- Extension.dll
                server.exe <---I-- Extension.dll (dynamic loaded extensions)
                               I-- Extension.dll
                                       ^
                                       |
+--------------------------------------------------------+
I Extensions loaded through LoadLibrary & dlopen         I
I need to have access to the singletons instantiated in: I
I common.lib, database.lib, shared.lib and game.lib      I

静态库提供了几个我想公开给动态加载的 dll 的单例,它们始终与主 exe 二进制兼容

我不能只将静态库转换为动态库,因为它会破坏很多并且需要太多努力。 我目前的做法是转静态单例getter:

class Log
{
public:
    static Log* instance();
};

变成类似这样的东西:

class Log { };

__declspec(dllexport) Log* instance();

并通过单独的动态库导出单例实例:

common_inst.dllcommon.lib.

导出单例

game_inst.dllgame.lib.

导出单例

这种方法有效,但我对此并不满意。

是否有另一种跨平台兼容的可能性,可以在不通过 dllexport 导出的情况下将单例共享到共享库?

如果您真的不想在主代码中包含任何 DLL 或导出,您可以使用虚函数方法。

假设每个扩展都有一个用于初始化的导出函数:

DLLEXPORT void InitExtension(MainProgramInfo info);

现在您必须添加您的扩展程序想要访问传递的结构的所有内容:

struct MainProgramInfo {
  Game *game;
  ScriptEngine *script_engine;
  ShaderCompiler *shader_compiler;
  ...
};

请注意,此处描述的 classes 未导出,但它们的声明必须可用于编译您的扩展。事实上,您甚至可以使用简化版本的头文件来编译您的扩展,但您必须确保每个 class 其所有虚方法都以相同的方式和相同的顺序声明。

一个虚方法调用只需要:

  1. 指向适当类型对象的有效指针(在运行时),
  2. 方法的正确签名(在编译时),
  3. 虚函数中方法的索引table(在编译时确定)。

不同于普通的函数调用,它不需要知道函数的地址,因此您不需要导出虚函数来调用它们。

因此,当您的扩展程序收到 MainProgramInfo 中的指针时,它可以开始调用这些对象的虚拟方法,而无需链接器直接使用它们。请注意,没有必要将每个对象都放入 MainProgramInfo:您可以只将顶级单例放在那里,并且您的扩展可以使用它的虚函数来获取指向其他 objects/singletons 的指针:

class Game {
  ...
  virtual Renderer *GetRenderer() const;
  virtual ScriptEngine *GetScriptEngine() const;
  virtual ShaderCompiler *GetShaderCompiler() const;
  ...
};

只有当您的编译器在主代码和扩展中以相同的方式生成虚方法的索引时,此方法才有效。这种方法的正确性是 not 由 C++ 标准保证的。它由 GCC ABI 保证(参见 this bug)。此外,它在 Doom 3 中被广泛使用以实现游戏模组,因此它也适用于 MSVC。

在一般情况下,我的建议是仍然将静态库转换为 Windows 平台的动态库,并导出所需的功能。也就是说,考虑到您所谈论的代码库的大小,我明白这在现阶段对您来说不可行。

如果我们反过来问"how can you get the functions (or objects) into the extensions?"而不是现在的"how do I get the functions (or objects) out of the static libraries?";我认为可能有一个有趣的选择。 类似于依赖注入,但用于模块

通过扩展 dll 中的适当函数 SetLogInstance(Log*)Log* 注入扩展 dll。反过来,每个扩展 dll 将维护指向单个 Log*; 的指针。并且每个人都会调用该记录器,就好像它是从导出函数中获得的一样。为了确保它在需要时尽早可用,可以在加载 dll 时将其添加到初始化代码中(通过。LoadLibrary)。

关于 std::set_new_handler and std::terminate_handler 等的标准库中已经使用了一些类似的模式。阿尔。它们是 "injected" 到运行时的函数,允许根据需要调用自定义处理程序。

还有一些额外的优势;

  1. 每个扩展都有它自己的 Log* 指针副本实例,因此如果需要,它可以被控制和隔离(用于测试或集中监控等)。
  2. 实例不再必须是关于进程的单例,仅是扩展,因此可以为主机提供更多控制来管理对象及其生命周期。
  3. 如果需要的话,将单例对象发展成某种仿函数会更容易(绑定技术可以根据需要使用仿函数)。