Линкуем C++ либы без конфликтов
Порой в одном приложении нужны две версии одной и той же библиотеки - и это приводит к неожиданным конфликтам символов при линковке. Расскажу, как с этим справиться.
Постановка задачи
Основное приложение main_app
линкуется со статической библиотекой libstatic_v2.a
. Для новой фичи подключить стороннюю библиотеку libdynamic.so
, которая зависима от libshared_v1.so
- по сути того же кода из libstatic_v2.a
, но старой версии.
Понизить версию libstatic_v2.a
нельзя - проект может сломаться, а чужую библиотеку переделывать никто не будет. При этом в интерфейсах libdynamic.so
ничего из libshared_v1.so
нет, так что две библиотеки, в теории, могут жить рядом.
Разберемся, что делать, если один и тот же символ реализован дважды, и линковщик “не понимает”, какую версию выбрать.
Демонстрационный hello_world
Демо лежит на GitHub.
Код заголовочных файлов libstatic_v2.a
и libshared_v1.so
идентичен, но у разных версий реализация отличается:
// header_v1.h header_v2.h
void print_version();
// main_v1.cpp
void print_version() {
std::cout << "1" << std::endl;
}
// main_v2.cpp
void print_version() {
std::cout << "2" << std::endl;
}
В libdynamic.so
реализована обертка над print_version
из libshared_v1.so
:
#include "header_v1.h"
extern void print_version();
void call_print_from_v1() {
std::cout << "Call to v1: ";
print_version();
}
В main_app
находятся два вызова из статической и динамической библиотек:
#include "header_v2.h"
extern void print_version(); // from static lib_v2
extern void call_print_from_v1(); // from dynamic libdyn
int main() {
std::cout << "Static lib_v2 prints: ";
print_version();
std::cout << "Dynamic libdyn->lib_v1 prints: ";
call_print_from_v1();
}
Собираем все через Cmake:
add_library(lib_v1_shared SHARED main_v1.cpp)
set_target_properties(lib_v1_shared PROPERTIES OUTPUT_NAME shared_v1)
add_library(lib_v2_static STATIC main_v2.cpp)
set_target_properties(lib_v2_static PROPERTIES OUTPUT_NAME static_v2)
add_library(lib_dyn SHARED lib_dyn.cpp)
set_target_properties(lib_dyn PROPERTIES OUTPUT_NAME dynamic)
target_link_libraries(lib_dyn PRIVATE lib_v1_shared)
add_executable(main_app main.cpp)
target_link_libraries(main_app PRIVATE lib_v2_static lib_dyn)
Конфликт
После запуска ожидаем увидеть корректную работу:
Static lib_v2 prints: 2
Dynamic libdyn->lib_v1 prints: Call to v1: 2 // <----- ожидали 1
Линковщик берёт первый глобальный символ print_version
из статической библиотеки и отдаёт его libdynamic.so
.
Скрываем символы
Решение заключается в том, чтобы символы из статической библиотеки убрать из глобальной области видимости. Для этого есть два способа.
Способ первый: exclude-libs
Указываем во время компановки проекта main_app
, что нужно скрыть символы из зависимостей:
# всех библиотек
target_link_options(main_app PRIVATE "-Wl,--exclude-libs,ALL")
# конкретной библиотеки
target_link_options(main_app PRIVATE "-Wl,--exclude-libs,libstatic_v2.a")
Способ работает только в GNU.
Способ второй: visibility=hidden
Этот способ универсален, указывается во время компиляции статической библиотеки:
target_compile_options(lib_v2_static PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden)
Проверка
После реализации одного из способов выше обе версии print_version
резолвятся корректно:
Static lib_v2 prints: 2
Dynamic libdyn->lib_v1 prints: Call to v1: 1
Убедимся в корректности исправлений с помощью readelf
, nm
и LD_DEBUG
.
Посмотрим, что libdynamic.so
будет пытаться резолвить символ:
readelf --dyn-syms lib/libdynamic.so | grep print_version
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _Z13print_versionv
# или
nm lib/libdynamic.so | grep print_version
U _Z13print_versionv
Флаг UND (U)
говорит о том, что реализация этой функции будет взята из внешнего источника.
До добавления исправляющих флагов сборки/компоновки символ print_version
должен быть глобальным:
readelf --dyn-syms main_app | grep print_version
17: 0000000000001300 40 FUNC GLOBAL DEFAULT 15 _Z13print_versionv
# или
nm main_app | grep print_version
0000000000001300 T _Z13print_versionv
LD_DEBUG=bindings main_app 2>&1 | grep print_version
944267: binding file libdynamic.so [0] to main_app [0]: normal symbol `_Z13print_versionv
В выводе первых двух команд видим флаг GLOBAL (T)
, значащий, что символ глобальный. Третья команда показывает, что символ print_version
для libdynamic.so
берется из main_app
.
Посмотрим на вывод аналогичных команд для версии проекта без конфликта:
readelf --dyn-syms main_app | grep print_version
# пусто
nm main_app | grep print_version
00000000000012a0 t _Z13print_versionv
LD_DEBUG=bindings main_app 2>&1 | grep print_version
945221: binding file libdynamic.so [0] to libshared_v1.so [0]: normal symbol `_Z13print_versionv'
Теперь флаг t
в nm
показывает, что символ локальный. Символ print_version
для libdynamic.so
берется из libshared_v1.so
.
Символы шаблонов
С обычными функциями проблема решена. В C++ в интерфейсах библиотек могут быть шаблоны, которые добавляют новый уровень боли.
Шаблоны в hello_world
Добавим шаблонную функцию с одинаковой сигнатурой, но разной реализацией в заголовочные файлы libstatic_v2.a
и libshared_v1.so
. Добавим обертку в libdynamic.so
и соответствующие вызовы в main_app
:
// header_v1.h
template<typename T> T multiply(T a, T b) { return a * b; }
// header_v2.h
template<typename T> T multiply(T a, T b) { return a * b + 200000; }
// libdynamic.so
int call_multiply_from_v1(int a, int b) {
std::cout << "Call to v1: ";
return multiply<int>(a, b);
}
// mani_app
int call_multiply_from_v1(int,int);
int main() {
// ...
std::cout << "multiply<int>() direct from v2: " << multiply<int>(3,4) << std::endl;
std::cout << "call_multiply_from_v1: " << call_multiply_from_v1(3,4) << std::endl;
}
Конфликт
Собираем проект и ожидаем корректный вывод:
multiply<int>() direct from v2: 200012
call_multiply_from_v1: Call to v1: 200012 // <----- ожидали 12
Наличие флагов -fvisibility=hidden
, -Wl,--exclude-libs,ALL
ситуаюцию не исправляет.
Попробуем закоментировать первую строку с вызовом multiply<int>(3,4)
:
call_multiply_from_v1: Call to v1: 12
Удивительно, но теперь получили верный ответ.
Разгадка кроется в месте инстанцирования шаблонов: шаблоны истанцируются в тех единицах трасляции, где были вызваны. Т.е. вызов multiply<int>(3,4)
инстанцирует шаблон в main_app
, создавая глобальный символ. Линковщик берёт глобальный символ multiply<int>
и подставляет его libdynamic.so
, хотя в этой библиотеке он есть:
nm libdynamic.so | grep multiply
00000000000011f0 W _Z8multiplyIiET_S0_S0_
nm main_app | grep mult
00000000000012f0 W _Z8multiplyIiET_S0_S0_
Символ помечен W
- weak. Так как оба типа равнозначны, линковщик берет символ из main_app
.
Скрываем символы v2
Решение заключается в том, чтобы символы из main_app
убрать из глобальной области видимости.
В этот раз нам нужно будет создать дополнительный файл со списком глобальных и локальных символов и добавить в флаги компановщика:
# main_app.map
{
global:
main;
local:
*;
};
# cmake main_app
target_link_options(main_app PRIVATE "-Wl,--version-script=${CMAKE_SOURCE_DIR}/main_app.map")
Метод работает только для GNU.
Проверка
После подключения к проекту main_app.map
получим полностью рабочий пример. При этом скрытие символов от статической библиотеки больше не нужно.
Static lib_v2 prints: 2
Dynamic libdyn->lib_v1 prints: Call to v1: 1
multiply<int>() direct from v2: 200012
call_multiply_from_v1: Call to v1: 12
Проверим, что все действительно так:
LD_DEBUG=bindings main_app 2>&1 | grep multiply
# до исправления
944647: binding file libdynamic.so [0] to main_app [0]: normal symbol `_Z8multiplyIiET_S0_S0_'
# после
944739: binding file libdynamic.so [0] to libdynamic.so [0]: normal symbol `_Z8multiplyIiET_S0_S0_'
Полный пример и сборка
Демо-проект лежит на GitHub.
Сборка со всеми опциями:
cmake -S . -B build -DWITH_VISIBILITY_HIDDEN=ON/OFF \
-DWITH_EXCLUDE_LIB2=ON/OFF \
-DWITH_VERSION_SCRIPT=ON/OFF \
-DCOMMENT_TEMPLATE_STATIC_CALL=0/1
cmake --build build
./build/bin/main_app
Вывод
Линковка - дело тонкое. Я уже дважды сталкивался с её подводными камнями: сначала - с настройкой rpath
для плагина Wireshark, теперь - с управлением видимостью символов через visibility
, --exclude-libs
и version scripts
. Оба опыта показали, что конфликты получить крайне легко.