group-telegram.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
.Поэтому "регистрация модуля" сработает и проблема будет решена