tgoop.com/cxx95/76
Last Update:
#creepy #compiler
Самое мерзкое правило в C++ для модульных программ и как его обойти
Недавно я в своем pet project снова столкнулся с тем, что могу назвать самым мерзким правилом C++ в своем опыте, по крайней мере для модульных программ. Оно связано с особенностями работы линковщика и требует всяких тайных знания для решения.
В больших модульных проектах для скорости разработки иногда используется такая схема - каждому модулю бизнес-логики соответствует статическая библиотека (static library).
Если какой-то модуль с помощью всяких флагов системы сборки не линковать в итоговый бинарник, то этот модуль не будет билдиться, а в итоговом бинарнике ничего не сломается. Просто в бинарнике будет меньше фичей.
С другой стороны, если модуль слинкован, то он должен как-то "зарегистрировать" себя в списке модулей.
Есть файл module.h с такими методами (упрощенно)
struct IModule { virtual void Do() = 0; };
void AddModule(std::unique_ptr<IModule> module);
const std::vector<std::unique_ptr<IModule>>& GetModules();
Файл main.cpp должен использовать GetModules(), а модуль должен зарегистрировать сам себя через AddModule.Единственный способ, которым это можно адекватно сделать - добавить код, который должен вызываться на старте программы. Это делается через статическую инициализацию объектов в конструкторе объекта. Где-то в одном из
.cpp-файлов модуля должно быть такое: struct Dummy {
Dummy() {
AddModule(std::make_shared<MyCoolModule>());
}
};
static Dummy dummy;
Дальше начинается кино. Переменная dummy и код для ее инициализации попадает в статическую библиотеку libcoolmodule.a (можно проверить через objdump), но при линковке бинарника эта переменная выбрасывается линкером как неиспользуемая. В итоге модуль не зарегистрируется.Процесс решения проблемы зависит от системы сборки, операционной системы и многих других вещей. Общепринятого решения проблемы нет. Как эту проблему решают в разных случаях:
__pragma comment(linker,"/include:?variable_name@")
static Var_t g_DumbVar __attribute__((__used__, section(".var_section.g_DumbVar"))) = (const Var_t) X_MARKER;
static Var_t* g_DumbVarGuard[] __attribute__((__used__, section(".guard"))) = { &g_DumbVar };
static и volatile.Через время поисков я нашел ответ, почему так работает линкер.
Когда собираешь бинарник, то индивидуальные объектные файлы (
.o) в команде к линкеру линкуются по порядку и ничто не выбрасывается.С библиотеками (
.a, архив из .o) есть "оптимизация": .o-файл из библиотеки линкуется только если в нем находится определение какого-нибудь undefined symbol, который требуется в уже слинкованных прежде .o-файлах. В противном случае считается, что этот .o-файл не нужен и в бинарник он не попадает.В системе сборки CMake есть метод, который позволит обойти это правило. Надо заменить такую строку:
add_library(enum_serializer STATIC module.cpp helper.cpp)на такую:
add_library(enum_serializer OBJECT module.cpp helper.cpp)И тогда, если какой-то бинарник зависит от
enum_serializer, он будет линковать не libenum_serializer.a, а module.o и helper.o.Поэтому "регистрация модуля" сработает и проблема будет решена
