Pure virtual functions and binary compatibility

冷暖自知 提交于 2019-12-21 13:04:32

问题


Now, I know it is generally bad to add new virtual functions to non-leaf classes as it breaks binary compatibility for any derived classes which haven't been recompiled. However, I have a slightly different situation:

I have an interface class and implementation class compiled into a shared library, for example:

class Interface {
    public:
        static Interface* giveMeImplPtr();
        ...
        virtual void Foo( uint16_t arg ) = 0;
        ...
}

class Impl {
    public:
        ...
        void Foo( uint16_t arg );
        ....
}

My main application uses this shared library, and could basically be written as:

Interface* foo = Implementation::giveMeImplPtr();
foo->Foo( 0xff );

In other words, the application doesn't have any classes which derive from Interface, it merely uses it.

Now, say I want to overload Foo( uint16_t arg ) with Foo( uint32_t arg ), am I safe to do:

 class Interface {
    public:
        static Interface* giveMeImplPtr();
        ...
        virtual void Foo( uint16_t arg ) = 0;
        virtual void Foo( uint32_t arg ) = 0;
        ...
}

and recompile my shared library without having to recompile the application?

If so, are there any unusual caveats I need to be aware of? If not, do I have any other options other than to take the hit and up-version the library, thus breaking backwards compatibility?


回答1:


ABI basically depends on the size and shape of the object, including the vtable. Adding a virtual function will definitely change the vtable, and how it changes depends on the compiler.

Something else to consider in this case is that you're not just proposing an ABI breaking change, but an API breaking one that is very difficult to detect at compile time. If these were not virtual functions and ABI compatibility wasn't an issue, after your change, something like:

void f(Interface * i) {
  i->Foo(1)
}

will quietly end up calling your new function, but only if that code is recompiled, which can make debugging very difficult.




回答2:


The simple answer is: no. Anytime you change the class definition at all, you potentially lose binary compatibility. Adding a non-virtual function or static members is usually safe in practice, although still formally undefined behavior, but that's about it. Anything else will probably break binary compatibility.




回答3:


You are trying to describe the popular "Make classes non-derivable" technique for preserving binary compatibility which is used, for example, in the Symbian C++ APIs (look for NewL factory method):

  1. Provide a factory function;
  2. Declare the C++ constructor private (and non-exported non-inline, and the class should not have friend classes or functions), this makes the class non-derivable and then you can:

    • Add virtual functions at end of the class declaration,
    • Add data members and change the size of the class.

This technique works only for GCC compiler because it saves the source order of virtual functions at the binary level.

Explanation

Virtual functions are invoked by the offset in the v-table of an object, not by the mangled name. If you can get the object pointer only by calling a static factory method and preserved the offset of all virtual functions (by saving the source order, adding new methods at end) then this will be backward binary compatible.

The compatibility will be broken if your class has a public constructor (inline or non-inline):

  • inline: applications will copy an old v-table and old memory layout of the class which will be different from the ones used in the new library; if you call any exported method or pass an object as the argument to such method then this may cause a memory corruption of segmentation fault;

  • non-inline: the situation is better, because you can change v-table by adding new virtual methods to the end of leaf class declaration, because the linker will relocate the v-table layout of derived classes at the client side if you'll load the new library version; but you still cannot change the size of the class (i.e. adding new fields), because the size is hard-coded at the compile time and calling a new-version constructor may break the memory of neighboring objects on the client stack or heap.

Tools

Try to use the abi-compliance-checker tool to check backward binary compatibility of your class library versions on Linux.




回答4:


It was very amazing for me when I was in similar situation and I found, that MSVC reverses the order of overloaded functions. According to your example, MSVC will construct the v_table (in binary) like this:

virtual void Foo( uint32_t arg ) = 0;
virtual void Foo( uint16_t arg ) = 0;

If we'll widen a little your example, like this:

class Interface {
    virtual void first() = 0;
    virtual void Foo( uint16_t arg ) = 0;
    virtual void Foo( uint32_t arg ) = 0;
    virtual void Foo( std::string arg ) = 0;
    virtual void final() = 0;
}

MSVC will construct the following v_table:

    virtual void first() = 0;
    virtual void Foo( std::string arg ) = 0;
    virtual void Foo( uint32_t arg ) = 0;
    virtual void Foo( uint16_t arg ) = 0;
    virtual void final() = 0;

Borland builder and GCC do not change the order, but

  1. They do not this in that versions, that I tested
  2. If your library compiled by GCC (for example), and app will be compiled by MSVC, it would be an epic fail

An the end... Never rely to binary compatibility. Any change of class must cause recompile of all the code, using it.



来源:https://stackoverflow.com/questions/14875052/pure-virtual-functions-and-binary-compatibility

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!