使用共享库版本支持不同 ABI 的最佳实践是什么?
What is the best practice for supporting different ABIs with a shared library release?
我相信 MS 在 MSVC 的每个主要版本中都打破了他们的 C++ ABI。我不确定他们的次要版本。也就是说,如果您将 dll 的二进制版本发布到 public,您将需要发布多个版本 - 一个版本对应于您希望支持的每个 MSVC 主要版本。如果在您分发库后发布了新的 MSVC 次要版本,如果人们的应用是使用新版本的 MSVC 构建的,那么他们可以安全地使用您的库吗?
维基百科显示了 table 个 MSVC 版本
https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#cite_note-43
从 _MSC_VER 看来,Visual Studio 2015 和 Visual Studio 2017 具有相同的编译器主要版本 19。所以构建了一个 DLL
Visual Studio 2015 应该可以与使用 Visual Studio 2017 构建的应用程序一起使用,对吗?
不同编译器版本的主要变化是 C/C++ 运行时。因此,例如,在 API 中传递 stream
或 FILE *
可能会造成麻烦,所以不要那样做。同样,不要 free
应用程序中在 DLL 中分配的内存,也不要在应用程序中删除在 DLL 中实例化的对象。反之亦然。
其他可能改变的是对象中成员变量的顺序/对齐方式/总大小、不同版本的编译器使用的名称修改方案,或者 vtable(s) 的布局在对象中(可能还有那些 vtables with the object 的位置,尤其是在使用多重或虚拟继承时)。
虽然隧道尽头有一些光。如果您准备将要在 API 中导出的 C++ class 包装在本质上看起来像 COM object 的东西中,那么您可以确保自己免受所有这些问题的影响。这是因为 Microsoft 已经有效地承诺不会更改此类对象的 vtable 布局,因为如果他们这样做,COM 就会崩溃。
这确实对如何使用此类 'COM-like' 对象施加了一些限制,但我稍后会谈到这一点。好消息是,您可以通过挑选最好的部分来避免实现成熟的 COM 对象所涉及的大部分繁重工作。例如,您可以执行以下操作。
首先,一个通用的、public、抽象的 class 允许我们为 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 导出的 class (MyPublicClass
) 的 public 头文件:
// 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 使用的版本)。
- class
MyPublicClass
的对象是通过工厂函数 Create_MyPublicClass
创建的。这是声明 extern "C"
以避免名称修改问题。
- All public
MyPublicClass
的方法被声明为 virtual
,同样是为了避免名称混淆问题。 MyPublicClassImplementation
当然可以随心所欲
MyPublicClass
没有数据成员。它可以有(如果它们被声明为私有)但它不需要。
这样做的成本是:
- 您可能需要做很多包装。
- 使用 DLL 的应用程序无法从 DLL 导出的 classes 派生。
- 进行所有方法调用
virtual
以及将它们转发到底层实现(如果这是您最终要做的),将会有一些(轻微的)性能损失。对我来说,这是我最不担心的事情。
- 您不能将这些对象放入堆栈。
好的方面:
- 您可以在未来的版本中以几乎任何您喜欢的方式更改您的实现。
- 如果编译器供应商声称支持 COM,您可能可以混合搭配这些编译器供应商。您的 DLL 的用户可能会喜欢这个。
只有您才能判断这种方法是否值得付出努力。 LMK.
编辑:我在清理一些杂草时仔细考虑了这一点,并意识到它需要与 std::unique_ptr 和 std::shared_ptr 一起使用才能发挥作用。还可以通过使 public class 抽象化(如 COM 所做的那样)然后在 DLL 中的派生 class 中实现所有功能来改进它,因为这在实现时为您提供了更大的灵活性class。因此,我重新编写了上面的代码以包含这些更改,并更改了一些内容的名称以使意图更清楚。希望对大家有帮助。
我相信 MS 在 MSVC 的每个主要版本中都打破了他们的 C++ ABI。我不确定他们的次要版本。也就是说,如果您将 dll 的二进制版本发布到 public,您将需要发布多个版本 - 一个版本对应于您希望支持的每个 MSVC 主要版本。如果在您分发库后发布了新的 MSVC 次要版本,如果人们的应用是使用新版本的 MSVC 构建的,那么他们可以安全地使用您的库吗?
维基百科显示了 table 个 MSVC 版本 https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#cite_note-43
从 _MSC_VER 看来,Visual Studio 2015 和 Visual Studio 2017 具有相同的编译器主要版本 19。所以构建了一个 DLL Visual Studio 2015 应该可以与使用 Visual Studio 2017 构建的应用程序一起使用,对吗?
不同编译器版本的主要变化是 C/C++ 运行时。因此,例如,在 API 中传递 stream
或 FILE *
可能会造成麻烦,所以不要那样做。同样,不要 free
应用程序中在 DLL 中分配的内存,也不要在应用程序中删除在 DLL 中实例化的对象。反之亦然。
其他可能改变的是对象中成员变量的顺序/对齐方式/总大小、不同版本的编译器使用的名称修改方案,或者 vtable(s) 的布局在对象中(可能还有那些 vtables with the object 的位置,尤其是在使用多重或虚拟继承时)。
虽然隧道尽头有一些光。如果您准备将要在 API 中导出的 C++ class 包装在本质上看起来像 COM object 的东西中,那么您可以确保自己免受所有这些问题的影响。这是因为 Microsoft 已经有效地承诺不会更改此类对象的 vtable 布局,因为如果他们这样做,COM 就会崩溃。
这确实对如何使用此类 'COM-like' 对象施加了一些限制,但我稍后会谈到这一点。好消息是,您可以通过挑选最好的部分来避免实现成熟的 COM 对象所涉及的大部分繁重工作。例如,您可以执行以下操作。
首先,一个通用的、public、抽象的 class 允许我们为 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 导出的 class (MyPublicClass
) 的 public 头文件:
// 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 使用的版本)。- class
MyPublicClass
的对象是通过工厂函数Create_MyPublicClass
创建的。这是声明extern "C"
以避免名称修改问题。 - All public
MyPublicClass
的方法被声明为virtual
,同样是为了避免名称混淆问题。MyPublicClassImplementation
当然可以随心所欲 MyPublicClass
没有数据成员。它可以有(如果它们被声明为私有)但它不需要。
这样做的成本是:
- 您可能需要做很多包装。
- 使用 DLL 的应用程序无法从 DLL 导出的 classes 派生。
- 进行所有方法调用
virtual
以及将它们转发到底层实现(如果这是您最终要做的),将会有一些(轻微的)性能损失。对我来说,这是我最不担心的事情。 - 您不能将这些对象放入堆栈。
好的方面:
- 您可以在未来的版本中以几乎任何您喜欢的方式更改您的实现。
- 如果编译器供应商声称支持 COM,您可能可以混合搭配这些编译器供应商。您的 DLL 的用户可能会喜欢这个。
只有您才能判断这种方法是否值得付出努力。 LMK.
编辑:我在清理一些杂草时仔细考虑了这一点,并意识到它需要与 std::unique_ptr 和 std::shared_ptr 一起使用才能发挥作用。还可以通过使 public class 抽象化(如 COM 所做的那样)然后在 DLL 中的派生 class 中实现所有功能来改进它,因为这在实现时为您提供了更大的灵活性class。因此,我重新编写了上面的代码以包含这些更改,并更改了一些内容的名称以使意图更清楚。希望对大家有帮助。