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 , . , , , . , !
- 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.
- 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 .