Soyez prudent avec vtable, ou comment vous tirer une balle dans le pied en mettant à jour la bibliothèque

Imaginez que vous développez une application qui utilise une sorte de bibliothèque partagée. La bibliothèque suit attentivement les principes de compatibilité descendante, sans changer l'ancienne interface et en n'en ajouter qu'une nouvelle. Il s'avère que même dans cet esprit, la mise à jour de la bibliothèque sans lier directement l'application peut entraîner des effets inattendus.





. 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. L'ajout de nouvelles méthodes à la fin ne résout pas le problème sur toutes les ABI, il vaut donc mieux ne pas s'y fier ou l'utiliser avec prudence.
  2. La gestion des versions d'interface par héritage résout le problème. dempa lancé un bon article sur ce sujet https://accu.org/index.php/journals/1718 .



All Articles