Про компоновку, dependency hell и обратную совместимость

В данной статье речь пойдёт о высокоуровневом взгляде на компоновку. Где ищутся разделяемые библиотеки на Linux, BSD*, Mac OS X, Windows, от которых зависят приложения? Что делать с обратной совместимостью? Как бороться с адом зависимостей?

Предполагается, что читатель знаком с такими наборами символов как «компилятор», «объектный файл», «компоновщик», «статическая библиотека», «динамическая библиотека», «динамический загрузчик» и некоторыми другими, поэтому разжёвывать мы ничего не будем.

Проблемы статической загрузки динамических библиотек:

  1. main.exe зависит от version-0.3.dll и bar.dll. bar в свою очередь, зависит от version-0.2.dll, которая бинарно не совместима с версией 0.3 (не просто символы отсутствуют, а совпадают имена, но различное число аргументов, или создают объекты разной природы и т. п.). Затрут ли символы из version-0.2.dll оные из version-0.3.dll? Тот же вопрос стоит тогда, когда используется одна статическая версия библиотеки (скажем, version-0.2.lib) и динамическая (version-0.3.dll);
  2. создание перемещаемых приложений: где динамический загрузчик будет искать version-0.?.dll и bar.dll для приложения из предыдущего пункта? Найдёт ли он зависимости main.exe, если тот будет перемещён в другую папку? Как нужно собрать main.exe, чтобы зависимости искались относительно исполняемого файла?
  3. dependency hell: две версии одной библиотеки /opt/kde3/lib/libkdecore.so и /opt/kde4/lib/libkdecore.so (с которой плазма уже не падает), половина программ требуют первую, другая половина программ — вторую. Обе библиотеки нельзя поместить в одну область видимости (один каталог). Эта же проблема есть и в п. 1, т. к. надо поместить две версии библиотеки version в один каталог.

После прочтения первого пункта читатель может воскликнуть: «Да это извращение! Так не делают! Надо использовать одну версию библиотеки!» Да, это так, но в жизни всякое бывает. Простейший пример: ваше приложение использует библиотеку а, и стороннюю закрытую библиотеку (я подчёркиваю это, чужой платный продукт) б, которая ну вот никак не может использовать ту же версию а, что и вы или наоборот.

Другим примером в рамках огромного проекта служит тот факт, что выпуск различных частей имеет разный период (с чем наша команда столкнулась и что вообще послужило причиной написания данного текста). Таким образом, главный продукт может время от времени выходить в конфигурации, описанной в пункте 1.

Dependency hell больше актуален для разработчиков системных библиотек и операционных систем, но и в прикладной области может возникнуть. Опять же предположим, что имеется огромный проект, в котором несколько исполняемых программ. Все они зависят от одной библиотеки, но разных версий. (Это не та же ситуация, что и в п. 1: там в один процесс загружается две версии одной библиотеки, а здесь в каждый процесс загружается только одна, но каждый использует свою версию).

Побег из ада

Ответ прост: надо добавить версию в имя файла библиотеки. Это позволит размещать файлы библиотек в одном каталоге. При этом рекомендуется добавлять версию ABI, а не API, порождая тем самым две параллельных ветки версий и соответствующие трудности.

Контроль версии – очень рутиная работа. Рассмотрим схему x.y.z:
  • x – мажорный выпуск. Ни о какой совместимости речи не идёт;
  • y – минорный выпуск. Либа совместима на уровне исходных текстов, но двоичная совместимость может быть сломана;
  • z – багофикс. Либа совместима в обе стороны.

Тогда в имя файла разумно включить x.y. Если при увеличении минорной версии совместимость сохранили, то достаточно сделать соответствующий симлинк:
version-1.1.dll
version-1.0.dll → version-1.1.dll

Будут работать и приложения, использующие version-1.1.0, и те, кто использует version-1.0.x.

Если совместимость сломали, то в системе будет два файла и снова всё будет работать.

Если по каким-то причинам совместимость сломали при багофиксе, то должна быть увеличена минорная версия (и нечего фейлится, как это сделала команда любимейшего Qt [1]).

Кстати говоря, никто не запрещает вообще включить версию API – тогда символических ссылок будет больше, т.к. совместимость чаще сохраняется. Зато в этом случае упомянутый фейл Qt разрулился бы легко и не заставил увеличивать минорную версию.

Это справедливо для всех платформ.

Решение оставшихся двух вопросов отличается в зависимости от ОС.

ELF & GNU ld (Linux, *BSD, etc)

В разделяемой библиотеке формата ELF присутствует так называемое SONAME [2][3]. Это – строка символов, которая прописывается в двоичный файл в секцию DT_SONAME. Просмотреть SONAME для библиотеки можно, например, так:
$ objdump -p /path/to/file | grep SONAME

