首页 > 解决方案 > 使用共享库版本支持不同 ABI 的最佳实践是什么?

问题描述

我相信 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 构建的 DLL 应该与使用 Visual Studio 2017 构建的应用程序一起使用,对吗?

标签: c++visual-c++dllshared-librariesabi

解决方案


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

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

不过,隧道尽头有一些光亮。如果您准备将要通过 API 导出的 C++ 类封装在本质上类似于COM 对象的东西中,那么您可以确保自己免受所有这些问题的影响。这是因为微软实际上已经承诺不会更改此类对象的 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 ();
};

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

// 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 内的派生类中实现所有功能来改进它,因为这在实现类时为您提供了更大的灵活性。因此,我重新编写了上面的代码以包含这些更改,并更改了一些东西的名称以使意图更清晰。希望它可以帮助某人。


推荐阅读