Статическое связывание со сгенерированными protobuf приводит к прерыванию

У меня есть проект, который компилирует сгенерированные С++ сериализаторы protobuf в статическую библиотеку. Исполняемый файл ссылается на эту библиотеку, и .so (.dll) тоже. Позже исполняемый файл загружает файл .so. Когда это происходит, я получаю:

[libprotobuf ERROR /mf-toolchain/src/protobuf-3.0.0-beta-1/src/google/protobuf/descriptor_database.cc:57] File already exists in database: mri.proto
[libprotobuf FATAL /mf-toolchain/src/protobuf-3.0.0-beta-1/src/google/protobuf/descriptor.cc:1128] CHECK failed: generated_database_->Add(encoded_file_descriptor, size): 
terminate called after throwing an instance of 'google::protobuf::FatalException'
what(): CHECK failed: generated_database_->Add(encoded_file_descriptor, size): 
Aborted (core dumped)

Просто чтобы быть ясным, у меня есть одна статическая библиотека A, которая связана с программой P и разделяемой библиотекой S. Позже P загружает S, и я получаю указанную выше ошибку.

Я просмотрел подобные ошибки в Stack Overflow и в Google в целом, но я совершенно уверен, что я только связываюсь с библиотекой, а не перекомпилирую исходный код. Насколько я могу судить, это должно означать, что скомпилированные данные одинаковы.

Также обратите внимание: эта проблема возникает только в Linux. Это отлично работает в Windows и OS X.


person Christopher    schedule 08.10.2015    source источник


Ответы (2)


Проблема в том, что ваша статическая библиотека содержит файл mri.pb.cc, который в своих глобальных инициализаторах регистрирует дескрипторы типов в глобальной базе данных дескрипторов, поддерживаемой libprotobuf. Поскольку ваша статическая библиотека загружается в вашу программу дважды, этот инициализатор запускается дважды, но поскольку в вашем процессе есть только одна копия libprotobuf, оба инициализатора регистрируются в одной и той же глобальной базе данных, и обнаруживается конфликт.

Чтобы решить эту проблему, вам нужно изменить вашу статическую библиотеку на разделяемую библиотеку, от которой зависит как основная программа, так и динамически загружаемая библиотека.

Я не уверен, почему вы видите другое поведение в Windows или OSX. Насколько я понимаю, на этих платформах вы фактически связываете две отдельные копии libprotobuf со своей программой — одну в основном исполняемом файле и одну в динамически загружаемой библиотеке. Таким образом, есть две копии базы данных дескрипторов и нет конфликта. Однако вы, вероятно, увидите здесь гораздо более тонкие проблемы. Если вы когда-нибудь будете передавать указатели объектов protobuf между основной программой и динамически загружаемым модулем (без сериализации и повторного разбора), вы можете получить объект protobuf, созданный одной копией библиотеки, но используемый с другой копией (и, следовательно, другая копия базы данных дескрипторов), что запутает библиотеку и вызовет странные вещи.

В качестве альтернативы, если вы никогда не передаете объекты protobuf через границу, вы можете «исправить» проблему в Linux, скомпоновав libprotobuf статически, чтобы получить две копии, как описано выше. Но это довольно опасно; Я бы не рекомендовал это.

person Kenton Varda    schedule 09.10.2015
comment
Я статически связывал libprotobuf, и это не помогло. Есть ли какие-либо проблемы со статическим связыванием, если я использую облегченную среду выполнения и никогда не передаю указатели? - person Christopher; 09.10.2015
comment
@Christopher Как правило, подход Linux к динамической компоновке плохо работает с загрузкой нескольких копий одних и тех же символов в один и тот же процесс. Возможно, копия libprotobuf в основном исполняемом файле переопределяет копию в динамической библиотеке, поэтому они оба используют одну копию, отсюда и проблема. Вам, вероятно, придется динамически связать libprotobuf и вашу библиотеку, чтобы полностью избежать множественных копий. - person Kenton Varda; 09.10.2015
comment
Оказалось, что проблема была не в этом. Эта ошибка была недавно исправлена ​​и была связана с плохо спроектированным статическим инициализатором. - person Christopher; 25.01.2016
comment
привет @Christopher, я столкнулся с той же проблемой, когда добавил две отдельные статические библиотеки, содержащие pb на iOS, не могли бы вы сказать мне, в какой версии google protobuf исправил эту ошибку? это очень полезно, если какая-либо ссылка предоставляется. благодарю вас. - person johnMa; 22.06.2016
comment
@johnma: я думаю, что это protobuf3-beta3. Некоторое время назад они пометили мою ошибку закрытой, хотя я не могу ее найти. - person Christopher; 22.06.2016
comment
@Christopher: наконец, я исправил это, изменив одно из пространств имен protobuf с google::xxx на mypb:xxx , и это решило мою проблему. - person johnMa; 09.09.2016

Эта ошибка появилась у меня в контексте исполняемого файла, связанного с двумя библиотеками (LibA, LibB), обе из которых компилировали одно и то же прото-сообщение, где LibB зависит от LibA и ссылается на него.

