C++ ABI Breaking Change
Introduction
In computer software, an application binary interface (ABI) is an interface between two binary program modules. Often, one of these modules is a library or operating system facility, and the other is an application or another library that is being used by a user.
An ABI breaking change from the library module update will cause undefined behaviors from the dependent application or library.
In this blog post, I would like to discuss C++ ABI breaking change and how to solve the problems brought by the ABI breaking changes.
ABI Breaking Change
In C++, the ABI is almost equivalent as the vtable. We will go through an example to understand what exactly the consequence will be when there is an ABI breaking change and how to fix it.
Repository Layout
The ABI Breaking Change Demo is hosted on GitHub. The repository layout is as follows.
1 | . |
Library V1
This is the first version of the library. We will create Rectangle
instances in our application and compute their areas using the overriding Rectangle::area()
function.
1 |
|
1 |
|
We build the library as a shared library.
1 | # Build library V1. |
1 | Vtable for Shape |
Library V2
This is the second version of the library, which has an ABI change comparing to the first version. Notice that we got a new additional virtual function perimeter()
comparing to the first version, which leads to the ABI change. However, there is no API change and the method Rectangle::area()
was not changed, so the user would not have to change their application implementations when they started to use the second version of the library.
As we will later see, depending on the location of the virtual function perimeter()
, the ABI change may or may not be ABI breaking.
1 |
|
1 |
|
Again, we build the library as a shared library.
1 | # Build library V2. |
1 | Vtable for Shape |
Application
This is the application implementation from user that uses the library we created. Basically we will create a Rectangle
instance and compute the area using the overriding Rectangle::area
function.
1 |
|
We will separately build the application using the header files from different versions of the libraries.
1 | # Build application. |
1 | Vtable for Shape |
1 | Vtable for Shape |
Because we have two libraries, we will get two application object files, app_header_v1.o
and app_header_v2.o
.
To link the application object files to the libraries, each application object file has two options, library V1 and V2.
1 | # Link application to libraries. |
Taken together, we will get four application executable files, including app_header_v1_library_v1.app
, app_header_v2_library_v2.app
, app_header_v1_library_v2.app
, and app_header_v2_library_v1.app
.
After running the four application executable files, we found that only app_header_v1_library_v1.app
and app_header_v2_library_v2.app
behaved as expected whereas app_header_v1_library_v2.app
and app_header_v2_library_v1.app
did not have the expected behaviors.
1 | $ LD_LIBRARY_PATH=../library_v1 ./app_header_v1_library_v1.app |
1 | $ LD_LIBRARY_PATH=../library_v1 ./app_header_v2_library_v1.app |
1 | $ LD_LIBRARY_PATH=../library_v2 ./app_header_v2_library_v2.app |
1 | $ LD_LIBRARY_PATH=../library_v2 ./app_header_v1_library_v2.app |
This is because the library V2 introduced an ABI breaking change to the library V1. Therefore, the application object file compiled with the header file from the library V1 becomes incompatible with the library V2, and the application object file compiled with the header file from the library V2 becomes incompatible with the library V1.
What’s Happened?
The vtable for the Rectangle
from the library V1 and the application executable object file app_header_v1.o
compiled with the header file from the library V1 is as follows.
1 | Vtable for Rectangle |
The vtable for the Rectangle
from the library V2 and the application executable object file app_header_v2.o
compiled with the header file from the library V2 is as follows.
1 | Vtable for Rectangle |
By the implementation of my GCC compiler, after compiling the application executable file app_header_v1.o
with the library V1 header file, the rect->area()
from the app_header_v1.o
has to invoke the function (int (*)(...))Rectangle::area
, which is the 3rd entry ((& Rectangle::_ZTV9Rectangle) + 16)
in the vtable static array, via the vtable pointer. However, if the vtable static array, provided by the library (which is compiler implementation dependent), is from the library V2, the 3rd entry in the static array ((& Rectangle::_ZTV9Rectangle) + 16)
actually becomes the function (int (*)(...))Rectangle::perimeter
. That’s why, the rect->area()
from the app_header_v1_library_v2.app
computed the perimeter
instead of area
.
Similarly, after compiling the application executable file app_header_v2.o
with the library V2 header file, the rect->area()
from the app_header_v2.o
has to invoke the function (int (*)(...))Rectangle::area
, which is the 4th entry ((& Rectangle::_ZTV9Rectangle) + 24)
in the vtable static array, via the vtable pointer. However, if the vtable static array, provided by the library (which is compiler implementation dependent), is from the library V1, the 4th entry is actually out of the boundary of the vtable static library. That’s why, the rect->area()
from the app_header_v2_library_v1.app
results in a segmentation fault.
Fundamentally, this ABI breaking change is due to the location of the virtual function area
got shifted after a new virtual function perimeter
got added.
Avoid ABI Breaking Change
To avoid ABI breaking change, in our case, the library developer can insert the new virtual function perimeter
after the existing virtual function area
. Then the location of the virtual function area
will remain the same in the library V2. The app_header_v1_library_v2.app
and app_header_v2_library_v1.app
will also run fine as expected.
From the user’s perspective, if we can, whenever we get an upgraded version of the library, we should recompile the dependent application or library and always run app_header_v1_library_v1.app
or app_header_v2_library_v2.app
to ensure any ABI incompatibility has been eliminated.
Real-World Implications
Fundamentally, an ABI breaking change is due to the vtable used for the updated library becomes incompatible with the vtable used for the dependent library or application. This has a real-world implications.
Suppose there is a library, say the OpenCV library libopencv-dev
which depends on a third-party library libgtk-3-dev
. In the libopencv-dev0.1
release, it was built against libgtk-3-dev0.2
. When the users install libopencv-dev0.1
on Linux, they will also have to install libgtk-3-dev0.2
. This is fine.
Later, when the user upgraded their systems, libgtk-3-dev
got upgraded to libgtk-3-dev0.3
but libopencv-dev
remained to be libopencv-dev0.1
. If there is no ABI breaking change, the user can still use libopencv-dev0.1
normally. However, if there is an ABI breaking change, libopencv-dev0.1
cannot just be used normally anymore. The user would have to recompile libopencv-dev
using the upgraded library libgtk-3-dev0.3
and it’s sometimes difficult and time-consuming to do. In addition, as we have seen in our example, sometimes the ABI breaking change can even silently change the behavior of programs without crashing them. That’s why the software developers should not break ABI unless it’s absolutely inevitable to make the user’s life easier.
Conclusions
Ideally, the best practice for the user is always rebuilding the application whenever the library gets updated so that any ABI change will not affect the application behavior. The best practice for the library developer is avoiding ABI breaking changes as many as possible.
References
C++ ABI Breaking Change