Tenha cuidado com vtable, ou como atirar no próprio pé atualizando a biblioteca

Imagine que você está desenvolvendo um aplicativo que usa algum tipo de biblioteca compartilhada. A biblioteca segue cuidadosamente os princípios de compatibilidade com versões anteriores, sem alterar a interface antiga e apenas adicionar uma nova. Acontece que, mesmo com isso em mente, atualizar a biblioteca sem vincular diretamente o aplicativo pode levar a efeitos inesperados.





. 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. Adicionar novos métodos ao final não resolve o problema em todos os ABIs, então é melhor não confiar nisso ou usá-lo com cautela.
  2. O controle de versão da interface por meio de herança resolve o problema. demplançou um bom artigo sobre este tópico https://accu.org/index.php/journals/1718 .



All Articles