Если программа/библиотека faz связывается с библиотекой baz, которая имеет SONAME = baz-0.dll, то строка baz-0.dll будет жёстко прописана в двоичном файле faz в секции DT_NEEDED, и при его запуске динамический загрузчик будет искать файл с именем baz-0.dll. При этом никто не запрещает назвать файл по-другому!

Просмотреть SONAME'ы, от которых зависит исполняемый файл можно так:
$ objdump -x /path/to/file | grep NEEDED

Динамический загрузчик ищет библиотеки из секции DT_NEEDED в следующих местах в данном порядке [4][5]:
  1. список каталогов в секции DT_RPATH, которая жёстко прописана в исполняемом файле. Поддерживается большинством *nix-систем. Игнорируется, если присутствует секция DT_RUNPATH;
  2. LD_LIBRARY_PATH – переменная окружения, также содержит список каталогов;
  3. DT_RUNPATH – тоже самое, что и DT_RPATH, только просматривается после LD_LIBRARY_PATH. Поддерживается только на самых свежих Unix-подобных системах;
  4. /etc/ld.so.conf – файл настроек динамического загрузчика ld.so, который содержит список папок с библиотеками;
  5. жёстко зашитые пути – обычно /lib и /usr/lib.

Формат данных для RPATH, LD_LIBRARY_PATH и RUNPATH такой же, как и для PATH: список путей, разделённых двоеточием. Просмотреть RUNPATH'ы можно, например, так:
$ objdump -x /path/to/file | egrep 'R(|UN)PATH'

R[UN]PATH может содержать специальную метку $ORIGIN, которую динамический загрузчик развернёт в полный абсолютный путь до загружаемой сущности. Здесь стоит отметить, что некоторые разработчики добавляют в RUNPATH “.” (точку). Это не тоже самое, что $ORIGIN! Точка развернётся в текущий рабочий каталог, который естественно не обязан совпадать с путём до сущности!

Для демонстрации написанного, разработаем приложение по схеме из п. 1 (ссылка на хранилище в гитхабе: github.com/gshep/linking-sample). Чтобы собрать всю систему достаточно перейти в корень папки и вызвать ./linux_make_good.sh, результат будет в папке result. Ниже будут разобраны некоторые этапы сборки.

На этапе компоновки библиотек version-0.x задаются SONAME:
$ gcc -shared -Wl,-soname,version-0.3.dll -o version-0.3.dll version.o

Они зависят только от системных библиотек и поэтому не требуют наличия секций R[UN]PATH.

Библиотека bar уже зависит от version-0.2, поэтому нужно указать RPATH:
$ gcc -shared -Wl,-rpath-link,/path/to/version-0.2/ -L/path/to/version-0.2/ -l:version-0.2.dll -Wl,-rpath,\$ORIGIN/ -Wl,--enable-new-dtags -Wl,-soname,bar.dll -o bar.dll bar.o

Параметр --enable-new-dtags указывает компоновщику заполнить секцию DT_RUNPATH.

Параметр -Wl,-rpath,... позволяет заполнить секцию R[UN]PATH. Для задания списка путей можно указать параметр несколько раз, либо перечислить все пути через двоеточие:
$ gcc -Wl,-rpath,/path/1/ -Wl,-rpath,/path/2 ...
$ gcc -Wl,-rpath,/path/1:/path/2 ...

Теперь всё содержимое папки result целиком или саму папку можно перемещать по файловой системе как угодно, но при запуске динамический загрузчик найдёт все зависимости и программа исполнится:
$ ./result/main.exe
Hello World!
bar library uses libversion 0.3.0, number = 3
version::get_version result = 0
But I uses liversion 0.3.0
number = 3

Вот мы и подошли к проблеме затирания символов! Bar использует version-0.2.dll, в которой get_number() возвращает 2, а само приложение version-0.3.dll, где та же функция возращает уже 3. По выводу приложения видно, что одна версия функции get_number затирается другой.

Дело в том [6; Dynamic Linking and Loading, Comparison of dynamic linking approaches], что GNU ld & ELF не использует SONAME или имя файла в качестве пространства имён для импортируемых символов:
если разные библиотеки экспортируют сущности с одними и теми же именами, то одни из них будут перетирать другие и в лучшем случае программа упадёт.

Случай, когда одна из библиотек суть статическая, решается просто: все символы статической библиотеки должны быть скрыты [7, 2.2.2 Define Global Visibility].

К сожалению, в случае динамических библиотек не всё так просто. У компоновщика/загрузчика GNU отсутствует такая функциональность, как прямое связывание [8]. Кто-то пилил эту возможность в Генту [9], но кажется, всё заглохло. В Солярке она есть [10][11], но сама Солярка сдохла…

