Pure virtual features and binary compatibility

Now I know that, as a rule, it’s bad to add new virtual functions to non-classical classes, since it violates binary compatibility for any derived classes that have not been recompiled. However, I have a slightly different situation:

I have an interface class and an 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 can basically be written as:

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

In other words, the application does not have classes that are derived from Interface , it just uses it.

Now let's say I want to overload Foo( uint16_t arg ) with Foo( uint32_t arg ) , I'm safe:

  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 recompiling the application?

If so, are there any unusual reservations that I need to know about? If not, do I have any other options besides taking a hit and updated version of the library, thereby violating backward compatibility?

+6
source share
4 answers

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

Something else to consider in this case is that you are not just proposing a change to the ABI interrupt, but also an API that is very difficult to detect at compile time. If these were not virtual functions, and compatibility with ABI was not a problem, after your change, something like:

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

will eventually call your new function, but only if this code is recompiled, which will make debugging very difficult.

+5
source

The simple answer is no. Every time you change a class, you can generally lose binary compatibility. Adding a non-virtual function or static elements is usually safe in practice, although it is still formally undefined behavior, but that's about it. Everything else is likely to break binary compatibility.

+5
source

This was very surprising for me when I was in a similar situation, and I found that MSVC was undoing the order of overloaded functions. According to your example, MSVC will build v_table (in binary format) as follows:

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

If we expand your example a bit, for example:

 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 build 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 don't reorder, but

  • They do not do this in the versions I tested.
  • If your library compiled by GCC (for example) and the application is compiled by MSVC, this will be an epic error

In the end ... Never rely on binary compatibility. Any change to the class should result in recompilation of all the code using it.

+2
source

You are trying to describe the popular method "Make classes not deducible" to preserve binary compatibility, which is used, for example, in the Symbian C ++ API (look for the NewL factory method)

  • Provide a factory function;
  • Declare the C ++ constructor as private (and not exported as non-built-in, and the class should not have classes or friend functions), this makes the class not deducible, and then you can:

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

This method works only for the GCC compiler , since it preserves the order of the sources of virtual functions at the binary level.

Explanation

Virtual functions are called by the offset in the v-table of the object, not by the changed name. If you can get a pointer to an object only by calling the static factory method and preserving the offset of all virtual functions (preserving the original order, adding new methods to the end), then it will be backward compatible with binary code.

Compatibility will be broken if your class has an open constructor (built-in or not built-in):

  • inline : applications will copy the old v-table and the old class memory layout, which will differ from those used in the new library; if you call any exported method or pass an object as an argument for such a method, this may lead to memory corruption due to a segmentation error;

  • non-inline : the situation is better because you can change the v-table by adding new virtual methods to the end of the sheet class declaration, because the linker will move the v-table location of the derived classes on the client side if you download the new version of the library; but you still can’t resize the class (for example, add new fields), since the size is hard-coded at compile time, and calling the new version constructor can break memory of neighboring objects in the client stack or heap.

Instruments

Try using the abi-compliance-checker tool to check the binary compatibility of your class library versions under Linux.

+2
source

All Articles