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

Share singleton instance to shared lib's without exporting it

本文关键字:共享 单一 实例      更新时间:2023-10-16

我正在寻找一种共享单例实例的替代方法在主可执行文件和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.dll导出common.lib的单例。

game_inst.dllgame.lib导出单例。

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

是否有另一种跨平台兼容的可能性来共享单例到共享库,而不通过dlexport导出它?

如果您真的不希望在主代码中有任何dll或导出,您可以使用虚函数方法。

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

DLLEXPORT void InitExtension(MainProgramInfo info);

现在你必须把你的扩展想要访问的所有东西添加到传递的结构体中:

struct MainProgramInfo {
  Game *game;
  ScriptEngine *script_engine;
  ShaderCompiler *shader_compiler;
  ...
};
请注意,这里描述的类是未导出的,但它们的声明必须可用于编译您的扩展。事实上,你甚至可以使用头文件的简化版本来编译你的扩展,但是你必须确保每个类的所有虚方法都以相同的方式和顺序声明。

虚方法调用只需要:

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

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

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

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

只有当编译器在主代码和扩展中以相同的方式生成虚拟方法的索引时,这种方法才有效。这种方法的正确性是c++标准所保证的而不是。它是由GCC ABI保证的(参见这个bug)。此外,它在毁灭战士3中被广泛使用,以执行游戏模型,所以它也适用于MSVC。

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

如果我们反过来问"如何将函数(或对象)放入扩展中?"而不是当前的"如何从静态库中获得函数(或对象)?";我想可能会有一个有趣的选择。类似于依赖注入,但针对模块

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

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

还有一些额外的优点;

  1. 每个扩展都有自己的Log*指针的副本实例,因此如果需要,它可以被控制和隔离(用于测试或集中监控等)。
  2. 对于进程来说,实例不再是单例的,只有扩展,所以主机可以控制对象及其生命周期。
  3. 如果需要,可以更容易地将单例对象演化为某种类型的函子(可以使用绑定技术根据需要对函子进行按摩)。