Что на самом деле происходит, когда две совместно используемые библиотеки определяют один и тот же символ?

#c #shared-libraries

#c #shared-libraries

Вопрос:

Недавно я столкнулся с проблемой сбоя, когда я связал две разделяемые библиотеки (обе созданные мной) вместе. В конце концов я обнаружил, что это было из-за того, что один исходный файл дублировался между двумя файлами. В этом исходном файле был определен глобальный std::vector (фактически статический член класса), и в итоге он был освобожден дважды — по одному каждой библиотекой.

Затем я написал некоторый тестовый код, чтобы проверить свою мысль. Здесь в заголовке я объявляю класс и глобальный объект этого класса:

 #ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};

extern const Data data;

#endif
  

FuncDefinedByLib Функция остается неопределенной.
Затем я создал две библиотеки, libA и libB , обе содержат этот заголовок.
libA выглядит так

 const Data data;

int Data::FuncDefinedByLib(void) const {return 1;}

void PrintA(void) {
  std::cout << "LibB:" << amp;data << " "
    << (void*)amp;Data::FuncDefinedByLib <<  " "
    << data.FuncDefinedByLib() << std::endl;
}
  

Он определяет глобальный data объект, FuncDefinedByLib функцию и функцию PrintA , которая выводит адрес data объекта, адрес FuncDefinedByLib и возвращаемое значение FuncDefinedByLib .

libB почти такой же, libA за исключением того, что имя PrintA изменено на PrintB и FuncDefinedByLib возвращает 2 вместо 1.

Затем я создаю программу, которая ссылается на обе библиотеки и вызывает PrintA и PrintB . До того, как столкнуться с проблемой сбоя, я думал, что обе библиотеки создадут свои собственные версии class Data . Однако фактический результат

 Constructor
Constructor
LibB:0x7efceaac0079 0x7efcea8bed60 1
LibB:0x7efceaac0079 0x7efcea8bed60 1
Destructor
Destructor
  

Указывает, что обе библиотеки используют только одну версию class Data и только одну версию const Data data , даже если класс и объект определены по-разному, то есть из libA (я понимаю, это потому, что libA сначала связывается). И двойное уничтожение четко объясняет мою проблему сбоя.

Итак, вот мои вопросы

  1. Как это происходит? Я понимаю, что основной код, связывающий две библиотеки, может ссылаться только на первый символ, который он видит. Но разделяемая библиотека должна была быть связана внутренне при ее создании (или это не так? Я действительно не очень разбираюсь в разделяемой библиотеке), как они могут знать, что в других библиотеках есть класс-близнец, и ссылаться на него, когда после того, как они были созданы самостоятельно?

  2. Я знаю, что дублирование кода между разделяемыми библиотеками, как правило, является плохой практикой. Но есть ли условие, при выполнении которого дублирование между библиотеками безопасно? Или есть систематический способ сделать мой код дублируемым без риска? Или это никогда не бывает безопасным и всегда должно быть строго запрещено? Я не хочу всегда разделять другую разделяемую библиотеку только для совместного использования крошечного фрагмента кода.

  3. Такое поведение выглядит волшебно. Кто-нибудь использует это поведение, чтобы делать какие-нибудь хорошие волшебные вещи?

Комментарии:

1. Два определения чего-либо (функции, типа и т.д.) В полной программе, в соответствии со стандартом C , Приводят либо к диагностируемой ошибке (например, оба определения, видимые компилятору в пределах одного модуля компиляции), Либо к неопределенному поведению. Неопределенное поведение может проявляться множеством способов, включая ошибки компоновщика (например, дублирующиеся символы), непреднамеренное поведение (например, библиотеки, загруженные в определенном порядке при запуске программы, и используется первое найденное определение). Необходимо контролировать эффекты (например, сохранять символы закрытыми для библиотеки), чтобы получить работающую программу. Специфика зависит от системы.

Ответ №1:

Часть 1: О компоновщике

Это известная проблема как в C, так и в C , и это результат текущей модели компиляции. Полное объяснение того, как это происходит, выходит за рамки данного ответа, однако в этом выступлении Мэтта Годболта дается подробное объяснение процесса для начинающих. Смотрите также эту статью о компоновщике.

В 2020 году выходит новая версия C , и в ней будет представлена новая модель компиляции (называемая модулями), которая позволяет избежать подобных проблем. Вы сможете импортировать и экспортировать содержимое из модуля, аналогично тому, как пакеты работают в Java.

Часть 2: Решение вашей проблемы

Существует несколько разных решений.

Волшебное решение 1: Одна уникальная глобальная переменная

Этот довольно гладкий. Если вы вставляете глобальную переменную внутри функции в качестве статической переменной, она всегда будет создаваться только один раз, и это обеспечивается стандартом (даже в многопоточной среде).

 #ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
Dataamp; getMyDataExactlyOnce() {
    // The compiler will ensure
    // that data only gets constructed once
    static Data data;
    // Because data is static, it's fine to return a reference to it
    return data; 
}

// Here, the global variable is a reference
extern const Dataamp; data = getMyDataExactlyOnce();

#endif
  

Волшебное решение 2: несколько различных глобальных переменных, по 1 на единицу перевода

Если вы пометите глобальную переменную как встроенную в C 17, то каждая единица перевода, включающая заголовок, получит свою собственную копию в своем собственном расположении в памяти. Смотрите: https://en.cppreference.com/w/cpp/language/inline

 #ifndef SHARED_HEADER_H_
#define SHARED_HEADER_H_

#include <iostream>

struct Data {
  Data(void) {std::cout << "Constructor" << std::endl;}
  ~Data(void) {std::cout << "Destructor" << std::endl;}
  int FuncDefinedByLib(void) const;
};
// Everyone gets their own copy of data
inline extern const Data data;

#endif
  

Часть 3: Можем ли мы использовать это для выполнения темной магии?

Вроде того. Если вы действительно хотите сотворить Темную магию с глобальными переменными, в C 14 представлены шаблонные глобальные переменные:

 template<class Key, class Value>
std::unordered_map<Key, Value> myGlobalMap; 

void foo() {
    myGlobalMap<int, int>[10] = 20;
    myGlobalMap<std::string, std::string>["Hello"] = "World"; 
}
  

Делайте с этим что хотите. Я не особо пользовался шаблонными глобальными переменными, хотя, полагаю, если бы вы делали что-то вроде подсчета количества вызовов функции или количества создания типа, было бы полезно сделать это.