【问题标题】:What is the best practice for supporting different ABIs with a shared library release?使用共享库版本支持不同 ABI 的最佳实践是什么?
【发布时间】:2018-05-20 03:42:55
【问题描述】:

我相信 MS 在 MSVC 的每个主要版本中都会打破他们的 C++ ABI。我不确定他们的次要版本。也就是说,如果您向公众发布您的 dll 的二进制构建,您似乎需要发布多个构建 - 您希望支持的每个主要版本的 MSVC 一个构建。如果在您分发库后发布了新的 MSVC 次要版本,如果他们的应用是使用新版本的 MSVC 构建的,人们可以安全地使用您的库吗?

维基百科显示了一个 MSVC 版本表 https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#cite_note-43

从 _MSC_VER 看来,Visual Studio 2015 和 Visual Studio 2017 的编译器具有相同的主要版本 19。所以一个用 Visual Studio 2015 应该与使用 Visual Studio 2017 构建的应用程序一起使用,对吗?

【问题讨论】:

    标签: c++ visual-c++ dll shared-libraries abi


    【解决方案1】:

    编译器版本的主要变化是 C/C++ 运行时。因此,例如,通过您的 API 传递 streamFILE * 可能会导致麻烦,所以不要这样做。同样,不要在应用程序中free 分配在 DLL 中的内存,也不要在应用程序中删除在 DLL 中实例化的对象。反之亦然。

    其他可能改变的事情是对象中成员变量的顺序/对齐/总大小,不同版本的编译器使用的名称修改方案,或者 vtable(s) 中的布局对象(可能还有这些 vtable 与对象的位置,尤其是在使用多重继承或虚拟继承时)。

    虽然隧道尽头有一些光亮。如果您准备将要在 API 中导出的 C++ 类包装在本质上类似于 COM object 的东西中,那么您可以确保自己免受所有这些问题的影响。这是因为微软实际上已经承诺不会更改此类对象的 vtable 布局,因为如果他们这样做了,COM 就会中断。

    这确实对如何使用此类“类似 COM”的对象施加了一些限制,但我稍后会谈到。好消息是,您可以通过挑选最好的部分来避免实现一个成熟的 COM 对象所涉及的大部分繁重工作。例如,您可以执行以下操作。

    首先,一个通用的公共抽象类让我们为 std::unique_ptr 和 std::shared_ptr 提供自定义删除器:

    // Generic public class
    class GenericPublicClass
    {
    public:
        // pseudo-destructor
        virtual void Destroy () = 0;
    
    protected:
        // Protected, virtual destructor
        virtual ~GenericPublicClass () { }
    };
    
    // Custom deleter for std::unique_ptr and std::shared_ptr
    typedef void (* GPCDeleterFP) (GenericPublicClass *);
    
    void GPCDeleter (GenericPublicClass *obj)
    {
        obj->Destroy ();
    };
    

    现在是要由 DLL 导出的类 (MyPublicClass) 的公共头文件:

    // Demo public class - interface
    class MyPublicClass;
    
    extern "C" MyPublicClass *MyPublicClass_Create (int initial_x);
    
    class MyPublicClass : public GenericPublicClass
    {
    public:
        virtual int Get_x () = 0;
        // ...
    
    private:
        friend MyPublicClass *MyPublicClass_Create (int initial_x);
        friend class MyPublicClassImplementation;
    
        MyPublicClass () { }
        ~MyPublicClass () = 0 { }
    };
    

    接下来是MyPublicClass的实现,它是DLL私有的:

    #include "stdio.h"
    
    // Demo public class - implementation
    class MyPublicClassImplementation : public MyPublicClass
    {
    
    public:
    
    // Constructor
    MyPublicClassImplementation (int initial_x)
    {
        m_x = initial_x;
    }
    
    // Destructor
    ~MyPublicClassImplementation ()
    {
        printf ("Destructor called\n");
        // ...
    }
    
    // MyPublicClass pseudo-destructor
    void Destroy () override
    {
        delete this;
    }
    
    // MyPublicClass public methods
    int Get_x () override
    {
        return m_x;
    }
    
    // ...
    
    protected:
        // ...
    
    private:
        int m_x;
        // ...
    };
    

    最后,一个简单的测试程序:

    #include "stdio.h"
    #include <memory>
    
    int main ()
    {
        std::unique_ptr <MyPublicClass, GPCDeleterFP> p1 (MyPublicClass_Create (42), GPCDeleter);
        int x1 = p1->Get_x ();
        printf ("%d\n", x1);
        std::shared_ptr <MyPublicClass> p2 (MyPublicClass_Create (84), GPCDeleter);
        int x2= p2->Get_x ();
        printf ("%d\n", x2);
    }
    

    输出:

    42
    84
    Destructor called
    Destructor called
    

    注意事项:

    • MyPublicClass 的构造函数和析构函数被声明为private,因为它们对 DLL 的用户是禁止的。这可确保 new 和 delete 使用相同版本的运行时库(即 DLL 使用的那个)。
    • MyPublicClass 类的对象是通过工厂函数Create_MyPublicClass 创建的。声明为 extern "C" 以避免名称混淆问题。
    • 所有 MyPublicClass 的公共方法都被声明为virtual,再次避免名称混淆问题。 MyPublicClassImplementation 当然可以为所欲为。
    • MyPublicClass 没有数据成员。它可以有(如果它们被声明为私有),但它不需要。

    这样做的成本是:

    • 您可能需要进行大量包装。
    • 使用 DLL 的应用程序不能从 DLL 导出的类派生。
    • 进行所有方法调用virtual 以及将它们转发到底层实现(如果您最终这样做的话)将会有一些(轻微的)性能损失。对我来说,这将是我最不担心的事情。
    • 您不能将这些对象放在堆栈上。

    有利的一面:

    • 您可以在未来的版本中以几乎任何您喜欢的方式更改您的实现。
    • 如果编译器供应商声称支持 COM,您可以混合和匹配编译器供应商。您的 DLL 的用户可能会喜欢这样。

    只有您才能判断这种方法是否值得努力。 LMK。

    编辑:我在清除一些荆棘时考虑了这一点,并意识到它需要与 std::unique_ptr 和 std::shared_ptr 一起使用才能有用。也可以通过将公共类抽象化(如 COM 所做的那样)然后在 DLL 内的派生类中实现所有功能来改进它,因为这在实现类时为您提供了更大的灵活性。因此,我重新编写了上面的代码以包含这些更改,并更改了一些东西的名称以使意图更清晰。希望它可以帮助某人。

    【讨论】:

    • 感谢您的回复。即使我遵循了所有的指导方针,我仍然想在 lib 内部使用 C++。这意味着我的库仍然需要使用 MS C++ dll。如果 MS dll 的次要版本与我构建的版本不同,会有问题吗?
    • 我不相信,不。但是,如果您通过 API 传递 FILE * 或流之类的东西,那么所有的赌注都没有了,我只想说,不要冒险 malloc 和 free(或 new 和 delete)的问题是它们可能使用DLL 中的堆和调用应用程序中的另一个,如果是这样,结果是完全不可预测的。我什至在微软自己的代码中看到了这种情况,在他们的调试器支持的一个组件中。然后他们在他们所谓的“安全”无效参数处理程序中吞下了错误,所以他们从来没有看到它。事情莫名其妙地不起作用。哦,讽刺。
    • 建议:用你能找到的最旧的编译器构建一个测试DLL,然后用最新的编译器构建一个测试应用程序,看看它是否有效。应该很有趣。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-10
    • 1970-01-01
    • 2011-07-23
    • 1970-01-01
    • 2016-04-18
    • 1970-01-01
    相关资源
    最近更新 更多