Одним из возможных вариантов является версионирование самих символов [7, 2.2.5 Use Export Maps]. На самом деле это больше похоже на декорирование символов. (Можно только представлять, что сейчас кричит читатель, программирующий на Си++...)

Данный способ заключается в том, чтобы создать так называемый версионный сценарий, в котором перечислить все экспортируемые и скрытые сущности [12][13]. Пример сценария из version-0.3:
VERSION_0.3 {
    global:
        get_version;
        get_version2;
        get_number;
    local:
        *;
};

На этапе компоновки указать данный файл с помощью параметра --version-script=/path/to/version.script. После этого приложение, которое будет связано с такой либой получит в NEEDED version-0.3.dll, а в таблице импорта неопределённый символ get_number@@VERSION_0.3, хотя в заголовочных файлах по-прежнему будет просто int get_number().

Натравите nm на любую программу, которая использует glibc, и вы прозреете!

Чтобы собрать пример с использованием версионирования символов в библиотеках version-0.x запустите корневой сценарий linux_make_good.sh с параметром use_version_script:
$ ./linux_make_good.sh use_version_script
$ ./result/main.exe
Hello World!
bar library uses libversion 0.2.0, number = 2
version::get_version result = 0
But I uses liversion 0.3.0
number = 3

$ nm ./result/main.exe
// …
                 U get_number@@VERSION_0.3
                 U get_version@@VERSION_0.3
0000000000401008 T main
                 U memset@@GLIBC_2.2.5

$ nm ./result/bar.dll
// …
                 U get_number@@VERSION_0.2
                 U get_version2@@VERSION_0.2
0000000000000800 t register_tm_clones
                 U strcat@@GLIBC_2.2.5

http://nooooooooooooooo.com/
  • Эй, Дарт! Наша libX будет поддерживать Линукс!
  • Noooooooooooooooooooooooooo!


Да, после фейла, с которым наша команда столкнулась, капитан принял волевое решение и теперь используется только одна версия либы (именно из-за Линукса).

Как обстоят дела на Маке?

Мак Ось использует формат Mach-o для исполняемых файлов, а для поиска символов двух-уровневое пространство имён [14, Two-level namespace][16]. Это по-умолчанию сейчас, но можно собрать с плоским пространством имён или вообще отключить его при запуске программы [15, FORCE_FLAT_NAMESPACE]. Узнать, собран ли бинарник с поддержкой пространства имён поможет команда:
$ otool -hv /path/to/binary/file

То есть не надо париться с каким-то дополнительным декорированием имён – просто включить версию в имя файла!

А что же с поиском зависимостей?

В макоси почти всё аналогично, только называется по-другому.

Вместо SONAME есть id библиотеки или install name. Просмотреть можно, например, так:
$ otool -D /usr/lib/libstdc++.dylib
Изменить можно с помощью install_name_tool.
При связывании с библиотекой её id прописывается в бинарнике.

Просмотреть зависимости бинарника можно так:
$ otool -L /path/to/main.exe
или
$ dyldinfo -dylibs /path/to/main.exe

При запуске dyld пытается открыть файл с именем «id» [15, DYNAMIC LIBRARY LOADING], т. е. рассматривает install name как абсолютный путь к зависимости. Если потерпел неудачу – то ищет файл с именем/суффиксом «id» в каталогах, перечисленных в переменной окружения DYLD_LIBRARY_PATH (полный аналог LD_LIBRARY_PATH).

Если поиск в DYLD_LIBRARY_PATH не дал результатов, то dyld аналогично просматривает ещё парочку переменных окружения [15], после чего поищет либу в стандартных каталогах.

Такая схема не позволяет собирать перемещаемые приложения, поэтому была введена специальная метка, которую можно прописывать в id: @executable_path/. Эта метка во время загрузки будет развёрнута в абсолютный путь до исполняемого файла.

Далее, можно поменять зависимости у готового бинарника:
$ install_name_tool -change /usr/lib/libstdc++.dylib @executable_path/libstdc++.dylib main.exe

Теперь загрузчик сначала будет искать эту либу в той же папке, где и main.exe. Чтобы не менять в готовом бинарнике, надо во время компоновки подсунуть либу libstdc++.dylib, у которой id = @executable_path/libstdc++.dylib.

Далее, возникает одна проблема, а точнее две. Пусть есть такая иерархия:
  • main.bin
  • tools/
    • auxiliary.bin
  • library.dll

main.bin зависит от library.dll, но и tools/auxiliary.bin зависит от неё же.
При этом id либы = @executable_path/library.dll, и оба бинарника были просто с ней скомпонованы. Тогда при запуске auxiliary.bin загрузчик будет искать /path/to/tools/library.dll и естественно не найдёт! Конечно можно ручками после компоновки подправить tools/auxiliary.bin или кинуть мягкую ссылку, но опять неудобства!

