C++ Logo

std-proposals

Advanced search

[std-proposals] Extensible vtables -- Ensuring ABI isn't broken

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Sat, 17 Feb 2024 17:47:47 +0000
Let's say version 1.5.6 of my shared library (a DLL file on MS-Windows, or
an SO file on Linux), contains the following class:

    class IDevice {
        virtual char const *GetName(void) = 0;
        virtual unsigned GetType(void) = 0;
     };

Objects of the type IDevice are only ever created or destroyed inside my
library using two exported functions 'CreateDevice' and 'FreeDevice'.

Let's say there's a program called Froggy that was built to link
dynamically with v1.5.6 of my library.

Two years go by, and let's say in version 2.3.7 of my library, I make an
extension to the IDevice class. I want the old Froggy binary to continue to
work properly when at runtime it finds v2.3.7 of my library instead of
v1.5.6. That is to say, I don't want an ABI break, and I want backward
compatibility. There's two ways of achieving this, the first technique is
as follows:

   class IDevice {
    public:
        virtual char const *GetName(void) = 0;
        virtual unsigned GetType(void) = 0;
    };

    class IDevice2 : public IDevice {
    public:
        virtual void *GetInputBuffer(void) = 0;
        virtual void *GetOutputBuffer(void) = 0;
    };

All of the preexisting functions that deal with an 'IDevice' can simply
dynamic_cast to an 'IDevice2' if they need to access the buffers.

The second technique is to simply extend the original class. The second
technique is as follows:

    class IDevice {
    public:
        virtual char const *GetName(void) = 0;
        virtual unsigned GetType(void) = 0;
        virtual void *GetInputBuffer(void) = 0;
        virtual void *GetOutputBuffer(void) = 0;
    };

This won't change the size, alignment or layout of an 'IDevice' object --
it will just extend the vtable by two pointers. This isn't an ABI break.

I've recently taken over coding a big SDK library, and I see both of the
above techniques used in the existing SDK code. I prefer the second
technique as it's simpler. I'm not even sure why the first technique was
ever used -- maybe because there are some cases where the second technique
results in an ABI break (e.g. multiple inheritance or virtual inheritance
or whatever I don't know).

Now of course the Standard doesn't mention vtables, nor does it allude to
something more generic such as a 'polymorphic facilitator', but still I
wonder could we possibly standardise the extending of vtables without
inducing an ABI break?

Maybe the C++26 standard could have a paragraph something like:

 - BEGIN TEXT
Given two distinct translation units, each containing a definition of a
class named S, where the class S is defined differently in the two
translation units, and if we refer to the definition in the first
translation unit as S1, and if we refer to the definition in the second
translation unit as S2, then any object of type S2 can be safely treated as
being an object of type S1, so long as S2 is an exact replica of S1 except
for one or more of the following additions:
(1) A static member function or a static member variable
(2) A non-static member variable, so long as it comes after all of the
member variables shared with S1
(3) A non-static non-virtual member function
(4) A non-static virtual member function, so long as it comes after all of
the non-static virtual member functions shared with S1.
(5) A constructor or an overloaded operator, so long as the class's type
traits remain intact (e.g. is_nothrow_assignable)
(6) A typedef or using declaration
(7) A friend
 - END TEXT

Furthermore, the standard header <type_traits> could provide the following
new helper class:

    template<class S1, class S2>
    class std::is_abi_compatible {
        inline static constexpr bool value = . . . ;
    };

So then you can ensure at compile-time that you haven't broken the ABI, as
follows:

    #include <type_traits> // is_abi_compatible

    class IDeviceOld {
    public:
        virtual char const *GetName(void) = 0;
        virtual unsigned GetType(void) = 0;
    };

    class IDeviceNew {
    public:
        virtual char const *GetName(void) = 0;
        virtual unsigned GetType(void) = 0;
        virtual void *GetInputBuffer(void) = 0;
        virtual void *GetOutputBuffer(void) = 0;
    };

    static_assert( std::is_abi_compatible_v<IDeviceOld,IDeviceNew> );

    typedef IDeviceNew IDevice;

Received on 2024-02-17 17:47:50