共享库中的类和静态变量

31

我试图用类似这样的架构编写C++代码:

应用程序-->核心(.so) <--插件(.so)

适用于Linux、Mac和Windows。核心与应用程序隐式链接,而插件则通过dlopen/LoadLibrary方法与应用程序显式链接。我的问题是:

  • 核心中的静态变量在运行时被复制了--插件和应用程序拥有不同的副本。
  • 至少在Mac上,当一个插件返回一个指向应用程序的指针时,在应用程序中对该指针进行动态转换总是会导致NULL。

    请问有人能给我一些关于不同平台的解释和说明吗?我知道这样做可能看起来很懒,但我真的找不到这个问题的系统性答案。

    我在插件的entry_point.cpp中做了什么:

    #include "raw_space.hpp"
    
    #include <gamustard/gamustard.hpp>
    
    using namespace Gamustard;
    using namespace std;
    
    namespace
    {
      struct GAMUSTARD_PUBLIC_API RawSpacePlugin : public Plugin
      {
        RawSpacePlugin(void):identifier_("com.gamustard.engine.space.RawSpacePlugin")
        {
        }
    
        virtual string const& getIdentifier(void) const
        {
          return identifier_;
        }
    
        virtual SmartPtr<Object> createObject(std::string const& name) const
        {
          if(name == "RawSpace")
          {
            Object* obj = NEW_EX RawSpaceImp::RawSpace;
            Space* space = dynamic_cast<Space*>(obj);
            Log::instance().log(Log::LOG_DEBUG, "createObject: %x -> %x.", obj, space);
            return SmartPtr<Object>(obj);
          }
          return SmartPtr<Object>();
        }
    
      private:
        string identifier_;
      };
    
      SmartPtr<Plugin> __plugin__;
    }
    
    extern "C"
    {
      int GAMUSTARD_PUBLIC_API gamustardDLLStart(void) throw()
      {
        Log::instance().log(Log::LOG_DEBUG, "gamustardDLLStart");
        __plugin__.reset(NEW_EX RawSpacePlugin);
        PluginManager::instance().install(weaken(__plugin__));
        return 0;
      }
    
      int GAMUSTARD_PUBLIC_API gamustardDLLStop(void) throw()
      {
        PluginManager::instance().uninstall(weaken(__plugin__));
        __plugin__.reset();
        Log::instance().log(Log::LOG_DEBUG, "gamustardDLLStop");
        return 0;
      }
    }
    

  • 你能否提供一个简单的代码示例,以便我可以看出你的水平?例如:你是否理解__attribute__((visibility("default")))__declspec(dllexport)和使用extern - Travis Gockel
    我曾尝试使用__attribute__((visibility("default"))),但在一个小的测试项目中,我没有使用它,一切都正常运行... - abel
    没错:这是因为 ELF 自动将事物公开可见(因此使用了“默认”)-- 不同之处在于您的目标是 Windows。 - Travis Gockel
    1个回答

    44

    一些背景

    C++中的共享库非常困难,因为标准对此没有任何规定。这意味着每个平台都有不同的实现方式。如果我们只考虑Windows和某些*nix变体(任何ELF),则差异是微妙的。第一个区别是共享对象可见性。强烈建议您阅读该文章,以便您了解可见性属性的概述以及它们为您做了什么,这将帮助您避免链接器错误。

    无论如何,您最终会得到类似于以下内容的代码(适用于许多系统的编译):

    #if defined(_MSC_VER)
    #   define DLL_EXPORT __declspec(dllexport)
    #   define DLL_IMPORT __declspec(dllimport)
    #elif defined(__GNUC__)
    #   define DLL_EXPORT __attribute__((visibility("default")))
    #   define DLL_IMPORT
    #   if __GNUC__ > 4
    #       define DLL_LOCAL __attribute__((visibility("hidden")))
    #   else
    #       define DLL_LOCAL
    #   endif
    #else
    #   error("Don't know how to export shared object libraries")
    #endif
    

    接下来,您需要创建一些共享头文件(standard.h?),并在其中加入一个漂亮的小 #ifdef 代码段。
    #ifdef MY_LIBRARY_COMPILE
    #   define MY_LIBRARY_PUBLIC DLL_EXPORT
    #else
    #   define MY_LIBRARY_PUBLIC DLL_IMPORT
    #endif
    

    这可以让您像这样标记类、函数和其他内容:
    class MY_LIBRARY_PUBLIC MyClass
    {
        // ...
    }
    
    MY_LIBRARY_PUBLIC int32_t MyFunction();
    

    这将告诉构建系统在调用函数时在哪里查找它们。
    现在进入正题!如果您要跨库共享常量,那么实际上您不应该关心它们是否重复,因为常量的大小应该很小,并且重复允许进行很多优化(这是好的)。 但是,由于您似乎正在处理非常量,情况略有不同。 在C ++中可以创建跨库单例的模式有很多种,但我自然最喜欢我的方式。
    在某个头文件中,假设您想共享一个整数,则应在 myfuncts.h 中编写:
    #ifndef MY_FUNCTS_H__
    #define MY_FUNCTS_H__
    // include the standard header, which has the MY_LIBRARY_PUBLIC definition
    #include "standard.h"
    
    // Notice that it is a reference
    MY_LIBRARY_PUBLIC int& GetSingleInt();
    
    #endif//MY_FUNCTS_H__
    

    然后,在myfuncts.cpp文件中,您将会有以下内容:

    #include "myfuncs.h"
    
    int& GetSingleInt()
    {
        // keep the actual value as static to this function
        static int s_value(0);
        // but return a reference so that everybody can use it
        return s_value;
    }
    

    处理模板

    C++拥有超强大的模板,这很棒。然而,在跨库使用模板时可能会非常痛苦。当编译器遇到一个模板时,它意味着“填入任何你想要的东西来使其工作正常”,如果你只有一个最终目标的话,这是完全可以接受的。然而,在使用多个动态共享对象时,问题就会出现了,因为它们理论上可以使用不同版本的不同编译器进行编译,所有这些编译器都认为自己的填空方法是正确的(我们凭什么争论 - 标准中没有定义)。这意味着模板可能会带来巨大的麻烦,但你确实有一些选择。

    不允许使用不同的编译器

    选择一个编译器(每个操作系统一个)并坚持使用它。只支持该编译器,并要求所有库使用相同的编译器进行编译。这实际上是一个非常好的解决方案(完全有效)。

    不要在导出的函数/类中使用模板

    只有在内部使用模板函数和类时才使用模板。这确实节省了很多麻烦,但总的来说非常受限制。个人而言,我喜欢使用模板。

    强制导出模板,并希望一切顺利

    这个方法效果惊人(特别是与不允许使用不同的编译器相配合时)。

    将以下内容添加到standard.h中:

    #ifdef MY_LIBRARY_COMPILE
    #define MY_LIBRARY_EXTERN
    #else
    #define MY_LIBRARY_EXTERN extern
    #endif
    

    并且在某些消费类的定义中(在您声明类本身之前):

    //    force exporting of templates
    MY_LIBRARY_EXTERN template class MY_LIBRARY_PUBLIC std::allocator<int>;
    MY_LIBRARY_EXTERN template class MY_LIBRARY_PUBLIC std::vector<int, std::allocator<int> >;
    
    class MY_LIBRARY_PUBLIC MyObject
    {
    private:
        std::vector<int> m_vector;
    };
    

    这几乎完美无缺...编译器不会对你有任何反应,生活将会很美好,除非你的编译器开始改变填充模板的方式并重新编译其中一个库而不是另一个库(即使在这种情况下,它仍然可能有效...有时候)。

    请记住,如果你正在使用像部分模板特化(或类型特征或任何更高级的模板元编程东西)之类的东西,所有的生产者和消费者都会看到相同的模板特化。也就是说,如果你有一个针对int等的vector<T>的专门实现,如果生产者看到了int的实现,但消费者没有看到,消费者将会愉快地创建错误类型的vector<T>,这将导致各种非常混乱的错误。因此要非常小心。


    如果您进行静态链接,那么您将不再拥有插件架构。静态链接只是使您的最终可执行文件将代码从静态库中合并到自身中。 - Travis Gockel
    嗯,我的意思是插件仍然是动态链接的,但核心是静态链接到应用程序中。 - abel
    我仍然在使用显式模板实例化遇到问题。我应该将你的代码放在.h文件还是.cpp文件中?我尝试将一些显式模板实例化代码(如你所建议的)放在一个被保护的.h文件中,该文件被多个其他.h/.cpp引用,但在使用gcc 4.0.1的mac上出现了链接错误。现在,我用“extern”声明所有实例化代码,然后在cpp中进行显式实例化。这样编译时没有出现错误。问题:
    • 我做错了什么吗?
    • 我目前的方法会按预期工作吗?
    - abel
    另外,如果我有一些静态变量(好吧,我会将变量放在匿名命名空间中)隐藏在cpp文件中,它们是否仍会在库之间重复? - abel
    在编程中,声明extern是首选的方法,因此它应该按预期工作。至于静态变量:如果它们最终被静态链接,那么它们将被重复。如果没有,那么就没问题了。 - Travis Gockel
    显示剩余10条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接