Ещё лучше проблема проявляет себя, когда речь заходит о подключаемых модулях (plugins):
  • main.bin
  • plugin/
    • 1.plugin
    • helper.dylib

1.plugin имеет запись @executable_path/helper.dylib, но во время запуска она развернётся в абсолютный путь до main.bin, а не 1.plugin!

Для решения этой проблемы яблочники с версии Оси 10.4 ввели новый маркер: @loader_path/. Во время загрузки зависимости, этот маркер развернётся в абсолютный путь к бинарнику, который дёргает зависимость.

Последняя сложность заключается в том, что надо две версии связываемых библиотек: одни будут устанавлены в систему, и иметь id = /usr/lib/libfoo.dylib, а другие использованы для сборки проектов, и их id = @loader_path/libfoo.dylib. Это легко решить с помощью install_name_tool, но утомительно; поэтому с версии 10.5 ввели метку @rpath/. Библиотека собирается с id = @rpath/libfoo.dylib и копируется куда угодно. Бинарник собирается со списком путей для поиска зависимостей, в котором разрешено использовать @{executable, loader}_path/:
$ gcc ... -Xlinker -rpath -Xlinker '@executable_path/libs' -Xlinker -rpath -Xlinker '/usr/lib' ...

Это аналогично RPATH/RUNPATH для ELF. При запуске бинарника строка @rpath/libfoo.dylib будет развёрнута в @executable_path/libs/libfoo.dylib, которая уже развернётся в абсолютный путь. Либо развернётся в /usr/lib/libfoo.dylib.

Просмотреть зашитые в бинарник rpath'ы можно так:
$ otool -l main.bin | grep -A 2 -i lc_rpath

Удалить, изменить или добавить rpath'ы можно с помощью install_name_tool.

Проверяем на примере:
$ ./macosx_make_good.sh 
Building version-0.2
Building version-0.3
Building bar
Building fooapp
$ ./result/main.exe 
Hello World!
bar library uses libversion 0.2.0, number = 2
version::get_version result = 0
But I uses liversion 0.3.0
number = 3

На айОС всё так же.

Как видно из примера, Mac OS X в плане динамических библиотек лучше Linux & Co.

И наконец, Windows!

Тут тоже всё хорошо [6; Dynamic Linking and Loading, Comparison of dynamic linking approaches]. Надо только добавить версию в имя файла и… симлинков нет! То есть они есть, но на них многие жалуются и работают они только на NTFS (Windows XP точно можно установить на FAT раздел). Следовательно, обратная совместимость может стоить приличного места на диске… Ну и ладно. )

Чтобы собрать пример на Windows потребуется запустить консоль Visual Studio, в которой уже будет настроено окружение. Далее сборка и запуск:
> .\windows_make_good.bat
// ...
>.\result\main.exe
Hello World!
bar library uses libversion 0.2.0, number = 2
version::get_version result = 0
But I uses liversion 0.3.0
number = 3

Либы ищутся только так [17]. Одним из возможных способов смены алгоритма поиска зависимостей является использование файла настроек приложения (application configuration file) и свойства privatePath у probing [18]. Однако данный способ применим только начиная с Windows 7/Server 2008 R2.

А ещё есть WinSxS и так называемые сборки (assemblies) [19]. Это – тема отдельной статьи. Однако пока писалась эта статья, снизошло озарение и понимание, что эти самые сборки нужны лишь для того (по крайней мере, Сишникам и Си++никам) чтобы все приложения компоновались, скажем, с comdlg32.dll, но все использовали разные версии.

Заключение

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

Основным решением является выбор правильного версионирования и контроля за ним.

В то время, как Curiosity бороздит марсианские просторыавтор пытался здесь поведать о том, как избежать затирания символов, на хабре уже давно есть статьи, где рассказано, как специально добиться обратного: habrahabr.ru/post/106107, habrahabr.ru/post/115558.

П.С. Пока велась работа над данной статьёй, автор посетил конференцию “На стачку!”, где послушал доклад К. Назарова из Parallels о версионировании [20]. Ничего неожиданного или необычного там не прозвучало, но было приятно услышать, что в такой известной компании осознали проблему и сделали правильные выводы. Из нового для себя автор вынес оттуда ссылку: semver.org.

Пользуясь возможностью, хочу поблагодарить своих коллег Александра Сидорова и Александра Прокофьева за конструктивную критику и ценные замечания!

