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

时间:2016-05-10 21:43:27

标签: 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();
}

可执行文件:

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

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

class IMyInterface {
public:
  virtual void foo();
}

在可执行文件中

IMyInterface* wTest = new MyClass();
wTest->foo();

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

class IMyInterface {
public:
  virtual void bar();
  virtual void foo();
}

现在正在静静地拨打bar()而不是foo()

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

修改

这样做

class EXPORT_LIB MyClass {
public:
  virtual void saySomething();
}

Exec的

MyClass wTest;
wTest.saySomething();

然后用这个重建DLL:

class EXPORT_LIB MyClass {
public:
  virtual void saySomething2();
  virtual void saySomething();
  virtual void saySomething3();
}

正在调用相应的saySomething()

2 个答案:

答案 0 :(得分:8)

打破二进制兼容性并不总是导致DLL无法加载,在许多情况下,您最终会导致内存损坏,这可能会或可能不会立即显现。这很大程度上取决于你所改变的具体情况以及事物的内容和现状。

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

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

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

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

这适用于全局函数或成员函数。成员函数本质上只是一个全局函数,具有隐藏的' this'论点。它不会改变任何东西的内存布局。

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

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

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

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

编辑:我刚才记得有一篇关于C ++中二进制兼容性的KDE Wiki的优秀文章,包括一个非常好的列表和不做的事情。有解释和解决方法。

答案 1 :(得分:3)

C ++没有说。

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

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

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

由于名称损坏,模板变得非常混乱。

保留二进制兼容性的最佳选择是同时使用pimpl习惯用法和nvi习语。