C++ - Что произойдет, если две библиотеки используют один и тот же исходный код для сборки
Я сомневаюсь, что это возможно, если я построил lib1.so
используя исходный файл common.cpp
а также lib2.so
используя тот же исходный файл common.cpp
снова. Теперь я хочу построить свое приложение APP
используя эти две библиотеки,
Мой вопрос
- это возможно или это даст мне ошибку?
- Если он будет успешно построен, то как будет решаться вопрос с именами? Например, скажем
foo
это класс вcommon.cpp
,foo_v1
является объектом foo в lib1.so иfoo_v2
является объектом foo в lib2.so. Сейчас во время хулиганаAPP
что случилось бы? Также возможно создать объект foo вAPP
приложение?
2 ответа
Естественно, можно посоветовать вам создать общую функциональность lib1.so
а также lib2.so
в отдельную общую библиотеку, libcommon.so
,
Но если вы все же хотите статически связать общую функциональность одинаково 1 в оба lib1.so
а также lib2.so
Вы можете связать эти две общие библиотеки с вашей программой. У линкера с этим проблем не будет. Вот иллюстрация:
common.h
#ifndef COMMON_H
#define COMMON_H
#include <string>
struct common
{
void print1(std::string const & s) const;
void print2(std::string const & s) const;
static unsigned count;
};
common.cpp
#include <iostream>
#include "common.h"
unsigned common::count = 0;
void common::print1(std::string const & s) const
{
std::cout << s << ". (count = " << count++ << ")" << std::endl;
}
void common::print2(std::string const & s) const
{
std::cout << s << ". (count = " << count++ << ")" << std::endl;
}
foo.h
#ifndef FOO_H
#define FOO_H
#include "common.h"
struct foo
{
void i_am() const;
private:
common _c;
};
#endif
foo.cpp
#include "foo.h"
void foo::i_am() const
{
_c.print1(__PRETTY_FUNCTION__);
}
bar.h
#ifndef BAR_H
#define BAR_H
#include "common.h"
struct bar
{
void i_am() const;
private:
common _c;
};
#endif
bar.cpp
#include "bar.h"
void bar::i_am() const
{
_c.print2(__PRETTY_FUNCTION__);
}
Теперь мы сделаем две общие библиотеки, libfoo.so
а также libbar.so
, Исходные файлы нам нужны foo.cpp
, bar.cpp
а также common.cpp
, Сначала скомпилируйте их в PIC ( объектно-зависимые файлы кода с независимой позицией:
$ g++ -Wall -Wextra -fPIC -c foo.cpp bar.cpp common.cpp
И вот объектные файлы, которые мы только что сделали:
$ ls *.o
bar.o common.o foo.o
Теперь ссылка libfoo.so
с помощью foo.o
а также common.o
:
$ g++ -shared -o libfoo.so foo.o common.o
Тогда ссылка libbar.so
с помощью bar.o
и опять) common.o
$ g++ -shared -o libbar.so bar.o common.o
Мы это видим common::...
символы определяются и экспортируются libfoo.so
:
$ nm -DC libfoo.so | grep common
0000000000202094 B common::count
0000000000000e7e T common::print1(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
0000000000000efa T common::print2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
(T
средства, определенные в разделе кода, B
означает, что определено в разделе неинициализированных данных). И точно так же верно и в отношении libbar.so
$ nm -DC libbar.so | grep common
0000000000202094 B common::count
0000000000000e7e T common::print1(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
0000000000000efa T common::print2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
Теперь мы сделаем программу, связанную с этими библиотеками:
main.cpp
#include "foo.h"
#include "bar.h"
int main()
{
foo f;
bar b;
common c;
f.i_am();
b.i_am();
c.print1(__PRETTY_FUNCTION__);
return 0;
}
Это вызывает foo
; это вызывает bar
и это вызывает common::print1
,
$ g++ -Wall -Wextra -c main.cpp
$ g++ -o prog main.o -L. -lfoo -lbar -Wl,-rpath=$PWD
Это работает как:
$ ./prog
void foo::i_am() const. (count = 0)
void bar::i_am() const. (count = 1)
int main(). (count = 2)
Что просто отлично. Возможно, вы беспокоитесь, что две копии статической переменной класса common::count
окажется в программе - один из libfoo.so
и еще один из libbar.so
и что foo
будет увеличивать одну копию и bar
увеличит другой. Но этого не произошло.
Как компоновщик разрешил common::...
символы? Хорошо видеть, что нам нужно найти их искаженные формы, как видит их компоновщик:
$ nm common.o | grep common
0000000000000140 t _GLOBAL__sub_I_common.cpp
0000000000000000 B _ZN6common5countE
0000000000000000 T _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
000000000000007c T _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Там все они есть, и мы можем сказать, какой из них с c++filt
:
$ c++filt _ZN6common5countE
common::count
$ c++filt _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
common::print1(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
$ c++filt _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
common::print2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) const
Теперь мы можем заново сделать связь prog
, на этот раз попросив компоновщика сообщить нам имена входных файлов, в которых эти common::...
символы были определены или на которые есть ссылки. Эта диагностическая связь немного глотка, поэтому я \
-раздели это:
$ g++ -o prog main.o -L. -lfoo -lbar -Wl,-rpath=$PWD \
-Wl,-trace-symbol=_ZN6common5countE \
-Wl,-trace-symbol=_ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE \
-Wl,-trace-symbol=_ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
main.o: reference to _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libfoo.so: definition of _ZN6common5countE
./libfoo.so: definition of _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libfoo.so: definition of _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libbar.so: reference to _ZN6common5countE
./libbar.so: reference to _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libbar.so: reference to _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Таким образом, компоновщик говорит нам, что он связал определение common::count
от ./libfoo.so
, Аналогично определению common::print1
, Аналогично определению common::print2
, Это связало все common::...
определения символов из libfoo.so
,
Это говорит нам, что ссылка (ы) на common::print1
в main.o
было решено к определению в libfoo.so
, Аналогично ссылка (и) на common::count
в libbar.so
, Аналогично ссылка (и) на common::print1
а также common::print2
в libbar.so
, Все common::...
ссылки на символы в программе были преобразованы в определения, предоставленные libfoo.so
,
Таким образом, не было многократных ошибок определения, и нет никакой неопределенности относительно того, какие "копии" или "версии" common::...
символы используются программой: она просто использует определения из libfoo.so
,
Зачем? Просто потому что libfoo.so
была первой библиотекой в связи, которая предоставила определения для common::...
символы. Если мы связываем prog
с заказом -lfoo
а также -lbar
наоборот:
$ g++ -o prog main.o -L. -lbar -lfoo -Wl,-rpath=$PWD \
-Wl,-trace-symbol=_ZN6common5countE \
-Wl,-trace-symbol=_ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE \
-Wl,-trace-symbol=_ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
main.o: reference to _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libbar.so: definition of _ZN6common5countE
./libbar.so: definition of _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libbar.so: definition of _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libfoo.so: reference to _ZN6common5countE
./libfoo.so: reference to _ZNK6common6print2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
./libfoo.so: reference to _ZNK6common6print1ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
тогда мы получим прямо противоположные ответы. Все common::...
ссылки на символы в программе теперь разрешены в определения, предоставленные libbar.so
, Так как libbar.so
предоставил их первым. Там все еще нет неопределенности, и это не имеет никакого значения для программы, потому что оба libfoo.so
а также libbar.so
связал common::...
определения из того же объектного файла, common.o
,
Компоновщик не пытается найти несколько определений символов. Как только он нашел определение символа S во входном объектном файле или совместно используемой библиотеке, он связывает ссылки на S с определением, которое он нашел, и делает это с разрешением S. Ему не важно, может ли совместно используемая библиотека, которую она найдет позже, предоставить другое определение S, такое же или другое, даже если эта более поздняя совместно используемая библиотека разрешает символы, отличные от S.
Единственный способ вызвать ошибку множественного определения - это заставить компоновщика статически связать несколько определений, то есть заставить его физически объединить в выходной двоичный файл два объектных файла. obj1.o
а также obj2.o
оба из которых содержат определение S. Если вы сделаете это, конкурирующие статические определения имеют точно такой же статус, и программа может использовать только одно определение, поэтому компоновщик должен вас подвести. Но ему не нужно обращать внимания на динамическое определение символа S, предоставляемое совместно используемой библиотекой, если оно уже разрешило S, и не делает этого.
[1] Конечно, если вы компилируете и ссылаетесь
lib1
а также lib2
с различными параметрами препроцессора, компилятора или компоновки вы можете саботировать "общие" функции в произвольной степени.
Или это даст мне ошибку?
Вы, вероятно, получите ссылки на переопределение символов / множественные ошибки при связывании.
Является ли это возможным
Вполне возможно. Даже если вы используете тот же файл common.cpp, вы не обязательно получите те же двоичные файлы. Вы можете использовать флаги препроцессора и компиляции, чтобы настроить создаваемый двоичный файл (например, использовать разные пространства имен или настроить именование) и, таким образом, избежать ошибок переопределения символов.
Если он будет успешно построен, то как будет решаться вопрос с именами?
Связывание не удастся, если произойдет несколько определений символов. Компоновщик не сможет определить, какая версия нужна.