Ссылки

  1. ^  QtMultimedia changes-5.0.1
  2. ^  http://en.wikipedia.org/wiki/Soname.
  3. ^  Program Library HOWTO, 3.1.1. Shared Library Names.
  4. ^  man ld-linux.so.
  5. ^  http://en.wikipedia.org/wiki/Rpath.
  6. ^ 1 2 Linkers and Loaders by John R. Levine, http://www.iecc.com/linker/.
  7. ^ 1 2  How To Write Shared Libraries by Ulrich Drepper, http://www.akkadia.org/drepper/dsohowto.pdf(pdf).
  8. ^  http://en.wikipedia.org/wiki/Direct_binding.
  9. ^  https://bugs.gentoo.org/show_bug.cgi?id=114008.
  10. ^  https://blogs.oracle.com/msw/date/20050614.
  11. ^  http://cryptonector.com/2012/02/dll-hell-on-linux-but-not-solaris/.
  12. ^  https://sourceware.org/binutils/docs/ld/VERSION.html.
  13. ^  http://www.tux.org/pub/tux/eric/elf/docs/GNUvers.txt.
  14. ^  man ld.
  15. ^ 1 2 3 man dyld.
  16. ^  http://en.wikipedia.org/wiki/Mach-O#Mach-O_file_layout.
  17. ^  MSDN, Dynamic-Link Library Search Order, http://msdn.microsoft.com/en-us/library/windows/desktop/ms682586%28v=vs.85%29.aspx.
  18. ^  http://stackoverflow.com/a/10390305/1758733.
  19. ^  http://en.wikipedia.org/wiki/WinSXS.
  20. ^  Назаров К., Экстремально предвзятый взгляд на версионирование программных продуктов, http://nastachku.ru/lectures#lecture_178.
  21.  Oracle, Linker and libraries Guide, http://docs.oracle.com/cd/E19683-01/817-1983/index.html.
  22.  Руководство новичка по эксплуатации компоновщика, http://habrahabr.ru/post/150327/.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 19

    +1
    К сожалению, часто бывает, что сторонний продукт bar линкуется с version.so, которая ссылается на version.<latest>.<latest>.so.

    Теперь, вы не можете просто взять и поставить новый version, потому что ссылка version.so перетрется при установке на последнюю версию и bar перестанет работать.

    Особенно весело будет, если вы хотите использовать сторонние библиотеки foo и bar, каждая из которых линкуется с version.so, но обе подразумевают разные версии этой самой version.so. Вот тут-то и начинаются проблемы.
      +2
      +1. Этим вообще грешат многие: как сами разработчики либ, так и создатели различных ОС с ядром Линукс и другие.

      С Вашей подачи забью камень в огород гугла:
      $ objdump -x chrome|grep NEEDED|grep nss3
        NEEDED               libnss3.so
      $ ./chrome
      ./chrome: /usr/lib64/seamonkey/libnss3.so: version 'NSS_3.14.3' not found (required by ./chrome)
      $ objdump -x /usr/lib64/seamonkey/libnss3.so |grep SONAME
        SONAME               libnss3.so
      

      У меня slackware64-14.0. Я же просто скачал хром с сайта гугла (google-chrome-stable_current_amd64.deb), т.к. хромиум мне ой как не хотелось компилировать. Как видите, меня ждал сюрприз. Решить его я могу только с помощью LD_LIBRARY_PATH, устанавливая первыми пути с нужными библиотеками, т.к. R(UN)PATH тоже не был предусмотрен (в противном случае я бы просто прокинул нужные ссылки в заданное место):
      $ objdump -x chrome|egrep 'R(\|UN)PATH'
      $ 
      


      Ссылки вида libX.so → libX.x.y.z.so – это вообще анахронизм, т.к. нужны только компоновщику во время связывания, чтобы явно не писать имя файла библиотеки, а просто указать -lx. Новый gcc & ld умеет прямо компоновать с нужным файлом: -l:x.so.3.2.1. Разве что на Маке я не нашёл такой возможности. (Дурацкий префикс 'lib' тоже не нужен).

      Это – пол беды. Задавать SONAME тоже никто не хочет (посмотрите выше на nss3 в слакваре): т.е. даже если целевой файл будет скомпонован с нужной версией либы, в его зависимостях будет просто libx.so (см. выше вывод для хрома).

      Ещё хочу упомянуть про pkgconfig. Для nss3 на моей системе файлик выглядит так:
      prefix=/usr
      exec_prefix=${prefix}
      libdir=/usr/lib64
      includedir=${prefix}/include/nss
      
      Name: NSS
      Description: Network Security Services
      Version: 3.13.5
      Requires: nspr >= 4.9.1 sqlite3
      Libs: -L${libdir} -lnss3 -lsmime3 -lssl3 -lsoftokn3  -lnssutil3
      Cflags: -I${includedir}
      

      а называется он nss.pc. Если кто-то воспользуется услугами pkgconfig для nss3, то с какой версией ssl3, smime3, softokn3, nssutil3 будут скомпонованы библиотеки?
        +2
        Вы написали гневную тираду на совершенно другую тему (впрочем близкую). Описанные вами проблемы — это просто-напросто отсуствие у Linux'а полноценного SDK.

        Какая-нибудь MSVCRT.DLL или USER.DLL в Windows устроены ровно так же, как libnss3.so в Linux. И если вы соберёте вашу программу так, что она будет вызвывать из этих библиотек функции, которых нет в Windows XP (или в Windows 7), то чёрта с два вы такую программу на Windows XP (или Windows 7) запустите.

        Но в Windows этому уделяется особое внимание, людям предоставлена возможность собрать с помощью MSVC 2013 программы, которые будут запускаться на Windows XP. Ни о чём таком в Linux никто даже не думает, увы. Ну за исключением разработчиков LSB, но если вы посмотрите на оный LSB, то обнаружите, что там кучи библиотек, нужных для разработки программ типа Chrome нету, а о сколько-нибудь полноценной поддержке C++11 даже и мечтать не приходится.
      +3
      Вы не зря тут упомянули про C++. Но если уж упомянули, то стоит сказать и как с ним-то быть. А то можно подумать, что все на чистом C работают. У людей, работающих с C++ использование version script'а не прокатит из-за шаблонов (они описаны в заголовочных файлах и если liba использовала при сборке version-1.0.h, а libb — version-1.1.h, то это значит что у них внутри сгенерировались одинаковые символы для каких-ниюудь std::list<version::Window>::size()).

      Для решения этой проблемы следует использовать inline namespace. Если приходится разрабатывать проект без поддержки C++11, то к вашим услугам (на MacOS и Linux) strong using появившийся в GCC 3.4 (не 4.4, а именно 3.4) — собственно он явился прототипом для inline namespace.
        0
        Вы задали очень хороший вопрос. Я сейчас пробежался по статье и, действительно, как-то упустил из виду Си++.

        Я думаю, что можно. Попробую аргументировать свою позицию.

        Есть несколько вариантов:
        1) version экспортирует только шаблоны, т.е. никаких сгенерированных классов наружу не торчит;
        2) version экспортирует шаблоны, а также некоторые классы, сгенерированные с помощью этих шаблонов.

        Сначала разберу второй случай. Проблема здесь заключается в том, что когда liba включит version-1.0.h
        // ...
        typedef version::list<version::window> windows_list;
        // ...
        

        к себе в исходный текст, то дальнейшее развитие событий зависит от того, как оформлен шаблон version::list. Если он – хедер-онли, то всё плохо:
        1) во-первых, в самой version.dll будет присутствовать сгенерировнный класс version::list<version::window>;
        2) когда будет компилироваться сама либа а, то в той единице трансляции, в которую был включен version.h, также будет сгенерирован класс version::list<version::window>. Он будет скомпилирован, в лучшем случае бинарно совместим с классом в version.dll, и все ссылки на него будут не undefined (что заставило бы загрузчик искать эти символы во время загрузки), а определены и указывать на сущности внутри а (в терминах nm – T либо t).

        Даже если класс version::list<version::window> и был версионирован в version.dll, он окажется в непонятном состоянии в liba. (Тут, кстати, стоит отметить, что именно по этой причине некоторые люди используют AddRef/Release семантику для экспортируемых классов, а также, не экспортируют stl-классы: нагенерреные классы внутри самой либе и её пользователе отличаются бинарно даже в случае Debug/Release )) ).

        Чтобы решить эту проблему для C++03 и выше, шаблон надо разделить на объявление и определение: version_list.h, version_list_impl.h. Внутри самой либы в файле реализации version_windows_list.cpp будет примерно так:
        #include "version_windows_list.hpp"
        
        // подключаем тело шаблона
        #include "version_list_impl.h"
        
        // явно инстанцируем шаблон
        template class version::list<version::window>;
        
          0
          случайно отправил комментарий.

          Теперь, при сборке самой version.dll будет использован version.script для символов vesion::list<version::window> внутри самой библиотеки. liba и другие при компиляции не получат тело шаблона version::list, что заставит компоновщик требовать его при связывании. Таким образом, все получат в зависимостях версионированный шаблонный класс. Эта техника разделения шаблона описана тут: http://www.parashift.com/c++-faq/templates-defn-vs-decl.html.

          Начиная с Си++11 можно заставить компилятор не инстанцировать шаблон даже для хедер-онли шаблонов с помощью extern. Например, для std::unique_ptr:
          // MyTypePtr.hpp
          #include <memory>
          
          namespace mylib
          {
          
          class MyType;
          
          } // namespace mylib
          
          extern template class std::unique_ptr<mylib::MyType>;
          
          namespace mylib
          {
          
          using MyTypePtr = std::unique_ptr<mylib::MyType>;
          
          } // namespace mylib
          
          
          // MyTypePtr.cpp
          
          #include "MyTypePtr.hpp"
          
          #include "MyType.hpp"
          
          template class std::unique_ptr<mylib::MyType>;
          


          Такая техника позволит экспортировать даже классы, сгенеренные из хедер-онли шаблонов, и применить для этих классов version.script. Стоит отметить, что extern уже давно использовался как расширение Си++ в msvc: http://support.microsoft.com/kb/168958/en-us.

          К сожалению, с помощью extern не получится экспортировать контейнер перемещаемых объектов: http://stackoverflow.com/questions/22381685/extern-template-class-stdcontainer-of-movable-objects.

          Теперь вернёмся к первому случаю, когда экспортируются только шаблоны. Как видим, эти шаблоны должны быть грамотно оформлены )). С точки зрения version, это проблема других, как они используют эти шаблоны. Если высовывают их наружу – это первый случай, только с позиции уже другой либы. Если используют для внутренних нужд – то с помощью того же version.script они должны быть помечены как local.
            0
            Теперь, при сборке самой version.dll будет использован version.script для символов vesion::list<version::window> внутри самой библиотеки. liba и другие при компиляции не получат тело шаблона version::list, что заставит компоновщик требовать его при связывании.
            Флаг вам в руки и барабан на шею. Поробуйте проделать это с Boost'ом или каким-нибудь Eigen'ом, потом возвращайтесь расскажете про результаты.

            С точки зрения version, это проблема других, как они используют эти шаблоны.
            Почему тогда не считать что проблемы версионированных символов, SONAME и прочего — это тоже «проблема других»? Наши тестовые программы работают — и ладно. Пусть всё через dlopen грузят, им жалко, что ли?
            0
            Даже если класс version::list<version::window> и был версионирован в version.dll, он окажется в непонятном состоянии в liba.
            Вот собственно тут вы и ошибаетесь. Если version::list<version::window> был версионирован в version.dll с использованием inline namespace<code> (или <code>string using), то в liba он тоже будет версионирован.
          +3
          Насколько я знаю, в Windows проблема разных библиотек но одного символа — вообще не стоит.

          Дело в том, что dll не позволяет undefined символы, как линуксе. Т.е. линковка идет при компиляции, и зависимости намертво пришиваются.
          Далее, при загрузке dll, в зависимых от нее приложениях и модулях патчатся стабы в таблице импорта(для функций например — добавляется инструкция прыжка на адрес в области памяти занимаемой образом конкретной заданной dll).
          Т.е. символ импортированный из конкретного модуля всегда будет указывать на символ в этом конкретном модуле.

          Далее, чтобы избежать версионирования имен dll — как раз таки можно использовать SxS, он для этого и предназначен.
          Для этого пишется manifest, который либо складывается в секцию .rsrc у dll/exe, либо же кладется рядом с приложением/dll в виде файла ${имя модуля включая суффикс exe/dll}.manifest
          В этом манифесте прописываются зависимости.

          Т.е. все намного проще и удобнее, чем в других ОС, на самом деле.
            +1
            Разумеется проблема не стоит. Вы даже выделить объект в одной библиотеке, а потом удалить в другой не можете, какие уж тут шаблоны.

            Но это как бы… лечение насморка с помощью гильотины.
              –2
              Вы даже выделить объект в одной библиотеке, а потом удалить в другой не можете, какие уж тут шаблоны.


              Выделять память нужно стандартным HeapAlloc(а потом делать placement new), и/или же предоставлять функции для удаления объекта из библиотеки. Т.е. эта проблема надуманная, т.к. создавать в одной библиотеке объект а потом удалять его как попало — это дурной тон и ССЗБ.

              Во-вторых, специально для C++, который может быть сам с собой не совместим даже в случае разных минорных версий компилятора — в MS придумали COM, в котором и ABI, и менеджмент памяти стандартизированный, и т.д.
                +1
                Выделение памяти в одной библиотеке, а удаление в другой возникает совершенно естественным путём при использовании шаблонов. Которое не менее естественно возникает при использовании стандартной библиотеки C++. Какой-нибудь простейший возврат std::string'а из функции уже приводит к этому эффекту.

                Все ваши рассказы про HeapAlloc, COM и прочее — это как раз и есть описание той самой гильотины. Это уже не нормальный C++-интерфейс библиотеки (как он описан в стандартах языка C++), а некоторое жалкое подобие поверх Microsoft'овских технологий.

                Да, это решает проблему разных версий библиотек — но какой ценой?
                  0
                  Это проблемы C++ — нет ABI, нет стандартизированного name mangling, и т.п.

                  Т.к. мы, простые смертные, не влияем на принятие стандарта C++, то мы используем к примеру COM. Моя позиция по этому вопросу такая — COM конечно не самое удобное в мире изобретение, особенно если смотреть из C++, но оно, в отличие от стандартного C++ — изобретение работающее, а это решающий фактор, уж извините.
                    +1
                    А вот с этого момента: поподробнее. С чего вы решили, что если у вас нет стандартного C++, то его и ни у кого нет?

                    В MacOS и в Linux'е, к примеру, он есть. c++filt входит в состав системы, стандартный ABI вполне себе существует и всё, в общем, вполне себе работает. Есть, конечно, проблемы свяанные с тем, что программисты соответствующие инструменты неправильно используют, ну так от этого и COM не защищает.

                    Я понимаю, что вам нравится бегать за Microsoft'ом и лизать ему задницу, но есть же какие-то пределы. Называть несуществующими вещи, с которыми вы можете столкнуться на большинстве платформ и говорить, что нужно вместо этого использовать нестандартное решение, доступное только на одной из них — это уже клиника какая-то.
            0
            Т.е. все намного проще и удобнее, чем в других ОС, на самом деле.

            С этим согласен полностью.

            К manifest«ам это не относится. Пока писал статью, я понял, что в Windows изначально был механизм (который Вы описали и который является two-level namespace по сути), чтобы не допустить dependency hell: мы бы имели в папке system32, к примеру, два файла comctl32-5.0.2.dll и comctl32-6.3.1.dll. Почему инженеры из Microsoft не сделали этого изначально? Я не знаю.

            Лично мне механизм с манифестами кажется избыточным.
              +1
              Манифесты на самом деле досточно удобны. Мне кажется, лучше пусть будет куча разных dll в специальном кеше и одна псевдо-dll в условном system32 или %programfiles%/%App%, чем та же куча в разных местах и с разными именами.
              Кроме версий dll, они могут версионировать COM-компоненты.
              Могут указывать где искать компоненты/dll, и т.п.

              Ну и вообще, формат расширяемый, т.к. xml, и поэтому через них можно много чем управлять. Например, уровнями integrity(aka MAC, aka UAC).

              Не отрицаю, что с первого взгляда они кажутся избыточными и переусложненными. Но, манифесты действительно решают проблемы, которые на других системах не решены никак.
                0
                Если манифеста нет как определять какая мне версия нужна, самая новая по тому что в ней больше всего реализовано, или самая старая раз я настолько динозавр что даже манифеста нет? OS Винда как настоящий партизан отдает мне то что считает совместимым с моим экзешником да ещё и окружение подменяет так что я даже не всегда адекватно могу узнать что мне подсунули! Как тот же юак распознавать если тебя под 98SE написали? А если как сисадмин считаю что в системеме должна быть одна и только одна версия библиотеки по тому что в ней мой патч/у старых версий проблемы с безопасностью/мало памяти? А если я как сисадмин хочу чтобы библиотека физически находилась не в том месте где это решил разработчик? А если у меня наоборот куча старого и не очень софта ещё без манифестов, но уже использующие несовместимые версии либы? Лично меня линуксовое решение куда больше устраивает.
                  0
                  Некропост, не удержался.
                  Если манифеста нет как определять какая мне версия нужна, самая новая по тому что в ней больше всего реализовано, или самая старая раз я настолько динозавр что даже манифеста нет?

                  Второе. Подсовывается либа времён Win2000. В \System32\ так же лежит она, даже в десятке. Всё что новее лежит в \WinSxS\, по сути, ОС стоит там, а в \System32\ лежат хардлинки для совместимости.
                  Как тот же юак распознавать если тебя под 98SE написали?

                  Если права нужны, то простой запуск запросит повышения прав, если они не нужны, то нужно рядом положить специальный манифест.
                  А если как сисадмин считаю что в системеме должна быть одна и только одна версия библиотеки по тому что в ней мой патч/у старых версий проблемы с безопасностью/мало памяти?

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

                  Можно положить рядом, она подхватится.
                  А если у меня наоборот куча старого и не очень софта ещё без манифестов, но уже использующие несовместимые версии либы?

                  Им подсунется старая, см. выше.
              0
              А по основной теме добавлю, считаю что ставить в систему какую-либо библиотеку вместе со своим софтом имеет право только разраб библиотеки, предварительно приняв ряд мер по обеспечению совместимости, в край мантейнер дистра. Используешь в проекте что-то чего нет у 99% пользователей — клади свои версии либы себе под нос, желательно при установке в лог маякни, что мол в системе несовместимая со мной версия, глядишь админ что-то да придумает чтоб и пол оси не разносить и в памяти 10 версий того-же QT-а с вебкиттом не висело. В том же линуксе никто не мешает определить в зависмостях паккета ту либо что должна быть в системе, а все чего дистром не предусмотрено складывать к себе /опт/шаре. Если же вы сами пишите действительно разделяемую либу, которой для функционирования продакшн версии вашего же пепелаца нужно сразу несколько версий… ну пфффф.

              Only users with full accounts can post comments. Log in, please.