我们何时打破二进制兼容性

Cer*_*ros 7 c++ dll virtual-functions abi

我的印象是每当你做其中一件时:

  • 添加新的公共虚拟方法 virtual void aMethod();
  • 添加新的公共非虚方法 void aMethod();
  • 从接口实现公共纯虚方法 virtual void aMethod override;

实际上是打破了二进制兼容性,这意味着如果一个项目构建在以前版本的DLL上,那么现在有了新方法可用,它将无法加载它.

根据我使用Visual Studio 2012测试的内容,这些都没有破坏任何东西.Dependency Walker报告没有错误,我的测试应用程序正在调用适当的方法.

DLL:

class EXPORT_LIB MyClass {
public:
  void saySomething();
}
Run Code Online (Sandbox Code Playgroud)

可执行文件:

int _tmain(int argc, _TCHAR* argv[])
{
  MyClass wTest;
  wTest.saySomething();
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我发现的唯一未定义的行为是,如果MyClass实现了一个纯虚拟接口,并且从我的可执行文件中,我调用了一个纯虚方法,然后在我的可执行文件使用的方法之前添加了一个新的纯虚方法.在这种情况下,Dependency Walker没有报告任何错误,但在运行时,它实际上调用了错误的方法.

class IMyInterface {
public:
  virtual void foo();
}
Run Code Online (Sandbox Code Playgroud)

在可执行文件中

IMyInterface* wTest = new MyClass();
wTest->foo();
Run Code Online (Sandbox Code Playgroud)

然后我更改界面而不重建我的可执行文件

class IMyInterface {
public:
  virtual void bar();
  virtual void foo();
}
Run Code Online (Sandbox Code Playgroud)

它现在正静静地呼唤bar()而不是foo().

做我所有的三个假设是否安全?

编辑:

这样做

class EXPORT_LIB MyClass {
public:
  virtual void saySomething();
}
Run Code Online (Sandbox Code Playgroud)

EXEC

MyClass wTest;
wTest.saySomething();
Run Code Online (Sandbox Code Playgroud)

然后使用以下方法重建DLL:

class EXPORT_LIB MyClass {
public:
  virtual void saySomething2();
  virtual void saySomething();
  virtual void saySomething3();
}
Run Code Online (Sandbox Code Playgroud)

正在调用合适的 saySomething()

Rob*_*olo 9

打破二进制兼容性并不总是导致DLL无法加载,在许多情况下,您最终会导致内存损坏,这可能会或可能不会立即显现.这在很大程度上取决于你所改变的细节以及现在如何以及现在在记忆中的布局.

DLL之间的二进制兼容性是一个复杂的主题.让我们先看看你的三个例子;

  • 添加新的公共虚拟方法 virtual void aMethod();

这几乎肯定会导致未定义的行为,它非常依赖于编译器,但是大多数编译器将使用某种形式的vtable来实现虚拟方法,因此添加新的将改变该表的布局.

  • 添加新的公共非虚方法 void aMethod();

这适用于全局函数或成员函数.成员函数本质上只是一个带有隐藏'this'参数的全局函数.它不会改变任何内存布局.

  • 从接口实现公共纯虚方法 virtual void aMethod override;

这不会导致任何未定义的行为,但正如您所发现的那样,它不会达到预期的效果.针对以前版本的库编译的代码不会知道这个函数已被覆盖,因此不会调用新的实现,它将继续调用旧的impl.根据您的使用情况,这可能是也可能不是问题,它不应该引起任何其他副作用.但是我认为你的里程可能会有所不同,具体取决于你使用的编译器.所以最好避免这种情况.

如果以任何方式更改导出函数的签名(包括更改参数和范围)或删除函数,将阻止加载DLL的内容是什么.那时动态链接器将无法找到它.这仅适用于正在使用相关函数的情况,因为链接器仅导入代码中引用的函数.

还有更多方法可以打破dll之间的二进制兼容性,这超出了本答案的范围.根据我的经验,他们通常遵循改变记忆中某些东西的大小或布局的主题.

编辑:我记得KDE上有一篇关于C++二进制兼容性的优秀文章,包括一个非常好的做法和注意事项列表以及解释和解决方法.


Edw*_*nge 3

C++没说。

Visual Studio 通常遵循 COM 规则,允许您将虚拟方法添加到最派生类的末尾,除非它们是重载。

任何非静态数据成员也会更改二进制布局。

非虚函数不影响二进制兼容性。

由于名称修改,模板会变得一团糟。

保持二进制兼容性的最佳选择是相当自由地使用 pimpl 惯用法和 nvi 惯用法。