Я столкнулся с этой ситуацией из-за того, что LibA ранее не зависел ни от какой из инфраструктур протобуфера, а LibB создал полный набор соответствующих прото-сообщений для этого внутреннего инструментального приложения для связи с другим приложением. С новым выпуском LibA потребовались новые зависимости от двух других библиотек, которые компилируют различные прото-сообщения (LibC, LibD). Проблема проявилась одинаково как с LibC, так и с LibD, я буду обсуждать LibC, так как решение было идентичным.

Во время загрузки приложение загружало LibC, и, в конце концов, оно загружало самый верхний модуль LibB, и именно тогда срабатывало прерывание в LogMessage::Finish() в common.cc. Я обнаружил, кто выполняет эту двойную загрузку, установив точку останова на несколько уровней выше контекста прерывания. Вот соответствующий источник для прото-сообщения с двойной загрузкой, которое я вызываю SomeMessage

void LibC_AddDesc_SomeMessage_2eproto() {
  static bool already_here = false; // <--- Breakpoint Set Here
  if (already_here) return;
  already_here = true;

Точка останова 1: загрузка LibC

LibC.dll!MyNamespace::LibC_AddDesc_SomeMessage_2eproto()  Line 415  C++
LibC.dll!MyNamespace::LibC_AddDesc_ParentMessage_2eproto()  Line 388    C++
LibC.dll!MyNamespace::StaticDescriptorInitializer_ParentMessage_2eproto::StaticDescriptorInitializer_ParentMessage_2eproto()  Line 494  C++
LibC.dll!MyNamespace::`dynamic initializer for 'static_descriptor_initializer_ParentMessage_2eproto_''()  Line 495 + 0x21 bytes C++
msvcr100d.dll!_initterm()  + 0x2c bytes 
LibC.dll!_CRT_INIT(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 289  C
LibC.dll!__DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 506 + 0x13 bytes   C
LibC.dll!_DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 477 C
ntdll.dll!LdrpRunInitializeRoutines()  + 0x1e8 bytes    
ntdll.dll!LdrpInitializeProcess()  - 0x14c9 bytes   
ntdll.dll!string "Enabling heap debug options\n"()  + 0x29a99 bytes 
ntdll.dll!LdrInitializeThunk()  + 0xe bytes 

Во время загрузки LibC я мог видеть, что точка останова сработала дважды, а статическая переменная already_here была установлена ​​с false на true, удерживалась в true и пропустила регистрацию этого сообщения.

Точка останова 2: загрузка LibB

Когда эта библиотека попытается загрузить, переменная already_here будет повторно инициализирована значением false, и мы попытаемся зарегистрировать это сообщение во второй раз, что вызовет прерывание.

LibB.dll!MyNamespace::LibC_AddDesc_SomeMessage_2eproto()  Line 415  C++
LibB.dll!MyNamespace::LibC_AddDesc_ParentMessage_2eproto()  Line 388    C++
LibB.dll!MyNamespace::LibC_AddDesc_FullMessage_2eproto()  Line 219  C++
LibB.dll!MyNamespace::StaticDescriptorInitializer_FullMessage_2eproto::StaticDescriptorInitializer_FullMessage_2eproto()  Line 358  C++
LibB.dll!MyNamespace::`dynamic initializer for 'static_descriptor_initializer_FullMessage_2eproto_''()  Line 359 + 0x21 bytes   C++
msvcr100d.dll!_initterm()  + 0x2c bytes 
LibB.dll!_CRT_INIT(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 289  C
LibB.dll!__DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 506 + 0x13 bytes   C
LibB.dll!_DllMainCRTStartup(void * hDllHandle, unsigned long dwReason, void * lpreserved)  Line 477 C
ntdll.dll!LdrpRunInitializeRoutines()  + 0x1e8 bytes    
ntdll.dll!LdrpInitializeProcess()  - 0x14c9 bytes   
ntdll.dll!string "Enabling heap debug options\n"()  + 0x29a99 bytes 
ntdll.dll!LdrInitializeThunk()  + 0xe bytes 

... и мы оказывались бы в stubs/common.cc в строке прерывания

void LogMessage::Finish() {
  bool suppress = false;

  if (level_ != LOGLEVEL_FATAL) {
    InitLogSilencerCountOnce();
    MutexLock lock(log_silencer_count_mutex_);
    suppress = internal::log_silencer_count_ > 0;
  }

  if (!suppress) {
    internal::log_handler_(level_, filename_, line_, message_);
  }

  if (level_ == LOGLEVEL_FATAL) {
    abort(); // <----- runtime crash!
  }
}

А для std::err вы найдете следующий текст...

libprotobuf ERROR descriptor_database.cc:57] File already exists in database: SomeMessage.proto
libprotobuf FATAL descriptor.cc:860] CHECK failed: generated_database_->Add(encoded_file_descriptor, size):

Решение было простым: я открыл проект LibC, поискал pb и удалил эти прото-сообщения из LibB. То же самое было сделано для LibD.

person jxramos    schedule 10.08.2016