2.3.4. Зависимости между библиотеками
Библиотеки часто связаны одна с другой. Например, во многих Linux-системах есть библиотека libtiff, содержащая функции чтения и записи графических файлов формата TIFF. Она, в свою очередь, использует библиотеки libjpeg (подпрограммы обработки JPEG-изображений) и libz (подпрограммы сжатия).
В листинге 2.9 показана небольшая программа, использующая функции библиотеки libtiff для работы с TIFF-файлом.
Листинг 2.9. (
tifftest.c) Применение библиотеки libtiff
#include <stdio.h>
#include <tiffio.h>
int main(int argc, char** argv) {
TIFF* tiff;
tiff = TIFFOpen(argv[1], "r");
TIFFClose(tiff);
return 0;
}
При компиляции этого файла необходимо указать флаг -ltiff:
% gcc -о tifftest tifftest.c -ltiff
По умолчанию будет скомпонована совместно используемая версия библиотеки: /usr/lib/libtiff.so. В связи с тем что она обращается к библиотекам libjpeg и libz (одна совместно используемая библиотека может ссылаться на другие аналогичные библиотеки, от которых она зависит), будут также подключены их совместно используемые версии. Чтобы проверить это, воспользуемся командой ldd:
% ldd tifftest
libtiff.so.3 => /usr/lib/libtiff.so.3 (0x4001d000)
libc.so.6 => /lib/libc.so.6 (0x40060000)
libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x40155000)
libz.so.1 => /usr/lib/libz.so.1 (0x40174000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
В противоположность этому статические библиотеки не могут указывать на другие библиотеки. Если попытаться подключить к программе статическую версию библиотеки libtiff, указав в командной строке опцию -static, компоновщик столкнется с нераспознаваемыми символическими константами:
% gcc -static -о tifftest tifftest.с -ltiff
/usr/bin/../lib/libtiff.a(tif_jpeg.o): In function
'TIFFjpeg_error_exit':
tif_jpeg.о(.text+0x2a): undefined reference to 'jpeg_abort'
/usr/bin/../lib/libtiff.a (tif_jpeg.o): In function
'TIFFjpeg_create_compress':
tif_jpeg.o(.text+0x8d): undefined reference to 'jpeg_std_error'
tif_jpeg.o(.text+0xcf): undefined reference to
'jpeg_CreateCompress'
...
В случае статической компоновки программы нужно самостоятельно указать две другие библиотеки:
% gcc -static -o tifftest tifftest.c -ltiff -ljpeg -lz
Иногда между двумя библиотеками образуется взаимная зависимость. Другими словами, первый архив ссылается на символические константы, определенные во втором архиве, и наоборот. Такая ситуация, как правило, является следствием неправильного проектирования. В этом случае нужно указать в командной строке одну и ту же библиотеку несколько раз. Компоновщик просмотрит библиотеку столько раз, сколько она присутствует в командной строке. Рассмотрим пример:
% gcc -o app арр.о -lfoo -lbar -lfoo
Теперь, даже если библиотека libfoo.a ссылается на символические константы в библиотеке libbar.a и наоборот, программа будет успешно скомпонована.
2.3.5. Преимущества и недостатки библиотек
Познакомившись со статическими архивами и совместно используемыми библиотеками. читатели, очевидно, задумались: какие же из них лучше использовать? Есть несколько важных моментов, о которых следует помнить.
Большим преимуществом совместно используемой библиотеки является то. что она экономит место на диске при инсталляции программы. Когда устанавливаются десять программ и все они работают с одной и той же библиотекой, экономия может оказаться весьма существенной, тогда как статический архив будет включен во все десять программ. Уменьшается также время загрузки, если программа загружается из Internet.
С этим связано еще одно преимущество совместно используемых библиотек: пользователи могут обновлять библиотеки, не затрагивая связанные с ними программы. Предположим, к примеру, что была создана библиотека, содержащая функции управления HTTP- соединениями. Потенциально с ней может работать множество программ. Если впоследствии в библиотеке обнаружится ошибка, достаточно будет просто заменить ее файл, и все программы, использующие данную библиотеку, немедленно обновятся. Не придется выполнять перекомпоновку всех программ, как в случае статического архива.
Описанные преимущества могут заставить читателей подумать, будто статические архивы бесполезны. Но их существование обусловлено вескими причинами. Тот факт, что обновление совместно используемой библиотеки отражается на всех связанных с нею программах, может на самом деле оказаться недостатком. Некоторые программы тесно связаны с используемыми библиотеками и не должны зависеть от произвольных изменений в системе.
Если библиотеки не должны инсталлироваться в каталог /lib или /usr/lib, нужно дважды подумать, стоит ли их делать совместно используемыми. (Библиотеки нельзя помещать в указанные каталоги, если предполагается, что программу будут инсталлировать пользователи, не имеющие привилегий системного администратора.) В частности, прием с флагом -Wl,-rpath не будет работать, поскольку не известно, где именно окажутся библиотеки. А просить пользователей устанавливать переменную LD_LIBRARY_PATH — не выход из положения, так как это означает для них выполнение дополнительного (для некоторых — не самого тривиального) действия.
Оценивать преимущества и недостатки двух типов библиотек нужно для каждой создаваемой программы отдельно.
2.3.6. Динамическая загрузка и выгрузка
Иногда на этапе выполнения программы требуется загрузить некоторый код без явной компоновки. Рассмотрим приложение, поддерживающее подключаемые модули: Web-броузер. Архитектура броузера позволяет сторонним разработчикам создавать дополнительные модули, расширяющие функциональные возможности броузера. Модуль реализуется в виде совместно используемой библиотеки и размещается в заранее известном каталоге. Броузер автоматически загружает код из этого каталога.
Для этих целей в Linux существует специальная функция dlopen(). Например, открыть библиотеку libtest.so можно следующим образом:
dlopen("libtest.so", RTLD_LAZY)
Второй параметр — это флаг, определяющий способ привязки символических констант в библиотеке. Данная установка подходит в большинстве случаев. Подробнее узнать о ней можно в документации.
Объявление функций работы с динамическими библиотеками находится в файле <dlfcn.h>. Использующие их программы должны компоноваться с флагом -ldl, обеспечивающим подключение библиотеки libdl.
Функция dlopen() возвращает значение типа void*, используемое в качестве дескриптора динамической библиотеки. Это значение можно передавать функции dlsym(), которая возвращает адрес функции, загружаемой из библиотеки. Например, если в библиотеке libtest.so определена функция my_function(), то она вызывается следующим образом:
void* handle = dlopen("libtest.so", RTLD_LAZY);
void (*test)() = dlsym(handle, "my_function");
(*test)();
dlclose(handle);
С помощью функции dlsym() можно также получить указатель на статическую переменную, содержащуюся в совместно используемой библиотеке.
Обе функции, dlopen() и dlsym(), в случае неудачного завершения возвращают NULL. В данной ситуации можно вызвать функцию dlerror() (без параметров), чтобы получить текстовое описание возникшей ошибки.
Функция dlclose() выгружает совместно используемую библиотеку. Строго говоря, функция dlopen() загружает библиотеку лишь в том случае, если она еще не находится в памяти. В противном случае просто увеличивается число ссылок на файл. Аналогичным образом функция dlclose() сначала уменьшает счетчик ссылок, и только если он становится равным нулю, выгружает библиотеку.
Когда совместно используемая библиотека пишется на C++, имеет смысл объявлять общедоступные функции со спецификатором extern "С". Например, если функция my_function() написана на C++ и находится в совместно используемой библиотеке, а нужно обеспечить доступ к ней с помощью функции dlsym(), объявите ее следующим образом:
extern "С" void my_function();
Тем самым компилятору C++ будет запрещено подменять имя функции. При отсутствии спецификатора extern "С" компилятор подставит вместо имени my_function совершенно другое имя, в котором закодирована информация о данной функции. Компилятор языка С не заменяет имена; он работает с теми именами, которые назначены пользователем.
Выполняющийся экземпляр программы называется процессом. Если на экране отображаются два терминальных окна, то, скорее всего, одна и та же терминальная программа запущена дважды — ей просто соответствуют два процесса. В каждом окне, очевидно, работает интерпретатор команд — это еще один процесс. Когда пользователь вводит команду в интерпретаторе, соответствующая ей программа запускается в виде процесса. По завершении работы программы управление вновь передается процессу интерпретатора.