Как бороться с конфликтами символов между статически связанными библиотеками?

Один из самых важных правил и рекомендаций при написании библиотеки - это положить все символы библиотеки в библиотечное пространство имен. С++ делает это проще, благодаря ключевому слову namespace. В C обычным подходом является префикс идентификаторов с некоторым конкретным префиксом библиотеки.

Правила стандарта C ставят некоторые ограничения на эти (для безопасной компиляции): компилятор C может смотреть только на первый 8 символов идентификатора, поэтому foobar2k_eggs и foobar2k_spam можно интерпретировать как одно и то же идентификаторы действительны - однако каждый современный компилятор допускает произвольные длинные идентификаторы, поэтому в наше время (21 век), мы не должны беспокоиться об этом.

Но что, если вы сталкиваетесь с некоторыми библиотеками, из которых вы не можете изменить имена /idenfiers? Может быть, вы получили только статические двоичные файлы и заголовки или не хотят, или им не разрешено настраивать и перекомпилировать себя.

Ответ 1

По крайней мере, в случае статических библиотек вы можете обходиться довольно удобно.

Рассмотрим те заголовки библиотек foo и bar. Ради этого урока я также расскажу о исходных файлах

примеры /ex 01/foo.h

int spam(void);
double eggs(void);

examples/ex01/foo.c(это может быть непрозрачно/недоступно)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

Пример /ex 01/bar.h

int spam(int new_spams);
double eggs(double new_eggs);

examples/ex01/bar.c(это может быть непрозрачно/недоступно)

int the_spams;
double the_eggs;

int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}

double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

Мы хотим использовать их в программе foobar

Пример /ex 01/foobar.c

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Одна проблема становится очевидной сразу: C не знает перегрузки. Таким образом, у нас есть две функции с двумя идентичное имя, но с другой подписью. Поэтому нам нужно каким-то образом отличить их. В любом случае, посмотрим, что компилятор должен сказать об этом:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

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

Итак, можем ли мы каким-то образом разрешить это столкновение идентификаторов без изменения исходных библиотек, исходный код или заголовки? На самом деле мы можем.

Сначала разрешаем проблемы времени компиляции. Для этого мы окружаем заголовок, включающий набор препроцессоров #define, которые префикс всех символов, экспортируемых библиотекой. Позже мы делаем это с помощью приятного уютного оберточного заголовка, но только ради демонстрации что происходит, делайте это дословно в исходном файле foobar.c:

Пример /ex 02/foobar.c

#include <stdio.h>

#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs

#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Теперь, если мы скомпилируем это...

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

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

Посмотрите на таблицы символов с помощью утилиты nm:

example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

Итак, теперь нам предлагается упражнение префикс этих символов в некоторых непрозрачных двоичных файлах. Да, я знаю в ходе этого примера у нас есть источники и могут изменить это. Но пока просто предположим у вас есть только те файлы .o или .a(на самом деле это всего лишь куча .o).

objcopy для спасения

Для нас особенно интересен один инструмент: objcopy

objcopy работает во временных файлах, поэтому мы можем использовать его, как если бы он работал на месте. Есть один option/operation, называемый --prefix-symbols, и у вас есть 3 догадки о том, что он делает.

Итак, бросьте этого парня на наши упрямые библиотеки:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

nm показывает нам, что это сработало:

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams

example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

Давайте попробуем связать все это:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

И действительно, это сработало:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

Теперь я оставляю это как упражнение для чтения для реализации инструмента /script, который автоматически извлекает символы библиотеки, использующей nm, записывает заголовочный файл оболочки структуры

/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

и применяет префикс символа к объектным файлам статической библиотеки, используя objcopy.

Что относительно разделяемых библиотек?

В принципе то же самое можно сделать с общими библиотеками. Однако общие библиотеки, это имя говорит об этом, разделяются между несколькими программами, поэтому беспорядок с общей библиотекой таким образом не является такой хорошей идеей.

Вам не обойтись написанием батутной обертки. Хуже того, вы не можете подключиться к общей библиотеке на уровне объектного файла, но вынуждены выполнять динамическую загрузку. Но это заслуживает своей собственной статьи.

Оставайтесь с нами, и счастливое кодирование.

Ответ 2

Правила стандарта C устанавливают некоторые ограничения для этих (для безопасной компиляции): компилятор AC может смотреть только на первые 8 символов идентификатора, поэтому foobar2k_eggs и foobar2k_spam могут быть интерпретированы как одни и те же идентификаторы достоверно - однако каждый современный компилятор позволяет использовать произвольные длинные идентификаторы, поэтому в наше время (21-й век) нам не нужно беспокоиться об этом.

Это не просто расширение современных компиляторов; для текущего стандарта C также требуется компилятор для поддержки достаточно длинных внешних имен. Я забыл точную длину, но теперь мне что-то вроде 31 символа, если я правильно помню.

Но что, если вы сталкиваетесь с некоторыми библиотеками, из которых вы не можете изменить имена /idenfiers? Возможно, у вас есть только статические двоичные файлы и заголовки, или вы не хотите, или им не разрешено настраивать и перекомпилировать себя.

Тогда ты застрял. Жалуйтесь автору библиотеки. Я однажды столкнулся с такой ошибкой, когда пользователи моего приложения не смогли создать его на Debian из-за ссылки Debian libSDL libsoundfile, которая (по крайней мере в то время) ужасно загрязняла глобальное пространство имен такими переменными, как dsp (I не бойся!). Я жаловался Debian, и они исправили свои пакеты и отправили исправление вверх по течению, где, как я предполагаю, оно было применено, поскольку я больше не слышал о проблеме снова.

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

Если вам действительно нужно быстрое исправление, и у вас есть источник, вы можете добавить кучу -Dfoo=crappylib_foo -Dbar=crappylib_bar и т.д. в make файл, чтобы исправить это. Если нет, используйте найденное решение objcopy.

Ответ 3

Если вы используете GCC, ключ -allow-multiple-definition linker - это удобный инструмент для отладки. Это заставляет компоновщика использовать первое определение (и не скулить по нему). Подробнее об этом здесь.

Это помогло мне во время разработки, когда у меня есть источник доступной библиотеки, доступной поставщикам, и по какой-то причине вам нужно отслеживать библиотечную функцию. Коммутатор позволяет вам скомпилировать и связать в локальной копии исходного файла и по-прежнему ссылаться на немодифицированную статическую библиотеку поставщиков. Не забудьте вытащить выключатель обратно из символов маркировки, как только завершение рейса завершено. Код выпуска доставки с преднамеренными столкновениями пространства имен склонен к ловушкам, включая непреднамеренные столкновения пространства имен.