Be careful with vtable, or how to shoot yourself in the foot by updating the library

Imagine that you are developing an application using some kind of shared library. The library carefully follows the principles of backward compatibility, without changing the old interface and only adding a new one. It turns out that even with this in mind, updating the library without directly linking the application can lead to unexpected effects.





. clang 10.0.0 Arch Linux, , , gcc, MSVC .



. , , - . , ( , , ). , , - : -, , , . , . .



: shared-, , -, , header . , :



Shared-



  • CMakeLists.txt


cmake_minimum_required(VERSION 3.5)

project(shared_lib LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(SOURCES lib.cpp)
set(HEADERS lib.h)

add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS})


  • lib.h


#ifndef LIB_H
#define LIB_H

namespace my
{

class Interface
{
public:
    virtual ~Interface() = default;

    virtual void a() = 0;
    virtual void c() = 0;
};

class Implementation : public Interface
{
public:
    void a() override;
    void c() override;
};

} // namespace my

#endif // LIB_H


  • lib.cpp


#include "lib.h"

#include <iostream>

namespace my
{

void Implementation::a()
{
    std::cout << "Implementation::a()" << std::endl;
}

void Implementation::c()
{
    std::cout << "Implementation::c()" << std::endl;
}

} // namespace my


-



  • CMakeLists.txt


cmake_minimum_required(VERSION 3.5)

project(client LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(libshared_binary_dir "/path/to/libshared_lib.so")
set(libshared_source_dir "/path/to/shared_lib/source")

add_executable(${PROJECT_NAME} main.cpp)

add_library(shared_lib SHARED IMPORTED)
set_property(TARGET shared_lib PROPERTY IMPORTED_LOCATION ${libshared_binary_dir}/libshared_lib.so)

target_include_directories(${PROJECT_NAME} PRIVATE ${libshared_source_dir})
target_link_libraries(${PROJECT_NAME} PRIVATE shared_lib)


  • main.cpp


#include <lib.h>

#include <memory>

int main()
{
    std::unique_ptr<my::Interface> ptr = std::make_unique<my::Implementation>();
    ptr->a(); 
    ptr->c(); 
}


-, , :



Implementation::a()
Implementation::c()


, . :



  • lib.h


#ifndef LIB_H
#define LIB_H

namespace my
{

class Interface
{
public:
    virtual ~Interface() = default;

    virtual void a() = 0;
    virtual void b() = 0; // +
    virtual void c() = 0;
};

class Implementation : public Interface
{
public:
    void a() override;
    void b() override;    // +
    void c() override;
};

} // namespace my

#endif // LIB_H


  • lib.cpp


#include "lib.h"

#include <iostream>

namespace my
{

void Implementation::a()
{
    std::cout << "Implementation::a()" << std::endl;
}

void Implementation::b()                             // +
{                                                    // +
    std::cout << "Implementation::b()" << std::endl; // +
}                                                    // +

void Implementation::c()
{
    std::cout << "Implementation::c()" << std::endl;
}

} // namespace my


, . , -, , b(), , so- . , , , , , , . :



Implementation::a()
Implementation::b()


- : c(), b()! , . , .



, ? , Interface : a() c(). header-. , , , , ABI, ( , ). c() , vtable ( a()). ! b(), , c() , .



b() c() . b() , , ( ). , - . , b() , , , . , , - , : , , , , . , vtable.



, dlopen. :



  • CMakeLists.txt


cmake_minimum_required(VERSION 3.5)

project(dynamic_client LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(libshared_source_dir "SOURCE_DIR")

add_executable(${PROJECT_NAME} main.cpp)

target_include_directories(${PROJECT_NAME} PRIVATE ${libshared_source_dir})


  • main.cpp


#include <lib.h>

#include <dlfcn.h>

#include <cassert>

int main()
{
    void* handle = ::dlopen("/path/to/libshared_lib.so", RTLD_NOW);
    assert(handle != nullptr);
    using make_instance_t = my::Interface* ();
    make_instance_t* function = reinterpret_cast<make_instance_t*>(::dlsym(handle, "make_instance"));
    assert(function != nullptr);

    my::Interface* ptr = function();
    ptr->a(); // Implementation::a() with both old and new shared library
    ptr->c(); // Implementation::c() with old, Implementation::b() with new shared library

    delete ptr;
    ::dlclose(handle);
}


make_instance():



  • lib.h


#ifndef LIB_H
#define LIB_H

// ...

extern "C"
{
    my::Interface* make_instance();
}

#endif // LIB_H


  • lib.cpp


#include "lib.h"

// ...

my::Interface* make_instance()
{
    return new my::Implementation();
}

// ...


, , , , , : vtable , . , , , . , !



P.S. ilammy, aamonster demp:



  1. Adding new methods to the end does not solve the problem on all ABIs, so it’s better not to rely on that, or use it with caution.
  2. Interface versioning through inheritance solves the problem. dempthrew off a good article on this topic https://accu.org/index.php/journals/1718 .



All Articles