【问题标题】:Share singleton instance to shared lib's without exporting it将单例实例共享到共享库而不导出它
【发布时间】:2015-09-10 13:47:11
【问题描述】:

我正在寻找一种共享单例实例的替代方法 在主可执行文件和 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 二进制兼容

我不能只将静态库转换为动态库,因为它会破坏很多并且太费力了。 我目前的做法是转静态单例吸气剂:

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

变成类似:

class Log { };

__declspec(dllexport) Log* instance();

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

common_inst.dllcommon.lib 导出单例。

game_inst.dllgame.lib 导出单例。

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

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

【问题讨论】:

    标签: c++ dll shared-libraries


    【解决方案1】:

    如果您真的不想在主代码中包含任何 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 保证(参见this bug)。此外,它在 Doom 3 中被广泛用于实现游戏模组,因此它也适用于 MSVC。

    【讨论】:

      【解决方案2】:

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

      如果我们反过来问“如何将函数(或对象)放入扩展中?”而不是当前的“我如何将函数(或对象)取出静态库?”;我认为可能有一个有趣的选择。 类似于依赖注入,但用于模块

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

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

      还有一些额外的优势;

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

      【讨论】:

      • 我喜欢你的方法,因为它便携且易于适应。我也更喜欢完整的动态链接,但是我正在谈论的代码库在静态库中有 800000 个 LoC,这意味着几乎不可能修复如果我将其更改为动态链接会发生的所有链接错误,请参阅 github.com/TrinityCore/TrinityCore 了解详细信息。
      • @DenisBlank。我同意,在这种规模下,从静态到动态的转变将是一项巨大的努力。在这种规模下,我希望注入扩展并尽可能多地限制扩展对核心的依赖关系并降低它们的耦合度。
      • 有一点看不懂。如果在主代码中定义了Log 类,则不能从扩展DLL 调用它的方法,除非该方法被导出。或者是内联的,或者是虚拟的 =)
      • 这里提到的所有方法都是如此。 OP 没有定义 Log 类的外观。与如何获得对 Log 对象的访问无关,它的成员也需要是可访问的。为此,我赞成使用函数对象进行注入(如第 3 点所述)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-06
      • 1970-01-01
      • 2016-05-07
      • 1970-01-01
      相关资源
      最近更新 更多