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 , . , , , . , !
- 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.
- Interface versioning through inheritance solves the problem. dempthrew off a good article on this topic https://accu.org/index.php/journals/1718 .